From bff3402159b915a45d507a668c5bc68be4582ebc Mon Sep 17 00:00:00 2001 From: David Thompson Date: Tue, 25 Nov 2025 12:38:56 -0500 Subject: [PATCH 1/8] Add methods to set per-component opacity on representations. --- .../pqSMTKResourceRepresentation.cxx | 24 +++++++++++++++++ .../pqSMTKResourceRepresentation.h | 4 +++ .../server/vtkSMTKResourceRepresentation.cxx | 26 +++++++++++++++++++ .../server/vtkSMTKResourceRepresentation.h | 1 + 4 files changed, 55 insertions(+) diff --git a/smtk/extension/paraview/appcomponents/pqSMTKResourceRepresentation.cxx b/smtk/extension/paraview/appcomponents/pqSMTKResourceRepresentation.cxx index b84a05179c..0e27394a67 100644 --- a/smtk/extension/paraview/appcomponents/pqSMTKResourceRepresentation.cxx +++ b/smtk/extension/paraview/appcomponents/pqSMTKResourceRepresentation.cxx @@ -114,6 +114,30 @@ bool pqSMTKResourceRepresentation::setVisibility(smtk::resource::ComponentPtr co return false; } +bool pqSMTKResourceRepresentation::setOpacity(smtk::resource::ComponentPtr comp, double opacity) +{ + return this->setOpacity(comp.get(), opacity); +} + +bool pqSMTKResourceRepresentation::setOpacity(const smtk::resource::PersistentObject* obj, double opacity) +{ + auto* pxy = this->getProxy(); + auto* mpr = pxy->GetClientSideObject(); // TODO: Remove the need for me. + auto* cmp = vtkCompositeRepresentation::SafeDownCast(mpr); + auto* spx = + cmp ? vtkSMTKResourceRepresentation::SafeDownCast(cmp->GetActiveRepresentation()) : nullptr; + if (spx) + { + if (spx->SetEntityOpacity(obj, opacity)) + { + // TODO: Emit a signal when opacity changes? + // Q_EMIT componentVisibilityChanged(obj, visible); + return true; + } + } + return false; +} + void pqSMTKResourceRepresentation::allVisibilities( std::map& visibilities) const { diff --git a/smtk/extension/paraview/appcomponents/pqSMTKResourceRepresentation.h b/smtk/extension/paraview/appcomponents/pqSMTKResourceRepresentation.h index cac904bb31..7318d0705f 100644 --- a/smtk/extension/paraview/appcomponents/pqSMTKResourceRepresentation.h +++ b/smtk/extension/paraview/appcomponents/pqSMTKResourceRepresentation.h @@ -42,6 +42,10 @@ public: /// Populate the \a visibilities map with per-component visibility information. void allVisibilities(std::map& visibilities) const; + /// Change the opacity of the specified component. Returns true if changed, false otherwise. + bool setOpacity(smtk::resource::ComponentPtr comp, double opacity); + bool setOpacity(const smtk::resource::PersistentObject* comp, double opacity); + Q_SIGNALS: /// Emitted from within setVisibility(). void componentVisibilityChanged(smtk::resource::ComponentPtr comp, bool visible); diff --git a/smtk/extension/paraview/server/vtkSMTKResourceRepresentation.cxx b/smtk/extension/paraview/server/vtkSMTKResourceRepresentation.cxx index 1a03a75f71..e3e50140be 100644 --- a/smtk/extension/paraview/server/vtkSMTKResourceRepresentation.cxx +++ b/smtk/extension/paraview/server/vtkSMTKResourceRepresentation.cxx @@ -624,6 +624,32 @@ bool vtkSMTKResourceRepresentation::SetEntityVisibility( return didChange; } +bool vtkSMTKResourceRepresentation::SetEntityOpacity( + const smtk::resource::PersistentObject* ent, + double opacity) +{ + if (!ent || !ent->id()) + { + return false; + } + + auto rdit = this->RenderableData.find(ent->id()); + if (rdit == this->RenderableData.end()) + { // No change because the object is not renderable. + return false; + } + auto currentOpacity = this->EntityMapper->GetCompositeDataDisplayAttributes()->GetBlockOpacity(rdit->second); + if (currentOpacity == opacity) + { + return false; + } + this->EntityMapper->GetCompositeDataDisplayAttributes()->SetBlockOpacity(rdit->second, opacity); + this->GlyphMapper->GetBlockAttributes()->SetBlockOpacity(rdit->second, opacity); + this->EntityMapper->Modified(); + this->GlyphMapper->Modified(); + return true; +} + bool vtkSMTKResourceRepresentation::ApplyStyle( smtk::view::SelectionPtr seln, RenderableDataMap& renderables, diff --git a/smtk/extension/paraview/server/vtkSMTKResourceRepresentation.h b/smtk/extension/paraview/server/vtkSMTKResourceRepresentation.h index 3b3229df2c..12b57f230a 100644 --- a/smtk/extension/paraview/server/vtkSMTKResourceRepresentation.h +++ b/smtk/extension/paraview/server/vtkSMTKResourceRepresentation.h @@ -290,6 +290,7 @@ public: void GetEntityVisibilities(std::map& visdata); bool SetEntityVisibility(smtk::resource::PersistentObjectPtr ent, bool visible); + bool SetEntityOpacity(const smtk::resource::PersistentObject* ent, double opacity); /**\brief Look for generator functions that alters a representation's appearance based on a selection. * -- GitLab From 3c477271e861f6ceb50d4d3f36244f559ec4cb11 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Tue, 25 Nov 2025 12:41:01 -0500 Subject: [PATCH 2/8] Fix task controls for opacity. Use the new methods on representations to update opacities of workflow objects on a port or in a task's resource. --- doc/release/notes/component-opacity.rst | 12 ++++++++++++ .../paraview/project/pqTaskControlView.cxx | 17 +++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 doc/release/notes/component-opacity.rst diff --git a/doc/release/notes/component-opacity.rst b/doc/release/notes/component-opacity.rst new file mode 100644 index 0000000000..39ab3a6d72 --- /dev/null +++ b/doc/release/notes/component-opacity.rst @@ -0,0 +1,12 @@ +ParaView Extensions +=================== + +Per-component opacity +--------------------- + +Previously – while it was possible to change the opacity of a component by running +an operation that changed the color property associated with the component – it was +not possible for user-interface elements to modify the opacity of individual +components. Methods for this have now been added to SMTK's ParaView representation +and the task-control view now properly handles user adjustments to component opacity +through these methods for quick but impermanent changes to component visualizations. diff --git a/smtk/extension/paraview/project/pqTaskControlView.cxx b/smtk/extension/paraview/project/pqTaskControlView.cxx index bc04bc467b..01a772a88e 100644 --- a/smtk/extension/paraview/project/pqTaskControlView.cxx +++ b/smtk/extension/paraview/project/pqTaskControlView.cxx @@ -78,6 +78,22 @@ bool representationObjectMapContainsResource(const RepresentationObjectMap::valu return (it != entry.second.end() || entry.second.find(nullptr) != entry.second.end()); } +bool representationUpdateBlockOpacities( + const RepresentationObjectMap::value_type& entry, double opacity) +{ + bool didChange = false; + auto pvDRep = dynamic_cast(entry.first); + if (!pvDRep) + { + return didChange; + } + for (const auto& ptr : entry.second) + { + didChange |= pvDRep->setOpacity(ptr, opacity); + } + return didChange; +} + } // anonymous namespace /// Private storage for a task-control views. @@ -600,6 +616,7 @@ void pqTaskControlView::updateOpacity(const std::string& name, double opacity) entry.first->getProxy()->UpdateVTKObjects(); needRender = true; } + needRender |= representationUpdateBlockOpacities(entry, opacity); // TODO: Handle per-block opacity settings for components. // Iterate over entry.second, find component inside the // pqSMTKRepresentation and set per-block opacity. -- GitLab From 4b465d4869528cd4fd9330fc64f156de2b49ecc5 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Tue, 25 Nov 2025 12:42:29 -0500 Subject: [PATCH 3/8] Hide empty group-view labels in tiled layouts. --- smtk/extension/qt/qtGroupView.cxx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/smtk/extension/qt/qtGroupView.cxx b/smtk/extension/qt/qtGroupView.cxx index abb80133ca..5713770ca2 100644 --- a/smtk/extension/qt/qtGroupView.cxx +++ b/smtk/extension/qt/qtGroupView.cxx @@ -66,7 +66,7 @@ public: void qtGroupViewInternals::updateChildren(qtGroupView* gview, qtBaseViewMemFn mfunc) { // In the case of tiling we don't want to show the - // label for empty views + // label for empty views, nor empty labels. if (m_style == qtGroupViewInternals::TILED) { int i, size = m_ChildViews.size(); @@ -82,7 +82,14 @@ void qtGroupViewInternals::updateChildren(qtGroupView* gview, qtBaseViewMemFn mf else { child->widget()->show(); - m_Labels.at(i)->show(); + if (m_Labels.at(i)->text().isEmpty()) + { + m_Labels.at(i)->hide(); + } + else + { + m_Labels.at(i)->show(); + } } } } @@ -626,6 +633,10 @@ void qtGroupView::addTileEntry(qtBaseView* child) QObject::connect(child, &qtBaseView::modified, this, &qtGroupView::childModified); QLabel* label = new QLabel(child->configuration()->label().c_str(), this->Widget); m_internals->m_Labels.append(label); + if (label->text().isEmpty()) + { + label->hide(); + } QFont titleFont; titleFont.setBold(true); titleFont.setItalic(true); -- GitLab From 3aed0389f2892b2841160fc71a5f4425fdaa5484 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Tue, 25 Nov 2025 12:43:36 -0500 Subject: [PATCH 4/8] Allow direct equality testing of tokens and strings in python. --- smtk/string/pybind11/PybindToken.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/smtk/string/pybind11/PybindToken.h b/smtk/string/pybind11/PybindToken.h index c973046491..4d055da014 100644 --- a/smtk/string/pybind11/PybindToken.h +++ b/smtk/string/pybind11/PybindToken.h @@ -50,6 +50,11 @@ inline py::class_ pybind11_init_smtk_string_Token(py::modul { return self == other; }) + .def("__eq__", [](const smtk::string::Token& self, const std::string& other) + { + auto otherToken = smtk::string::Token(other); + return self == otherToken; + }) .def("__lt__", [](const smtk::string::Token& self, const smtk::string::Token& other) { return self < other; -- GitLab From e04c39291f22c0ce4d42c4e9ac3d8ec17dc52867 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Tue, 25 Nov 2025 12:46:02 -0500 Subject: [PATCH 5/8] Add python bindings for port-forwarding agents. --- .../task/pybind11/PybindPortForwardingAgent.h | 47 +++++++++++++++++++ smtk/task/pybind11/PybindTask.cxx | 3 ++ 2 files changed, 50 insertions(+) create mode 100644 smtk/task/pybind11/PybindPortForwardingAgent.h diff --git a/smtk/task/pybind11/PybindPortForwardingAgent.h b/smtk/task/pybind11/PybindPortForwardingAgent.h new file mode 100644 index 0000000000..30fc08d064 --- /dev/null +++ b/smtk/task/pybind11/PybindPortForwardingAgent.h @@ -0,0 +1,47 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= + +#ifndef pybind_smtk_task_PortForwardingAgent_h +#define pybind_smtk_task_PortForwardingAgent_h + +#include + +#include "smtk/task/PortForwardingAgent.h" + +#include "smtk/task/Port.h" + +namespace py = pybind11; + +inline py::class_< smtk::task::PortForwardingAgent > pybind11_init_smtk_task_PortForwardingAgent(py::module &m) +{ + py::class_< smtk::task::PortForwardingAgent, smtk::task::Agent > instance(m, "PortForwardingAgent"); + instance + .def("typeToken", &smtk::task::PortForwardingAgent::typeToken) + .def("classHierarchy", &smtk::task::PortForwardingAgent::classHierarchy) + .def("matchesType", &smtk::task::PortForwardingAgent::matchesType, py::arg("candidate")) + .def("generationsFromBase", &smtk::task::PortForwardingAgent::generationsFromBase, py::arg("base")) + .def("state", &smtk::task::PortForwardingAgent::state) + .def("configure", [](smtk::task::PortForwardingAgent& self, const std::string& jsonConfig) + { + auto config = nlohmann::json::parse(jsonConfig); + self.configure(config); + }) + .def("configuration", &smtk::task::PortForwardingAgent::configuration) + .def("name", &smtk::task::PortForwardingAgent::name) + .def("portData", [](smtk::task::PortForwardingAgent& self, smtk::task::Port::Ptr port) + { + return self.portData(port.get()); + }, py::arg("port")) + .def("parent", &smtk::task::PortForwardingAgent::parent, py::return_value_policy::reference_internal) + ; + return instance; +} + +#endif diff --git a/smtk/task/pybind11/PybindTask.cxx b/smtk/task/pybind11/PybindTask.cxx index 3e0944bf75..5e6a6de20f 100644 --- a/smtk/task/pybind11/PybindTask.cxx +++ b/smtk/task/pybind11/PybindTask.cxx @@ -9,6 +9,7 @@ //========================================================================= #include +#include #include #include "nlohmann/json.hpp" @@ -36,6 +37,7 @@ using namespace nlohmann; #include "PybindSubmitOperationAgent.h" #include "PybindTask.h" #include "PybindTrivialProducerAgent.h" +#include "PybindPortForwardingAgent.h" #include "PybindWorklet.h" #include "PybindInstances.h" @@ -60,6 +62,7 @@ PYBIND11_MODULE(_smtkPybindTask, m) auto smtk_task_SubmitOperationAgent = pybind11_init_smtk_task_SubmitOperationAgent(m); auto smtk_task_GatherObjectsAgent = pybind11_init_smtk_task_GatherObjectsAgent(m); auto smtk_task_TrivialProducerAgent = pybind11_init_smtk_task_TrivialProducerAgent(m); + auto smtk_task_PortForwardingAgent = pybind11_init_smtk_task_PortForwardingAgent(m); auto smtk_task_Task = pybind11_init_smtk_task_Task(m); pybind11_init_smtk_task_State(m); pybind11_init_smtk_task_stateEnum(m); -- GitLab From 6db9c2eb14c5f43f3fc1517243aeb667615a088b Mon Sep 17 00:00:00 2001 From: David Thompson Date: Tue, 25 Nov 2025 12:48:08 -0500 Subject: [PATCH 6/8] Allow python code to set shape data on markup components. --- smtk/markup/pybind11/PybindDiscreteGeometry.h | 46 +++++++++++++++++++ smtk/markup/pybind11/PybindMarkup.cxx | 2 + smtk/markup/pybind11/PybindUnstructuredData.h | 20 +++++++- 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 smtk/markup/pybind11/PybindDiscreteGeometry.h diff --git a/smtk/markup/pybind11/PybindDiscreteGeometry.h b/smtk/markup/pybind11/PybindDiscreteGeometry.h new file mode 100644 index 0000000000..10b5b30a52 --- /dev/null +++ b/smtk/markup/pybind11/PybindDiscreteGeometry.h @@ -0,0 +1,46 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= + +#ifndef pybind_smtk_markup_DiscreteGeometry_h +#define pybind_smtk_markup_DiscreteGeometry_h + +#include + +#include "smtk/markup/DiscreteGeometry.h" + +#include "smtk/extension/vtk/pybind11/PybindVTKTypeCaster.h" + +#include "smtk/common/UUID.h" +#include "smtk/common/pybind11/PybindUUIDTypeCaster.h" + +namespace py = pybind11; + +inline PySharedPtrClass< smtk::markup::DiscreteGeometry> pybind11_init_smtk_markup_DiscreteGeometry(py::module &m) +{ + PySharedPtrClass< smtk::markup::DiscreteGeometry, smtk::markup::SpatialData> instance(m, "DiscreteGeometry"); + instance + .def_static("CastTo", [](const std::shared_ptr& obj) + { return std::dynamic_pointer_cast(obj); }) + ; + py::class_ shapeOpts(instance, "ShapeOptions"); + shapeOpts + .def(py::init<>()) + .def(py::init<::smtk::markup::DiscreteGeometry::ShapeOptions const &>()) + .def("trackedChanges", + [](smtk::markup::DiscreteGeometry::ShapeOptions& opts) + { return opts.trackedChanges; }) + .def("setTrackedChanges", + [](smtk::markup::DiscreteGeometry::ShapeOptions& opts, smtk::operation::Operation::Result res) + { opts.trackedChanges = res; }, py::arg("tracked_changes")) + ; + return instance; +} + +#endif diff --git a/smtk/markup/pybind11/PybindMarkup.cxx b/smtk/markup/pybind11/PybindMarkup.cxx index b64b91618c..2bdad79a27 100644 --- a/smtk/markup/pybind11/PybindMarkup.cxx +++ b/smtk/markup/pybind11/PybindMarkup.cxx @@ -25,6 +25,7 @@ using PySharedPtrClass = py::class_, Args...>; #include "PybindResource.h" #include "PybindComponent.h" #include "PybindSpatialData.h" +#include "PybindDiscreteGeometry.h" #include "PybindDomain.h" #include "PybindDomainMap.h" #include "PybindUnstructuredData.h" @@ -51,5 +52,6 @@ PYBIND11_MODULE(_smtkPybindMarkup, markup) auto smtk_markup_Domain = pybind11_init_smtk_markup_Domain(markup); auto smtk_markup_DomainMap = pybind11_init_smtk_markup_DomainMap(markup); auto smtk_markup_SpatialData = pybind11_init_smtk_markup_SpatialData(markup); + auto smtk_markup_DiscreteGeometry = pybind11_init_smtk_markup_DiscreteGeometry(markup); auto smtk_markup_UnstructuredData = pybind11_init_smtk_markup_UnstructuredData(markup); } diff --git a/smtk/markup/pybind11/PybindUnstructuredData.h b/smtk/markup/pybind11/PybindUnstructuredData.h index 0541d8de08..f48ae33558 100644 --- a/smtk/markup/pybind11/PybindUnstructuredData.h +++ b/smtk/markup/pybind11/PybindUnstructuredData.h @@ -13,6 +13,7 @@ #include +#include "smtk/markup/AssignedIds.h" #include "smtk/markup/UnstructuredData.h" #include "smtk/extension/vtk/pybind11/PybindVTKTypeCaster.h" @@ -24,12 +25,29 @@ namespace py = pybind11; inline PySharedPtrClass< smtk::markup::UnstructuredData> pybind11_init_smtk_markup_UnstructuredData(py::module &m) { - PySharedPtrClass< smtk::markup::UnstructuredData, smtk::markup::SpatialData> instance(m, "UnstructuredData"); + PySharedPtrClass< smtk::markup::UnstructuredData, smtk::markup::DiscreteGeometry, smtk::markup::SpatialData> instance(m, "UnstructuredData"); instance .def("shape", &smtk::markup::UnstructuredData::shape) + .def("setShapeData", &smtk::markup::UnstructuredData::setShapeData) .def_static("CastTo", [](const std::shared_ptr& obj) { return std::dynamic_pointer_cast(obj); }) ; + py::class_ + shapeOpts(instance, "ShapeOptions"); + shapeOpts + .def(py::init<>()) + .def(py::init<::smtk::markup::UnstructuredData::ShapeOptions const &>()) + .def("sharedCellIds", [](const smtk::markup::UnstructuredData::ShapeOptions& opts) + { return opts.sharedCellIds; }) + .def("setSharedCellIds", []( + smtk::markup::UnstructuredData::ShapeOptions& opts, const std::shared_ptr& ids) + { opts.sharedCellIds = ids; }, py::arg("sharedCellIds")) + .def("sharedPointIds", [](const smtk::markup::UnstructuredData::ShapeOptions& opts) + { return opts.sharedPointIds; }) + .def("setSharedPointIds", []( + smtk::markup::UnstructuredData::ShapeOptions& opts, const std::shared_ptr& ids) + { opts.sharedPointIds = ids; }, py::arg("sharedPointIds")) + ; return instance; } -- GitLab From 1897a8dbebd5d24b4953848f07e610b5e8dccb44 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Wed, 26 Nov 2025 11:17:14 -0500 Subject: [PATCH 7/8] Add `smtk::common::StringUtil::uniqify()`. --- smtk/common/StringUtil.cxx | 28 +++++++++ smtk/common/StringUtil.h | 47 +++++++++++++++ smtk/common/testing/cxx/CMakeLists.txt | 1 + smtk/common/testing/cxx/UnitTestUniqify.cxx | 67 +++++++++++++++++++++ 4 files changed, 143 insertions(+) create mode 100644 smtk/common/testing/cxx/UnitTestUniqify.cxx diff --git a/smtk/common/StringUtil.cxx b/smtk/common/StringUtil.cxx index 995ffa9dd7..abd6b88526 100644 --- a/smtk/common/StringUtil.cxx +++ b/smtk/common/StringUtil.cxx @@ -225,5 +225,33 @@ bool StringUtil::mixedAlphanumericComparator(const std::string& aa, const std::s double nb = atof(bb.substr(i).c_str()); return na < nb; } + +bool StringUtil::endsWithUnderscoreNumber(std::string& key, int& ii) +{ + std::size_t pos = key.find_last_of('_'); + if (pos == std::string::npos) + { + return false; + } + std::string num = key.substr(pos + 1); + std::size_t ep; + try + { + ii = std::stoi(num, &ep, 10); + if (ep != num.size()) + { + ii = 0; + return false; + } + key = key.substr(0, pos + 1); + } + catch (std::exception& ee) + { + ii = 0; + return false; + } + return true; +} + } // namespace common } // namespace smtk diff --git a/smtk/common/StringUtil.h b/smtk/common/StringUtil.h index 3ac0c2e261..9ddd5709c1 100644 --- a/smtk/common/StringUtil.h +++ b/smtk/common/StringUtil.h @@ -12,6 +12,7 @@ #include "smtk/CoreExports.h" +#include #include #include @@ -81,6 +82,52 @@ public: * \sa DescriptivePhrase::compareByTitle */ static bool mixedAlphanumericComparator(const std::string& aa, const std::string& bb); + + /**\brief Detect whether the \a key ends with an underscore followed by an integer number. + * + * If so, this returns true with \a key reduced to the prefix and \a ii set to the + * numeric value following the underscore. If not, this returns false and \a ii will + * be set to 0 while \a key will be unchanged. + */ + static bool endsWithUnderscoreNumber(std::string& key, int& ii); + + /**\brief Return a value of \a src that is unique within the context of \a preExisting. + * + * The returned value will not be any key of the \a preExisting dictionary (whether + * \a Dict is a set or map). No value is inserted into \a preExisting; if you choose + * to use the returned value, you are responsible for updating the dictionary. + */ + template + static std::string uniqify(const std::string& src, Dict& preExisting) + { + std::string key = src; + { + int ii; + key = src.data(); + std::string prefix; + if (StringUtil::endsWithUnderscoreNumber(key, ii)) + { + prefix = key; + } + else + { + prefix = key + "_"; + ii = 0; + } + for (++ii; true; ++ii) + { + std::ostringstream ks; + ks << prefix << ii; + key = ks.str(); + if (preExisting.find(key) == preExisting.end()) + { + break; + } + } + } + return key; + } + }; } // namespace common diff --git a/smtk/common/testing/cxx/CMakeLists.txt b/smtk/common/testing/cxx/CMakeLists.txt index 98a5659788..ab2b03aac7 100644 --- a/smtk/common/testing/cxx/CMakeLists.txt +++ b/smtk/common/testing/cxx/CMakeLists.txt @@ -48,6 +48,7 @@ set(unit_tests UnitTestTypeHierarchy.cxx UnitTestTypeMap.cxx UnitTestTypeName.cxx + UnitTestUniqify.cxx UnitTestUpdateFactory.cxx UnitTestVersionNumber.cxx UnitTestVisit.cxx diff --git a/smtk/common/testing/cxx/UnitTestUniqify.cxx b/smtk/common/testing/cxx/UnitTestUniqify.cxx new file mode 100644 index 0000000000..1ec6ee78e2 --- /dev/null +++ b/smtk/common/testing/cxx/UnitTestUniqify.cxx @@ -0,0 +1,67 @@ +#include "smtk/common/StringUtil.h" + +#include +#include +#include +#include +#include +#include + +int UnitTestUniqify(int, char**) +{ + bool ok = true; + std::vector fails{ "foo", "foo_a", "foo_0a", "foo_lksfj_a0", "_", "_25699456738475773459358234577", "foo_" }; + std::vector passes{ "foo_0", "foo_1", "foo_42", "bar_86", "_37" }; + + int ii; + for (const auto& fail : fails) + { + ii = 0; + std::string key = fail; + if (smtk::common::StringUtil::endsWithUnderscoreNumber(key, ii)) + { + std::cerr << "ERROR: \"" << fail << "\" should not have passed. (" << key << ", " << ii << ")\n"; + ok = false; + } + else + { + if (key != fail || ii != 0) + { + std::cerr << "ERROR: \"" << fail << "\" modified inputs (" << key << ", " << ii << ")\n"; + } + else + { + std::cout << "Expected failure \"" << fail << "\" (" << key << ", " << ii << ")\n"; + } + } + } + + for (const auto& pass : passes) + { + ii = 0; + std::string key = pass; + if (!smtk::common::StringUtil::endsWithUnderscoreNumber(key, ii)) + { + std::cerr << "ERROR: \"" << pass << "\" should have passed. (" << key << ", " << ii << ")\n"; + ok = false; + } + else + { + std::cout << "\"" << key << "\" with " << ii << "\n"; + } + } + + std::map dict{ {"foo_0", "foo_2"}, {"foo_1", "foo_2"}, {"foo_37", "foo_38"}, {"bar_86", "bar_88"}, {"bar_87", "bar_87"}, {"_0", "_1"} }; + for (const auto& pass : passes) + { + std::string uu = smtk::common::StringUtil::uniqify(pass, dict); + std::cout << "\"" << pass << "\" → \"" << uu << "\"\n"; + if (dict.find(pass) != dict.end() && dict[pass] != uu) + { + std::cerr << "ERROR: \"" << pass << "\" was \"" << uu << "\", expected \"" << dict[pass] << "\".\n"; + ok = false; + } + } + + return ok ? 0 : 1; +} -- GitLab From c90c863a057157ad67f94889d0f47a57fa71267e Mon Sep 17 00:00:00 2001 From: David Thompson Date: Thu, 4 Dec 2025 17:51:42 -0500 Subject: [PATCH 8/8] Deal with style-name collisions. --- doc/release/notes/worklet-style-collision.rst | 13 ++++++++++ smtk/task/Manager.cxx | 24 +++++++++++++++++++ smtk/task/Manager.h | 11 +++++++++ smtk/task/json/jsonManager.cxx | 22 ++++++++++++++++- 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 doc/release/notes/worklet-style-collision.rst diff --git a/doc/release/notes/worklet-style-collision.rst b/doc/release/notes/worklet-style-collision.rst new file mode 100644 index 0000000000..0b90511c08 --- /dev/null +++ b/doc/release/notes/worklet-style-collision.rst @@ -0,0 +1,13 @@ +Task Subsystem +============== + +Worklet style collisions +------------------------ + +When a worklet is emplaced, it is deserialized as if it were a task manager unto itself. +However, this can cause an issue when the project's task manager already contains +definitions for style tags with the same name (for example, if two worklets in the gallery +define a "show-geometry" style with different values). Previously, each worklet emplaced +would overwrite the old style definition with its own (breaking already-emplaced tasks). +Now, if name collisions between style tags already present in the task manager are detected +and the style definitions are not identical, a new style tag is created and used. diff --git a/smtk/task/Manager.cxx b/smtk/task/Manager.cxx index 8e68989f47..c5927c0c60 100644 --- a/smtk/task/Manager.cxx +++ b/smtk/task/Manager.cxx @@ -21,6 +21,8 @@ #include "smtk/resource/Resource.h" +#include "smtk/common/StringUtil.h" + #include "smtk/io/Logger.h" using namespace smtk::string::literals; @@ -269,6 +271,28 @@ nlohmann::json Manager::getStyle(const smtk::string::Token& styleClass) const return nlohmann::json(); } +void Manager::addStyles( + const nlohmann::json& styles, + std::unordered_map& styleMap) +{ + for (const auto& entry : styles.items()) + { + std::string key = entry.key(); + auto it = m_styles.find(key); + if (it != m_styles.end()) + { + if (*it != entry.value()) + { + key = smtk::common::StringUtil::uniqify(key, m_styles); + styleMap[entry.key()] = key; + smtkWarningMacro(smtk::io::Logger::instance(), + " Style \"" << entry.key() << "\" mapped to \"" << key << "\"."); + } + } + m_styles[key] = entry.value(); + } +} + smtk::resource::Resource* Manager::resource() const { return m_parent; diff --git a/smtk/task/Manager.h b/smtk/task/Manager.h index bff8796971..93ad564dfe 100644 --- a/smtk/task/Manager.h +++ b/smtk/task/Manager.h @@ -136,8 +136,19 @@ public: /// Given a style key, return a style config. nlohmann::json getStyle(const smtk::string::Token& styleClass) const; nlohmann::json getStyles() const { return m_styles; }; + + /// Overwrite this task-manager's styles with the provided \a styles. void setStyles(const nlohmann::json& styles) { m_styles = styles; } + /// Append \a styles into this task manager. + /// + /// In the event of a name collision (where the content for styles of the same + /// name is different), the name is made unique and an entry is added to the + /// \a styleMap from the original name to the uniquified name. + void addStyles( + const nlohmann::json& styles, + std::unordered_map& styleMap); + /// If this manager is owned by a resource (typically a project), return it. smtk::resource::Resource* resource() const; diff --git a/smtk/task/json/jsonManager.cxx b/smtk/task/json/jsonManager.cxx index 84261abcc9..703d2eadd3 100644 --- a/smtk/task/json/jsonManager.cxx +++ b/smtk/task/json/jsonManager.cxx @@ -174,7 +174,27 @@ void from_json(const nlohmann::json& jj, Manager& taskManager) // Copy style specifications into the task manager. if (jj.contains("styles")) { - taskManager.setStyles(jj.at("styles")); + std::unordered_map styleMap; + taskManager.addStyles(jj.at("styles"), styleMap); + if (!styleMap.empty()) + { + // Update all the tasks we are deserializing with new style tags as needed. + if (jj.contains("tasks")) + { + for (const auto& jsonTask : jj.at("tasks")) + { + auto* task = helper.tasks().get(jsonTask); + if (!task) { continue; } + for (const auto& mapEntry : styleMap) + { + if (task->removeStyle(mapEntry.first)) + { + task->addStyle(mapEntry.second); + } + } + } + } + } } // Read in the worklet gallery if one exists. -- GitLab