From 6570c34fa558f4027572f93ecae213e544a901c4 Mon Sep 17 00:00:00 2001 From: bakpaul Date: Wed, 4 Mar 2026 22:35:35 +0100 Subject: [PATCH] Add two examples #1 and #3 --- splib/simulation/headers.py | 1 + stlib/core/baseNodeModifier.py | 169 +++++++++++++++ stlib/core/baseNodeModifier2.py | 253 +++++++++++++++++++++++ stlib/core/baseNodeModifierParameters.py | 33 +++ stlib/core/basePrefab.py | 10 +- 5 files changed, 465 insertions(+), 1 deletion(-) create mode 100644 stlib/core/baseNodeModifier.py create mode 100644 stlib/core/baseNodeModifier2.py create mode 100644 stlib/core/baseNodeModifierParameters.py diff --git a/splib/simulation/headers.py b/splib/simulation/headers.py index d3c61a21b..3de29bd28 100644 --- a/splib/simulation/headers.py +++ b/splib/simulation/headers.py @@ -31,6 +31,7 @@ def setupDefaultHeader(node, displayFlags = "showVisualModels", backgroundColor= 'Sofa.Component.Visual', ], **kwargs) + node.addObject('DefaultVisualManagerLoop') node.addObject('DefaultAnimationLoop',name="animation", parallelODESolving=parallelComputing, **kwargs) return node diff --git a/stlib/core/baseNodeModifier.py b/stlib/core/baseNodeModifier.py new file mode 100644 index 000000000..b39b23278 --- /dev/null +++ b/stlib/core/baseNodeModifier.py @@ -0,0 +1,169 @@ + +import splib.mechanics.fix_points +from splib.core.enum_types import ConstraintType +from stlib.core.baseNodeModifierParameters import BaseNodeModifierParameters, SingleNodeModifierParameters, PairNodeModifierParameters +from splib.core.utils import DEFAULT_VALUE +from stlib.entities import Entity +import Sofa +from Sofa.Core import Node + +import dataclasses + +class NodeModifier(Sofa.Core.Controller): + + parameters : BaseNodeModifierParameters + + def __init__(self, parameters : BaseNodeModifierParameters): + Sofa.Core.Controller.__init__(self, **parameters.toDict()) + self.parameters = parameters + + +class SingleNodeModifier(NodeModifier): + + def __init__(self, parameters : BaseNodeModifierParameters): + super().__init__(parameters) + + def modify(self, node : Node): + self.parameters.modifier(node) + + +class PairNodeModifier(NodeModifier): + + def __init__(self, parameters : BaseNodeModifierParameters): + super().__init__(parameters) + + def modify(self, node1 : Node, node2 : Node): + self.parameters.modifier(node1,node2) + + + +class MultiNodeModifier(NodeModifier): + + def __init__(self, parameters : BaseNodeModifierParameters): + super().__init__(parameters) + + def modify(self, *nodes : Node ): + self.parameters.modifier(*nodes) + + + +@dataclasses.dataclass +class FixConstraintParameters(SingleNodeModifierParameters): + constraintType : ConstraintType = ConstraintType.PROJECTIVE + boxROI : list[ list[ float ] ] = dataclasses.field(default_factory= [[0,0,0], [1,1,1]]) + + def modifier(self, node : Node): + splib.mechanics.fix_points.addFixation(node.Material, type = self.constraintType, boxROIs = self.boxROI) + pass + + + +@dataclasses.dataclass +class AttachmentConstraintParameters(PairNodeModifierParameters): + constraintType : ConstraintType = ConstraintType.PROJECTIVE + + def modifier(self, node1 : Node, node2 : Node): + # splib.mechanics.fix_points.addFixation(node, type = self.constraintType, boxROI = self.boxROI) + pass + + + +@dataclasses.dataclass +class SimulationSolversParameters(SingleNodeModifierParameters): + constraintType : ConstraintType = ConstraintType.PROJECTIVE + boxROI : list[ list[ float ] ] = DEFAULT_VALUE + + def modifier(self, node1 : Node): + splib.simulation.ode_solvers.addImplicitODE(node1) + splib.simulation.linear_solvers.addLinearSolver(node1, constantSparsity=False, ) + pass + + + +@dataclasses.dataclass +class SimulationSettingsParameters(SingleNodeModifierParameters): + useLagrangian : bool = False + + def modifier(self, node1 : Node): + if(self.useLagrangian): + splib.simulation.headers.setupLagrangianCollision(root, displayFlags = "showVisualModels",backgroundColor=[0.8, 0.8, 0.8, 1], + parallelComputing = True,alarmDistance=0.3, contactDistance=0.02, + frictionCoef=0.5, tolerance=1.0e-4, maxIterations=20) + else: + splib.simulation.headers.setupDefaultHeader(root, displayFlags = "showVisualModels", + backgroundColor=[0.8, 0.8, 0.8, 1], + parallelComputing = True) + + + + + +if __name__ == "__main__": + + from stlib.geometries.plane import PlaneParameters + from stlib.geometries.file import FileParameters + from stlib.geometries.extract import ExtractParameters + from stlib.materials.deformable import DeformableBehaviorParameters + from stlib.collision import Collision, CollisionParameters + from stlib.entities import Entity, EntityParameters + from stlib.visual import Visual, VisualParameters + from splib.core.enum_types import CollisionPrimitive, ElementType, ConstitutiveLaw + from splib.simulation.headers import setupLagrangianCollision, setupDefaultHeader + from splib.simulation.ode_solvers import addImplicitODE + from splib.simulation.linear_solvers import addLinearSolver + from stlib.core.basePrefab import BasePrefab, BasePrefabParameters + + ### !!!!!! To be able to apply the modifier from the root, because I need to do this for the simulation settings + # for instance, I need it to be a prefab, and this ONLY to be able to add a secondary 'informative' object in it automatically + # ----> BEURK + root = BasePrefab(parameters=BasePrefabParameters(name="root")) + + + root.addObject("RequiredPlugin", pluginName=[ 'Sofa.Component', + 'Sofa.GL.Component.Rendering3D', + 'SofaImGui' + ]) + + + LogoParams = EntityParameters(name = "Logo", + geometry = FileParameters(filename="mesh/SofaScene/Logo.vtk"), + material = DeformableBehaviorParameters(), + collision = CollisionParameters(geometry = FileParameters(filename="mesh/SofaScene/LogoColli.sph")), + visual = VisualParameters(geometry = FileParameters(filename="mesh/SofaScene/LogoVisu.obj"))) + + LogoParams.geometry.elementType = ElementType.TETRAHEDRA + LogoParams.material.constitutiveLawType = ConstitutiveLaw.ELASTIC + LogoParams.material.parameters = [200, 0.4] + LogoParams.material.massDensity = 0.003261 + LogoParams.collision.primitives = [CollisionPrimitive.SPHERES] + #TODO make this flawless with spheres. Here collisions elements are not in the topology and a link is to be made between the loader and the collision object + LogoParams.collision.kwargs = {"SphereCollision" : {"radius" : "@Geometry/loader.listRadius"}} + LogoParams.visual.color = [0.7, .35, 0, 0.8] + + Logo = root.add(Entity, parameters = LogoParams) + + ### Who needs to apply it ? Logo or root ? + # Logo.applyModifier(SingleNodeModifier, SimulationSolversParameters(), Logo) + root.applyModifier(SingleNodeModifier, SimulationSolversParameters(name="solvers"), Logo) + root.applyModifier(SingleNodeModifier, FixConstraintParameters(name="fixedConstraints", boxROI=[[0, -20, -20], [1, 20, 20]]), Logo) + # + root.applyModifier(SingleNodeModifier, SimulationSettingsParameters(name="settings"), root) + + + root.gravity=[0,0,9.81] + Sofa.Simulation.initRoot(root) + + import Sofa.Gui + + # Find out the supported GUIs + print ("Supported GUIs are: " + Sofa.Gui.GUIManager.ListSupportedGUI(",")) + # Launch the GUI (imgui is now by default, to use Qt please refer to the example "basic-useQtGui.py") + Sofa.Gui.GUIManager.Init("myscene", "imgui") + Sofa.Gui.GUIManager.createGUI(root, __file__) + Sofa.Gui.GUIManager.SetDimension(1080, 1080) + # Initialization of the scene will be done here + Sofa.Gui.GUIManager.MainLoop(root) + Sofa.Gui.GUIManager.closeGUI() + print("GUI was closed") + + diff --git a/stlib/core/baseNodeModifier2.py b/stlib/core/baseNodeModifier2.py new file mode 100644 index 000000000..ed59cc537 --- /dev/null +++ b/stlib/core/baseNodeModifier2.py @@ -0,0 +1,253 @@ + +import splib.mechanics.fix_points +from splib.core.enum_types import ConstraintType +from stlib.core.baseNodeModifierParameters import BaseNodeModifierParameters, SingleNodeModifierParameters, PairNodeModifierParameters +from splib.core.utils import DEFAULT_VALUE +from stlib.entities import Entity +import Sofa +from Sofa.Core import Node + +import sys +import dataclasses + +class NodeModifier(Sofa.Core.Controller): + + parameters : BaseNodeModifierParameters + + def __init__(self, parameters : BaseNodeModifierParameters): + Sofa.Core.Controller.__init__(self, **(parameters.toDict())) + self.parameters = parameters + + @staticmethod + def findMostcommonPath( *nodes : Node) -> str : + if len(nodes) == 1 : + return str(nodes[0].linkpath) + + nodesParentsTree = [] + minSize = sys.maxsize + for node in nodes: + print(node.getLinkPath()) + nodesParentsTree.append(str(node.getLinkPath()).split('/')) + if len(nodesParentsTree[-1]) < minSize: + minSize = len(nodesParentsTree[-1]) + + for i in range(minSize): + isCommon = True + for j in range(1, len(nodesParentsTree)) : + isCommon = isCommon and nodesParentsTree[j][i] == nodesParentsTree[0][i] + if not isCommon: + return '/'.join(nodesParentsTree[0][:i]) + + return '/'.join(nodesParentsTree[0][:minSize]) + + + def register( self, holder, *nodes : Node) -> Node : + #Find holder path as the most common ancestor if None is given + if(holder is None): + path = NodeModifier.findMostcommonPath(*nodes) + + childrenToRoot = path.split('/') + if(childrenToRoot[0] == '@' ): + childrenToRoot = childrenToRoot[1:] + + holder = nodes[0].getRoot() + for child in childrenToRoot: + if(child == ''): + break + holder = holder.getChild(child) + + #Remove last / if exists (this is only the case for root + holderPath = str(holder.getLinkPath()) + if holderPath[-1] == "/": + holderPath = holderPath[:-1] + + #Find Relative paths to nodes on which it is applied + add myself to data + for node in nodes: + targetPath = str(node.getLinkPath()) + relPath = targetPath.removeprefix(holderPath) + relPath = "@" + relPath.count('/')*"../" + "Modifiers/" + self.name.value + + + if node.getData("modifiedBy") is None: + node.addData("modifiedBy", type="vector",value=[], default=[], help="Modifiers that modified this Node", group="STLIB") + + node.modifiedBy = node.modifiedBy.value + [relPath] + + #Add targets to self data + for node in nodes: + targetPath = str(node.getLinkPath()) + relPath = targetPath.removeprefix(holderPath) + relPath = "@.." + relPath + + if self.getData("modified") is None: + self.addData("modified", type="vector",value=[], default=[], help="Nodes modified by this modifier", group="STLIB") + + self.modified = self.modified.value + [relPath] + + #Now finally register + if "Modifiers" not in holder.children: + holder.addChild("Modifiers") + holder.Modifiers.addObject(self) + + + +class SingleNodeModifier(NodeModifier): + + def __init__(self, parameters : BaseNodeModifierParameters): + super().__init__(parameters) + + def modify(self, node : Node, holder : Node = None): + self.parameters.modifier(node) + self.register(holder, node) + + + +class PairNodeModifier(NodeModifier): + + def __init__(self, parameters : BaseNodeModifierParameters): + super().__init__(parameters) + + def modify(self, node1 : Node, node2 : Node, holder : Node = None): + self.parameters.modifier(node1,node2) + self.register(holder, node1, node2) + + + +class MultiNodeModifier(NodeModifier): + + def __init__(self, parameters : BaseNodeModifierParameters): + super().__init__(parameters) + + def modify(self, *nodes : Node , holder : Node = None): + self.parameters.modifier(*nodes) + self.register(holder, *nodes) + + + + +@dataclasses.dataclass +class FixConstraintParameters(SingleNodeModifierParameters): + constraintType : ConstraintType = ConstraintType.PROJECTIVE + boxROI : list[ list[ float ] ] = dataclasses.field(default_factory= [[0,0,0], [1,1,1]]) + + def modifier(self, node : Node): + splib.mechanics.fix_points.addFixation(node.Material, type = self.constraintType, boxROIs = self.boxROI) + pass + + + +@dataclasses.dataclass +class AttachmentConstraintParameters(PairNodeModifierParameters): + constraintType : ConstraintType = ConstraintType.PROJECTIVE + + def modifier(self, node1 : Node, node2 : Node): + # splib.mechanics.fix_points.addFixation(node, type = self.constraintType, boxROI = self.boxROI) + pass + + + +@dataclasses.dataclass +class SimulationSolversParameters(SingleNodeModifierParameters): + constraintType : ConstraintType = ConstraintType.PROJECTIVE + boxROI : list[ list[ float ] ] = DEFAULT_VALUE + + def modifier(self, node1 : Node): + splib.simulation.ode_solvers.addImplicitODE(node1) + splib.simulation.linear_solvers.addLinearSolver(node1, constantSparsity=False, ) + pass + + + +@dataclasses.dataclass +class SimulationSettingsParameters(SingleNodeModifierParameters): + useLagrangian : bool = False + + def modifier(self, node1 : Node): + if(self.useLagrangian): + splib.simulation.headers.setupLagrangianCollision(root, displayFlags = "showVisualModels",backgroundColor=[0.8, 0.8, 0.8, 1], + parallelComputing = True,alarmDistance=0.3, contactDistance=0.02, + frictionCoef=0.5, tolerance=1.0e-4, maxIterations=20) + else: + splib.simulation.headers.setupDefaultHeader(root, displayFlags = "showVisualModels", + backgroundColor=[0.8, 0.8, 0.8, 1], + parallelComputing = True) + + + + + +if __name__ == "__main__": + + from stlib.geometries.plane import PlaneParameters + from stlib.geometries.file import FileParameters + from stlib.geometries.extract import ExtractParameters + from stlib.materials.deformable import DeformableBehaviorParameters + from stlib.collision import Collision, CollisionParameters + from stlib.entities import Entity, EntityParameters + from stlib.visual import Visual, VisualParameters + from splib.core.enum_types import CollisionPrimitive, ElementType, ConstitutiveLaw + from splib.simulation.headers import setupLagrangianCollision, setupDefaultHeader + from splib.simulation.ode_solvers import addImplicitODE + from splib.simulation.linear_solvers import addLinearSolver + from stlib.core.basePrefab import BasePrefab, BasePrefabParameters + + #### The root is a vanilla Node + root = Sofa.Core.Node(name="root") + + + root.addObject("RequiredPlugin", pluginName=[ 'Sofa.Component', + 'Sofa.GL.Component.Rendering3D', + 'SofaImGui' + ]) + + + LogoParams = EntityParameters(name = "Logo", + geometry = FileParameters(filename="mesh/SofaScene/Logo.vtk"), + material = DeformableBehaviorParameters(), + collision = CollisionParameters(geometry = FileParameters(filename="mesh/SofaScene/LogoColli.sph")), + visual = VisualParameters(geometry = FileParameters(filename="mesh/SofaScene/LogoVisu.obj"))) + + LogoParams.geometry.elementType = ElementType.TETRAHEDRA + LogoParams.material.constitutiveLawType = ConstitutiveLaw.ELASTIC + LogoParams.material.parameters = [200, 0.4] + LogoParams.material.massDensity = 0.003261 + LogoParams.collision.primitives = [CollisionPrimitive.SPHERES] + #TODO make this flawless with spheres. Here collisions elements are not in the topology and a link is to be made between the loader and the collision object + LogoParams.collision.kwargs = {"SphereCollision" : {"radius" : "@Geometry/loader.listRadius"}} + LogoParams.visual.color = [0.7, .35, 0, 0.8] + + Logo = root.add(Entity, parameters = LogoParams) + + SomeChildren = root.addChild("SomeChildren") + LogoParams.name="Logo1" + Logo1 = SomeChildren.add(Entity, parameters = LogoParams) + LogoParams.name="Logo2" + Logo2 = SomeChildren.add(Entity, parameters = LogoParams) + + ### Now code is clearer + # Newbies don't need to care, if they don't give the holder then it is set by default to most common ancestor + # But us that are building the prefabs we will use it with caution and give the right holder + SingleNodeModifier(SimulationSolversParameters(name="solvers")).modify(Logo, holder=root) + SingleNodeModifier(FixConstraintParameters(name="fixedConstraints", boxROI=[[0, -20, -20], [1, 20, 20]])).modify(Logo) + SingleNodeModifier(SimulationSettingsParameters(name="settings")).modify(root) + PairNodeModifier(AttachmentConstraintParameters(name="FirstPair")).modify(Logo1, Logo2) + PairNodeModifier(AttachmentConstraintParameters(name="SecondPair")).modify(Logo1, Logo2, holder=root) + + + root.gravity=[0,0,9.81] + Sofa.Simulation.initRoot(root) + + import Sofa.Gui + + # Find out the supported GUIs + print ("Supported GUIs are: " + Sofa.Gui.GUIManager.ListSupportedGUI(",")) + # Launch the GUI (imgui is now by default, to use Qt please refer to the example "basic-useQtGui.py") + Sofa.Gui.GUIManager.Init("myscene", "imgui") + Sofa.Gui.GUIManager.createGUI(root, __file__) + Sofa.Gui.GUIManager.SetDimension(1080, 1080) + # Initialization of the scene will be done here + Sofa.Gui.GUIManager.MainLoop(root) + Sofa.Gui.GUIManager.closeGUI() + print("GUI was closed") + + diff --git a/stlib/core/baseNodeModifierParameters.py b/stlib/core/baseNodeModifierParameters.py new file mode 100644 index 000000000..44fda6004 --- /dev/null +++ b/stlib/core/baseNodeModifierParameters.py @@ -0,0 +1,33 @@ +import dataclasses +from splib.core.utils import DEFAULT_VALUE + +from Sofa.Core import Node +from typing import Callable, Optional, Any + +@dataclasses.dataclass +class BaseNodeModifierParameters(object): + name : str = "NodeModifier" + kwargs : dict = dataclasses.field(default_factory=dict) + + def modifier(self, *node : Node): + pass + def toDict(self): + return dataclasses.asdict(self) + + + + + +@dataclasses.dataclass +class SingleNodeModifierParameters(BaseNodeModifierParameters): + def modifier(self, node : Node): + pass + + + +@dataclasses.dataclass +class PairNodeModifierParameters(BaseNodeModifierParameters): + def modifier(self, node1 : Node, node2 : Node): + pass + + diff --git a/stlib/core/basePrefab.py b/stlib/core/basePrefab.py index 83d29f55d..2cf6362c0 100644 --- a/stlib/core/basePrefab.py +++ b/stlib/core/basePrefab.py @@ -15,7 +15,15 @@ def __init__(self, parameters: BasePrefabParameters): def init(self): raise NotImplemented("To be overridden by child class") - + def applyModifier(self, modifierType, parameters, *args ): + if "Modifiers" not in self.children: + self.addChild("Modifiers") + + modifier = self.Modifiers.addObject(modifierType(parameters=parameters)) + modifier.modify(*args) + + + def localToGlobalCoordinates(pointCloudInput, pointCloudOutput): raise NotImplemented("Send an email to Damien, he will help you. Guaranteed :)")