Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,32 @@

A project about having co-operation between Sofa and Blender.

Once the blender plugin is activated in the blender interface.
User has the choice between to way to have sofa and blender to interact.
# Features
- build and generate sofa simulation using a dedicated node interface
- importing result of a Sofa simulation in blender either using Client-server's mode or Disk Baking

# Client-server's mode
# Node base editor for Sofa Simulation

The editor allow to edit a full Sofa Scene, save it to disk.
- direct use of Blender's object for geometrical editting
- any Sofa object supported
- UX support for Prefab
- UX support for Controllers

The linking magics rules:
- self to src => similar behavior as the one in Sofa (linking any socket with similar name)
- self to "+" => add an src socket
- named socket to "+" => add a socket on the "+" side with similar name and type as the dragged from
- "+" => add socket from the list of socket associated to a component or create a complete new one


# Importing a Sofa simulation's run in blender
## Client-server's mode
This solution transmit the data over a network connexion.
A dedicated component must be added to the scene in charge of streaming the simulation data over the network.
This component is implemented in c++ and compiled as a binary plugin for Sofa.

# Disk baking solution.
## Disk baking solution.
Instead to transmit data over a network connexion between Sofa and Blender it is alternatively
possible to bake (compute) Sofa simulation and store the simulation data on disk.
The current implementation is using python and thus works with the binary releases of Sofa 23.12.
Expand Down
22 changes: 22 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Things to do:
- [X] add "generic" component with a customizable "className"
- [] fix adding a template on output (shows the value... even if there is an input)
- [] fix problem: the group outputs are not correctly saved in the .py file (invalid type, only string) and no internal parenting
- [] save generic component in a fake storage
- [] fix problem: the prefab used in scenes are not saved recursively when clicking on their calling point
- [] properly handle the .sofa_blender/ configuration direction using a recursive approch
- [] when reloading the type from a Prefab, all the existing links are removed.
- [] group input does not support templates...
- [] linking template does not work... they are not data to parent.
- [] implmeent a PythonExpression that take its input to bind them and apply a function to that (see example in pep01).
- [] fix the default value of attributes that are initialized from nodes.json
- [] implement a mechanisme to handle the RequiredPlugin for a Prefab
- [] implement a mechanisme to get the SOFA error messages highlighting the corresponding nodes and socket. => put them in RED ?
- [] implement a PythonController
- [] SofaBlenderSocket => BlenderObject
- [] Add a visual feedback when invalid value
- [] BlenderObject does not report error if the blender object is missing.
- [] the "name" as input in RequiredPlugin collides with setting of names from GUI
- [] on RequiredPlugin, the pluginName does not have a text fiel for selection.
- [] SofaBlender.py is missing when exporting in a new directory
- [X] Fix the shared component type in CustomObject
35 changes: 35 additions & 0 deletions blender-plugin/addons/SofaNodeEditor/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
bl_info = {
"name": "SOFANodeEditor",
"description": "A plugin to design SOFA simulations in blender.",
"author": "Damien Marchal",
"blender": (5, 0, 0),
"category": "Object",
}

import bpy
from bpy.props import BoolProperty, StringProperty, IntProperty
from bpy.types import AddonPreferences, Operator

from . import (
panels,
operators,
sofanodes,
sofaerrors
)

class SOFANodeEditorSettings(AddonPreferences):
bl_idname = __name__

def register():
bpy.utils.register_class(SOFANodeEditorSettings)
panels.register()
operators.register()
sofanodes.register()

def unregister():
bpy.utils.unregister_class(SOFANodeEditorSettings)
panels.unregister()
operators.unregister()
sofanodes.unregister()

print("RELOAD .")
Empty file.
45 changes: 45 additions & 0 deletions blender-plugin/addons/SofaNodeEditor/minilayout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
class MiniDAGLayout:
"""Chat GPT"""
COLUMN_WIDTH = 400
ROW_HEIGHT = 180

def __init__(self, tree):
self.tree = tree

def compute_levels(self):
levels = {}
visited = set()

def dfs(node):
if node in levels:
return levels[node]

incoming = [link.from_node for link in node.inputs[0].links] if node.inputs else []

if not incoming:
levels[node] = 0
else:
levels[node] = max(dfs(parent) for parent in incoming) + 1

return levels[node]

for node in self.tree.nodes:
dfs(node)

return levels

def apply(self):
levels = self.compute_levels()

columns = {}
for node, level in levels.items():
columns.setdefault(level, []).append(node)

for level, nodes in columns.items():
count = len(nodes)
for i, node in enumerate(nodes):
y = (count - 1) * self.ROW_HEIGHT / 2 - i * self.ROW_HEIGHT
node.location = (
level * self.COLUMN_WIDTH,
y
)
57 changes: 57 additions & 0 deletions blender-plugin/addons/SofaNodeEditor/modules/SofaBlender.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# SofaBlender version 1.0
import Sofa

python2sofa = {
"<class 'str'>" : "string",
"<class 'float'>" : "float"
}

class ConstantValue(Sofa.Core.Controller):
def __init__(self, *args, **kwargs):
Sofa.Core.Controller.__init__(self, *args, **kwargs)

for k,v in kwargs.items():
if k not in ["name"]:
self.addData(name=k, type=python2sofa[str(type(v))], help="TODO", default=v)

def addObject(self, type, **kwargs):
tmp = {}
for name, value in kwargs.items():
if value is not None:
tmp[name] = value
return self.addObject(type, **tmp)


import zmq
import threading
import queue

message_queue = queue.Queue()

def zmq_listener():

context = zmq.Context()
socket = context.socket(zmq.PULL)
socket.bind("tcp://*:5555")

while True:
data = socket.recv_json()
message_queue.put(data)

thread = threading.Thread(target=zmq_listener, daemon=True)
thread.start()

class SofaBlenderLiveHook(Sofa.Core.Controller):
def __init__(self, *args, **kwargs):
Sofa.Core.Controller.__init__(self, *args, **kwargs)

def onEvent(self, event):
print("EVENT RECEIVED:", event)

def onIdleEvent(self, event):
while True:
try:
msg = message_queue.get_nowait()
print("Processing:", msg)
except queue.Empty:
pass
174 changes: 174 additions & 0 deletions blender-plugin/addons/SofaNodeEditor/nodes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
[ {
"classname" : "MechanicalObject",
"category" : "Mechanics",
"default_name" : "state",
"inputs" : [["object", "src"]],
"outputs" : []
},{
"classname" : "MeshVTKLoader",
"category" : "Loader",
"default_name" : "loader",
"inputs" : [["filepath", "filename"]],
"outputs" : []
},{
"classname" : "MeshOBJLoader",
"category" : "Loader",
"default_name" : "loader",
"inputs" : [["filepath", "filename"]],
"outputs" : []
},{
"classname" : "MeshMatrixMass",
"category" : "Mechanics",
"default_name" : "mass",
"inputs" : [["object", "mstate"],["object", "src"]],
"outputs" : []
},{
"classname" : "UniformMass",
"category" : "Mechanics",
"default_name" : "mass",
"inputs" : [["object", "mstate"],["object", "src"]],
"outputs" : []
},{
"classname" : "StiffSpringForceField",
"category" : "Mechanics",
"default_name" : "forcefield",
"inputs" : [["object", "mstate"],["object", "src"]],
"outputs" : []
},{
"classname" : "RegularGridTopology",
"category" : "Topology",
"default_name" : "modifier",
"inputs" : [["object", "src"], ["string","n"],["string","min"],["string","max"]],
"outputs" : []
},{
"classname" : "Hexa2TetraTopologicalMapping",
"category" : "Topology",
"default_name" : "convert",
"inputs" : [["object", "src"], ["object","input"],["object","output"], ["bool","swapping"]],
"outputs" : []
},{
"classname" : "TetrahedronSetTopologyModifier",
"category" : "Topology",
"default_name" : "modifier",
"inputs" : [["object", "mstate"], ["object", "src"]],
"outputs" : []
},{
"classname" : "TetrahedronSetTopologyContainer",
"category" : "Topology",
"default_name" : "topology",
"inputs" : [["object", "mstate"], ["object", "src"]],
"outputs" : []
},{
"classname" : "TetrahedronFEMForceField",
"category" : "Mechanics",
"default_name" : "forcefield",
"inputs" : [["object", "mstate"], ["object", "src"], ["float", "youngModulus"], ["float", "poissonRatio"]],
"outputs" : []
},{
"classname" : "IdentityMapping",
"category" : "Mapping",
"default_name" : "mapping",
"inputs" : [["object", "input"], ["object", "output"]],
"outputs" : []
},{
"classname" : "OglModel",
"category" : "Visual",
"default_name" : "rendering",
"inputs" : [["object", "src"]],
"outputs" : []
},{
"classname" : "BlenderObject",
"category" : "Geometry ",
"default_name" : "geom",
"inputs" : [["blender_object", "blender object"]],
"outputs" : [["filepath", "filename"],
["vector", "location"],
["vector", "orientation"],
["vector", "scale"]]
},{
"classname" : "BlenderController",
"category" : "Controller ",
"default_name" : "controller",
"inputs" : [],
"outputs" : []
},{
"classname" : "Matrix",
"category" : "Math",
"default_name" : "self",
"inputs" : [["vector", "translation"],
["vector", "rotation"],
["vector", "scale"]
],
"outputs" : [["object", "transform"]
]
},{
"classname" : "Transform",
"category" : "Math",
"default_name" : "self",
"inputs" : [["object", "transform A"],
["object", "transform B"]
],
"outputs" : [["object", "transform"]
]
},{
"classname" : "MatrixToTRS",
"category" : "Math",
"default_name" : "matrixtrs1",
"inputs" : [["object", "transform"]],
"outputs" : [["vector", "location"],
["vector", "rotation"],
["vector", "scale"]]
},{
"classname" : "TransformEngine",
"category" : "Transform",
"default_name" : "engine",
"inputs" : [["string","input_position"],
["string", "translation"],
["vector", "rotation"],
["vector", "scale"]],
"outputs" : [["string", "output_position"]]
},{
"classname" : "EulerImplicitSolver",
"category" : "Simulation",
"default_name" : "timeintegration",
"inputs" : [],
"outputs" : []
},{
"classname" : "SparseLDLSolver",
"category" : "Simulation",
"default_name" : "numericalsolver",
"inputs" : [],
"outputs" : []
},{
"classname" : "CGLinearSolver",
"category" : "Simulation",
"default_name" : "numericalsolver",
"inputs" : [["float","tolerance"],["float", "threshold"],["int", "iterations"]],
"outputs" : []
},{
"classname" : "File writer",
"category" : "Experimental",
"default_name" : "Value",
"inputs" : [],
"outputs" : []
},{
"classname" : "Pandas processor",
"category" : "Experimental",
"default_name" : "Value",
"inputs" : [],
"outputs" : []
},{
"classname" : "ConstantValue",
"category" : "Value",
"default_name" : "values",
"inputs" : [],
"outputs" : []
},
{
"classname" : "Time varing values",
"category" : "Experimental",
"default_name" : "time_values",
"inputs" : [],
"outputs" : []
}
]
Loading