Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
#include <SofaPython3/Sofa/Core/Binding_Base.h>
#include <SofaPython3/Sofa/Core/Binding_BaseComponent.h>
#include <SofaPython3/Sofa/Core/Binding_BaseComponent_doc.h>
#include <SofaPython3/Sofa/Core/Binding_Controller.h>
#include <SofaPython3/PythonFactory.h>

#include <sofa/core/ObjectFactory.h>
Expand Down
96 changes: 96 additions & 0 deletions bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Component.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/******************************************************************************
* SofaPython3 plugin *
* (c) 2026 CNRS, University of Lille, INRIA *
* *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU Lesser General Public License as published by *
* the Free Software Foundation; either version 2.1 of the License, or (at *
* your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT *
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License *
* for more details. *
* *
* You should have received a copy of the GNU Lesser General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*******************************************************************************
* Contact information: contact@sofa-framework.org *
******************************************************************************/

#include <SofaPython3/Sofa/Core/Binding_Component.inl>

SOFAPYTHON3_BIND_ATTRIBUTE_ERROR()

/// Makes an alias for the pybind11 namespace to increase readability.
namespace py { using namespace pybind11; }

namespace sofapython3
{
using sofa::core::objectmodel::Event;
using sofa::core::objectmodel::BaseComponent;

// ---------------------------------------------------------------------------
// Component_Trampoline
// ---------------------------------------------------------------------------

Component_Trampoline::Component_Trampoline()
: TrampolineBase(this) // pass this as BaseComponent* — no CRTP needed
{
}

void Component_Trampoline::draw(const sofa::core::visual::VisualParams* params)
{
PythonEnvironment::executePython(this, [this, params](){
PYBIND11_OVERLOAD(void, Component, draw, params);
});
}

void Component_Trampoline::init()
{
PythonEnvironment::executePython(this, [this](){
initializePythonCache();
PYBIND11_OVERLOAD(void, Component, init, );
});
}

void Component_Trampoline::reinit()
{
PythonEnvironment::executePython(this, [this](){
PYBIND11_OVERLOAD(void, Component, reinit, );
});
}

void Component_Trampoline::handleEvent(sofa::core::objectmodel::Event* event)
{
trampoline_handleEvent(event);
}

std::string Component_Trampoline::getClassName() const
{
return trampoline_getClassName();
}

// ---------------------------------------------------------------------------
// Module registration
// ---------------------------------------------------------------------------

void moduleAddComponent(py::module &m) {
py::class_<Component,
Component_Trampoline,
BaseComponent,
py_shared_ptr<Component>> f(m, "Component",
py::dynamic_attr(),
sofapython3::doc::component::componentClass);

f.def(py::init(&trampoline_init<Component_Trampoline>));
f.def("__setattr__", &trampoline_setattr<Component_Trampoline>);

f.def("init", &Component::init);
f.def("reinit", &Component::reinit);
f.def("draw", [](Component& self, sofa::core::visual::VisualParams* params){
self.draw(params);
}, pybind11::return_value_policy::reference);
}

} // namespace sofapython3
89 changes: 89 additions & 0 deletions bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Component.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/******************************************************************************
* SofaPython3 plugin *
* (c) 2026 CNRS, University of Lille, INRIA *
* *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU Lesser General Public License as published by *
* the Free Software Foundation; either version 2.1 of the License, or (at *
* your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT *
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License *
* for more details. *
* *
* You should have received a copy of the GNU Lesser General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*******************************************************************************
* Contact information: contact@sofa-framework.org *
******************************************************************************/

#pragma once

#include <pybind11/pybind11.h>
#include <sofa/core/objectmodel/BaseComponent.h>
#include <string>
#include <unordered_map>

namespace sofapython3 {


class TrampolineBase
{
public:
explicit TrampolineBase(sofa::core::objectmodel::BaseComponent* self);
~TrampolineBase();

void trampoline_handleEvent(sofa::core::objectmodel::Event* event);
std::string trampoline_getClassName() const;
void invalidateMethodCache(const std::string& methodName);

protected:
void initializePythonCache();
pybind11::object getCachedMethod(const std::string& methodName);
bool callCachedMethod(const pybind11::object& method, sofa::core::objectmodel::Event* event);
bool callScriptMethod(const pybind11::object& self, sofa::core::objectmodel::Event* event,
const std::string& methodName);

/// Raw non-owning pointer to the concrete trampoline as BaseComponent.
/// Safe because TrampolineBase is always embedded in the same object.
sofa::core::objectmodel::BaseComponent* m_componentSelf { nullptr };

pybind11::object m_pySelf;
std::unordered_map<std::string, pybind11::object> m_methodCache;
bool m_cacheInitialized = false;
};


template<class T>
sofa::core::sptr<T> trampoline_init(pybind11::args& /*args*/, pybind11::kwargs& kwargs);

template<class T>
void trampoline_setattr(pybind11::object self, const std::string& s, pybind11::object value);



class Component : public sofa::core::objectmodel::BaseComponent {
public:
SOFA_CLASS(Component, sofa::core::objectmodel::BaseComponent);
void init() override {}
void reinit() override {}
};

class Component_Trampoline : public Component, public TrampolineBase
{
public:
SOFA_CLASS(Component_Trampoline, Component);

Component_Trampoline();

void init() override;
void reinit() override;
void draw(const sofa::core::visual::VisualParams* params) override;
void handleEvent(sofa::core::objectmodel::Event* event) override;
std::string getClassName() const override;
};

void moduleAddComponent(pybind11::module &m);

} /// namespace sofapython3
197 changes: 197 additions & 0 deletions bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Component.inl
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/******************************************************************************
* SofaPython3 plugin *
* (c) 2026 CNRS, University of Lille, INRIA *
* *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU Lesser General Public License as published by *
* the Free Software Foundation; either version 2.1 of the License, or (at *
* your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT *
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License *
* for more details. *
* *
* You should have received a copy of the GNU Lesser General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*******************************************************************************
* Contact information: contact@sofa-framework.org *
******************************************************************************/
#pragma once

#include <pybind11/pybind11.h>
#include <pybind11/cast.h>
#include <sofa/core/visual/VisualParams.h>
#include <SofaPython3/Sofa/Core/Binding_Base.h>
#include <SofaPython3/Sofa/Core/Binding_Component.h>
#include <SofaPython3/Sofa/Core/Binding_Component_doc.h>
#include <SofaPython3/PythonFactory.h>
#include <SofaPython3/PythonEnvironment.h>

/// Makes an alias for the pybind11 namespace to increase readability.
namespace py { using namespace pybind11; }

namespace sofapython3
{
using sofa::core::objectmodel::Event;
using sofa::core::objectmodel::BaseComponent;


inline TrampolineBase::TrampolineBase(BaseComponent* self)
: m_componentSelf(self)
{
}

inline TrampolineBase::~TrampolineBase()
{
if (m_cacheInitialized)
{
PythonEnvironment::gil acquire {"~TrampolineBase"};
m_methodCache.clear();
m_pySelf = py::object();
}
}

inline void TrampolineBase::initializePythonCache()
{
if (m_cacheInitialized)
return;

// py::cast on the BaseComponent* will find the most-derived registered
// pybind11 type (e.g. the user's Python subclass), which is exactly what
// we want — no need for static_cast<T*>(this) anymore.
m_pySelf = py::cast(m_componentSelf);

getCachedMethod("onEvent");
m_cacheInitialized = true;
}

inline py::object TrampolineBase::getCachedMethod(const std::string& methodName)
{
auto it = m_methodCache.find(methodName);
if (it != m_methodCache.end())
return it->second;

py::object method;
if (py::hasattr(m_pySelf, methodName.c_str()))
{
py::object fct = m_pySelf.attr(methodName.c_str());
if (PyCallable_Check(fct.ptr()))
method = fct;
}

m_methodCache[methodName] = method;
return method;
}

inline bool TrampolineBase::callCachedMethod(const py::object& method, Event* event)
{
if (m_componentSelf->f_printLog.getValue())
{
std::string eventStr = py::str(PythonFactory::toPython(event));
msg_info(m_componentSelf) << "on" << event->getClassName() << " " << eventStr;
}

py::object result = method(PythonFactory::toPython(event));
if (result.is_none())
return false;

return py::cast<bool>(result);
}

inline void TrampolineBase::invalidateMethodCache(const std::string& methodName)
{
if (!m_cacheInitialized)
return;
m_methodCache.erase(methodName);
}

inline std::string TrampolineBase::trampoline_getClassName() const
{
PythonEnvironment::gil acquire {"getClassName"};

if (m_pySelf)
return py::str(py::type::of(m_pySelf).attr("__name__"));

// Fallback before cache is initialized: cast via BaseComponent*
return py::str(py::type::of(py::cast(m_componentSelf)).attr("__name__"));
}

inline bool TrampolineBase::callScriptMethod(
const py::object& self, Event* event, const std::string& methodName)
{
if (m_componentSelf->f_printLog.getValue())
{
std::string name = std::string("on") + event->getClassName();
std::string eventStr = py::str(PythonFactory::toPython(event));
msg_info(m_componentSelf) << name << " " << eventStr;
}

if (py::hasattr(self, methodName.c_str()))
{
py::object fct = self.attr(methodName.c_str());
py::object result = fct(PythonFactory::toPython(event));
if (result.is_none())
return false;
return py::cast<bool>(result);
}
return false;
}

inline void TrampolineBase::trampoline_handleEvent(Event* event)
{
PythonEnvironment::executePython(m_componentSelf, [this, event](){
if (!m_cacheInitialized)
initializePythonCache();

std::string methodName = std::string("on") + event->getClassName();

py::object method = getCachedMethod(methodName);
if (!method)
method = getCachedMethod("onEvent");

if (method)
{
bool isHandled = callCachedMethod(method, event);
if (isHandled)
event->setHandled();
}
});
}


template<class T>
sofa::core::sptr<T> trampoline_init(py::args& /*args*/, py::kwargs& kwargs)
{
auto c = sofa::core::sptr<T>(new T());
c->f_listening.setValue(true);

for (auto kv : kwargs)
{
std::string key = py::cast<std::string>(kv.first);
py::object value = py::reinterpret_borrow<py::object>(kv.second);

if (key == "name")
c->setName(py::cast<std::string>(kv.second));
try {
BindingBase::SetAttr(*c, key, value);
} catch (py::attribute_error& /*e*/) {
// kwargs may be plain Python attributes unrelated to SOFA — ignore
}
}
return c;
}

template<class T>
void trampoline_setattr(py::object self, const std::string& s, py::object value)
{
if (s.rfind("on", 0) == 0 && PyCallable_Check(value.ptr()))
{
auto* trampoline = dynamic_cast<T*>(py::cast<BaseComponent*>(self));
if (trampoline)
trampoline->invalidateMethodCache(s);
}
BindingBase::__setattr__(self, s, value);
}

} // namespace sofapython3
Loading
Loading