From 7a088542f0483f41d753db0b72fdc42860c5d241 Mon Sep 17 00:00:00 2001 From: Adam Kewley Date: Mon, 18 Nov 2024 18:25:17 +0100 Subject: [PATCH] Implement prototypical ModelWarperV3 (step-by-step design) tab --- src/OpenSimCreator/CMakeLists.txt | 5 + .../Documents/Model/IComponentAccessor.h | 49 + .../Documents/Model/IModelStatePair.cpp | 7 + .../Documents/Model/IModelStatePair.h | 11 +- .../Model/IVersionedComponentAccessor.h | 24 + .../UI/ModelEditor/AddComponentPopup.cpp | 2 +- .../UI/ModelWarperV3/ModelWarperV3Tab.cpp | 1041 +++++++++++++++++ .../UI/ModelWarperV3/ModelWarperV3Tab.h | 25 + src/OpenSimCreator/UI/OpenSimCreatorTabs.h | 2 + .../UI/Shared/FunctionCurveViewerPopup.cpp | 31 +- .../UI/Shared/FunctionCurveViewerPopup.h | 4 +- .../UI/Shared/GeometryPathEditorPopup.cpp | 50 +- .../UI/Shared/GeometryPathEditorPopup.h | 4 +- .../UI/Shared/ObjectPropertiesEditor.cpp | 307 +++-- .../UI/Shared/ObjectPropertiesEditor.h | 10 +- .../UI/Shared/PropertiesPanel.cpp | 2 +- .../UI/Simulation/SimulationOutputPlot.cpp | 2 +- src/OpenSimCreator/UI/SplashTab.cpp | 2 +- src/oscar/UI/oscimgui.cpp | 7 +- src/oscar/UI/oscimgui.h | 8 + src/oscar/Utils/StringHelpers.cpp | 15 + src/oscar/Utils/StringHelpers.h | 6 + tests/testoscar/Utils/TestStringHelpers.cpp | 26 +- third_party/osim | 2 +- 24 files changed, 1458 insertions(+), 184 deletions(-) create mode 100644 src/OpenSimCreator/Documents/Model/IComponentAccessor.h create mode 100644 src/OpenSimCreator/Documents/Model/IModelStatePair.cpp create mode 100644 src/OpenSimCreator/Documents/Model/IVersionedComponentAccessor.h create mode 100644 src/OpenSimCreator/UI/ModelWarperV3/ModelWarperV3Tab.cpp create mode 100644 src/OpenSimCreator/UI/ModelWarperV3/ModelWarperV3Tab.h diff --git a/src/OpenSimCreator/CMakeLists.txt b/src/OpenSimCreator/CMakeLists.txt index 33b17c0b6..cc9771261 100644 --- a/src/OpenSimCreator/CMakeLists.txt +++ b/src/OpenSimCreator/CMakeLists.txt @@ -120,7 +120,10 @@ add_library(OpenSimCreator STATIC Documents/Model/BasicModelStatePair.h Documents/Model/Environment.cpp Documents/Model/Environment.h + Documents/Model/IComponentAccessor.h + Documents/Model/IModelStatePair.cpp Documents/Model/IModelStatePair.h + Documents/Model/IVersionedComponentAccessor.h Documents/Model/ModelStateCommit.cpp Documents/Model/ModelStateCommit.h Documents/Model/ModelStatePairInfo.cpp @@ -300,6 +303,8 @@ add_library(OpenSimCreator STATIC UI/MeshWarper/MeshWarpingTabToolbar.cpp UI/MeshWarper/MeshWarpingTabToolbar.h UI/MeshWarper/MeshWarpingTabUserSelection.h + UI/ModelWarperV3/ModelWarperV3Tab.cpp + UI/ModelWarperV3/ModelWarperV3Tab.h UI/ModelEditor/AddBodyPopup.cpp UI/ModelEditor/AddBodyPopup.h diff --git a/src/OpenSimCreator/Documents/Model/IComponentAccessor.h b/src/OpenSimCreator/Documents/Model/IComponentAccessor.h new file mode 100644 index 000000000..cb1c6fe44 --- /dev/null +++ b/src/OpenSimCreator/Documents/Model/IComponentAccessor.h @@ -0,0 +1,49 @@ +#pragma once + +#include + +#include + +namespace OpenSim { class Component; } + +namespace osc +{ + class IComponentAccessor { + protected: + IComponentAccessor() = default; + IComponentAccessor(const IComponentAccessor&) = default; + IComponentAccessor(IComponentAccessor&&) noexcept = default; + IComponentAccessor& operator=(const IComponentAccessor&) = default; + IComponentAccessor& operator=(IComponentAccessor&&) noexcept = default; + + friend bool operator==(const IComponentAccessor&, const IComponentAccessor&) = default; + public: + virtual ~IComponentAccessor() noexcept = default; + + const OpenSim::Component& getComponent() const { return implGetComponent(); } + operator const OpenSim::Component& () const { return getComponent(); } + + bool isReadonly() const { return not implCanUpdComponent(); } + bool canUpdComponent() const { return implCanUpdComponent(); } + OpenSim::Component& updComponent() { return implUpdComponent(); } + + private: + // Implementors should return a const reference to an initialized (finalized properties, etc.) component + virtual const OpenSim::Component& implGetComponent() const = 0; + + // Implementors may return whether the component contained by the concrete `IComponentAccessor` implementation + // can be modified in-place. + // + // If the response can be `true`, implementors must also override `implUpdComponent` accordingly. + virtual bool implCanUpdComponent() const { return false; } + + // Implementors may return a mutable reference to the contained component. It is up to the caller + // of `updComponent` to ensure that the component is still valid + initialized after modification. + // + // If this is implemented, implementors should override `implCanUpdComponent` accordingly. + virtual OpenSim::Component& implUpdComponent() + { + throw std::runtime_error{"component updating not implemented for this IComponentAccessor"}; + } + }; +} diff --git a/src/OpenSimCreator/Documents/Model/IModelStatePair.cpp b/src/OpenSimCreator/Documents/Model/IModelStatePair.cpp new file mode 100644 index 000000000..7c2e7edbb --- /dev/null +++ b/src/OpenSimCreator/Documents/Model/IModelStatePair.cpp @@ -0,0 +1,7 @@ +#include "IModelStatePair.h" + +#include +#include + +const OpenSim::Component& osc::IModelStatePair::implGetComponent() const { return implGetModel(); } +OpenSim::Component& osc::IModelStatePair::implUpdComponent() { return implUpdModel(); } diff --git a/src/OpenSimCreator/Documents/Model/IModelStatePair.h b/src/OpenSimCreator/Documents/Model/IModelStatePair.h index a031888f2..4d4fd188c 100644 --- a/src/OpenSimCreator/Documents/Model/IModelStatePair.h +++ b/src/OpenSimCreator/Documents/Model/IModelStatePair.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -16,7 +18,7 @@ namespace osc { // virtual accessor to an `OpenSim::Model` + `SimTK::State` pair, with // additional opt-in overrides to aid rendering/UX etc. - class IModelStatePair { + class IModelStatePair : public IVersionedComponentAccessor { protected: IModelStatePair() = default; IModelStatePair(const IModelStatePair&) = default; @@ -93,6 +95,13 @@ namespace osc void setUpToDateWithFilesystem(std::filesystem::file_time_type t) { implSetUpToDateWithFilesystem(t); } private: + // overrides + specializes `IComponentAccessor` API + const OpenSim::Component& implGetComponent() const final; + bool implCanUpdComponent() const { return implCanUpdModel(); } + OpenSim::Component& implUpdComponent() final; + UID implGetComponentVersion() const final { return implGetModelVersion(); } + void implSetComponentVersion(UID newVersion) final { implSetModelVersion(newVersion); } + // Implementors should return a const reference to an initialized (finalized properties, etc.) model. virtual const OpenSim::Model& implGetModel() const = 0; diff --git a/src/OpenSimCreator/Documents/Model/IVersionedComponentAccessor.h b/src/OpenSimCreator/Documents/Model/IVersionedComponentAccessor.h new file mode 100644 index 000000000..d5cb27dd2 --- /dev/null +++ b/src/OpenSimCreator/Documents/Model/IVersionedComponentAccessor.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +namespace osc +{ + class IVersionedComponentAccessor : public IComponentAccessor { + public: + UID getComponentVersion() const { return implGetComponentVersion(); } + void setComponentVersion(UID id) { implSetComponentVersion(id); } + + private: + // Implementors may return a `UID` that uniquely identifies the current version of the component. + virtual UID implGetComponentVersion() const + { + // assume the version always changes, unless the concrete implementation + // provides a way of knowing when it doesn't + return UID{}; + } + + // Implementors may use this to manually set the version of the component (sometimes useful for caching) + virtual void implSetComponentVersion(UID) {} + }; +} diff --git a/src/OpenSimCreator/UI/ModelEditor/AddComponentPopup.cpp b/src/OpenSimCreator/UI/ModelEditor/AddComponentPopup.cpp index 83936bf29..c55986ab3 100644 --- a/src/OpenSimCreator/UI/ModelEditor/AddComponentPopup.cpp +++ b/src/OpenSimCreator/UI/ModelEditor/AddComponentPopup.cpp @@ -76,7 +76,7 @@ class osc::AddComponentPopup::Impl final : public StandardPopup { StandardPopup{popupName}, m_Model{std::move(model)}, m_Proto{std::move(prototype)}, - m_PrototypePropertiesEditor{parent, m_Model, [proto = m_Proto]() { return proto.get(); }} + m_PrototypePropertiesEditor{&parent, m_Model, [proto = m_Proto]() { return proto.get(); }} {} private: diff --git a/src/OpenSimCreator/UI/ModelWarperV3/ModelWarperV3Tab.cpp b/src/OpenSimCreator/UI/ModelWarperV3/ModelWarperV3Tab.cpp new file mode 100644 index 000000000..2075086e4 --- /dev/null +++ b/src/OpenSimCreator/UI/ModelWarperV3/ModelWarperV3Tab.cpp @@ -0,0 +1,1041 @@ +#include "ModelWarperV3Tab.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace osc; + +namespace +{ + // Tries to delete an item from an `OpenSim::Set`. + // + // Returns `true` if the item was found and deleted; otherwise, returns `false`. + template + bool TryDeleteItemFromSet(OpenSim::Set& set, const T* item) + { + for (size_t i = 0; i < size(set); ++i) { + if (&At(set, i) == item) { + return EraseAt(set, i); + } + } + return false; + } + + // A single, potentially user-provided, scaling parameter. + // + // It is the responsibility of the engine/UI to gather/provide this to the + // scaling engine at scale-time. + using ScalingParameterValue = std::variant; + + // Returns a string representation of a `ScalingParameterValue`. + std::string to_string(const ScalingParameterValue& v) + { + return std::visit(Overload{ + [](const auto& inner) { return std::to_string(inner); }, + }, v); + } + + // A declaration of a scaling parameter. + // + // `ScalingStep`s can declare that they may/must use a named `ScalingParameterValue`s + // at runtime. This class is how they express that requirement. It's the + // scaling engine/UI's responsibility to handle this declaration. + class ScalingParameterDeclaration final { + public: + explicit ScalingParameterDeclaration(std::string name, ScalingParameterValue defaultValue) : + m_Name{std::move(name)}, + m_DefaultValue{std::move(defaultValue)} + {} + + const std::string& name() const { return m_Name; } + const ScalingParameterValue& default_value() const { return m_DefaultValue; } + private: + std::string m_Name; + ScalingParameterValue m_DefaultValue; + }; + + // A chosen scaling parameter default, which is usually provided by the top-level document + // to override the default provided via the `ScalingParameterDeclaration`. + class ScalingParameterDefault final : public OpenSim::Object { + OpenSim_DECLARE_CONCRETE_OBJECT(ScalingParameterDefault, OpenSim::Object); + public: + OpenSim_DECLARE_PROPERTY(parameter_name, std::string, "The name of the parameter that should be defaulted"); + OpenSim_DECLARE_PROPERTY(default_value, std::string, "The default value of the parameter (a string that requires parsing, based on the declarations)"); + + ScalingParameterDefault() + { + constructProperty_parameter_name("unknown"); + constructProperty_default_value("unknown_value"); + } + + explicit ScalingParameterDefault(std::string_view name, std::string_view value) + { + constructProperty_parameter_name(std::string{name}); + constructProperty_default_value(std::string{value}); + } + }; + + // Runtime scaling parameters, as collected by the runtime. + class ScalingParameters final { + }; + + // Persisted state between separate scaling executions, to improve the performance + // (esp. when scaling via UI edits). + class ScalingCache final { + }; + + // The state of a validation check performed by a `ScalingStep`. + enum class ScalingStepValidationState { + Warning, + Error, + }; + + // A message produced by a `ScalingStep`'s validation check. + class ScalingStepValidationMessage final { + public: + + // Constructs a validation message that's related to the value(s) held in + // a property with name `propertyName` on the `ScalingStep`. + explicit ScalingStepValidationMessage( + std::string propertyName, + ScalingStepValidationState state, + std::string message) : + + m_MaybePropertyName{std::move(propertyName)}, + m_State{state}, + m_Message{message} + {} + + // Constructs a validation message that's in some (general) way related to + // the `ScalingStep` that produced it. + explicit ScalingStepValidationMessage( + ScalingStepValidationState state, + std::string message) : + + m_State{state}, + m_Message{std::move(message)} + {} + + std::optional tryGetPropertyName() const + { + return not m_MaybePropertyName.empty() ? std::optional{m_MaybePropertyName} : std::nullopt; + } + ScalingStepValidationState getState() const { return m_State; } + CStringView getMessage() const { return m_Message; } + private: + std::string m_MaybePropertyName; + ScalingStepValidationState m_State; + std::string m_Message; + }; + + // An abstract base class for a single model-scaling step. + // + // Scaling steps are applied one-by-one to a copy of the source model in + // order to yield the "result" or "scaled" model. Each scaling step can + // request external data (`ScalingParameterDeclaration`). + class ScalingStep : public OpenSim::Component { + OpenSim_DECLARE_ABSTRACT_OBJECT(ScalingStep, Component); + + OpenSim_DECLARE_PROPERTY(label, std::string, "a user-facing label for the scaling step"); + protected: + explicit ScalingStep(std::string_view label) + { + constructProperty_label(std::string{label}); + } + public: + // Returns a user-facing label that describes this `ScalingStep`. + CStringView label() const { return get_label(); } + + // Sets this `ScalingStep`'s user-facing label. + void setLabel(CStringView newLabel) { set_label(std::string{newLabel}); } + + // Calls `callback` with each parameter declaration that this `ScalingStep` accepts + // at scaling-time. + // + // It is expected that higher-level engines provide values that satisfy these + // declarations to `applyScalingStep`. + void forEachScalingParameterDeclaration( + const std::function& callback) const + { + implForEachScalingParameterDeclaration(callback); + } + + // Applies this `ScalingStep`'s scaling function in-place to the `resultModel`. The + // original `sourceModel` is also provided, if relevant. + // + // It is expected that `scalingParameters` contains at least the scaling parameter + // values that match the declarations emitted by `forEachScalingParameterDeclaration`. + void applyScalingStep( + ScalingCache& scalingCache, + const ScalingParameters& scalingParameters, + const OpenSim::Model& sourceModel, + OpenSim::Model& resultModel) const + { + implApplyScalingStep(scalingCache, scalingParameters, sourceModel, resultModel); + } + + // Returns a sequence of `ScalingStepValidationMessage`, which should be empty, + // or non-errors, before higher-level engines call `applyScalingStep` (otherwise, + // an exception may be thrown by `applyScalingStep`). + std::vector validate( + ScalingCache& scalingCache, + const ScalingParameters& scalingParameters, + const OpenSim::Model& sourceModel) const + { + return implValidate(scalingCache, scalingParameters, sourceModel); + } + private: + // Implementors should provide the callback with any `ScalingParameterDeclaration`s in order + // to ensure that the runtime can later provide the `ScalingParameterValue` during model + // scaling. + virtual void implForEachScalingParameterDeclaration(const std::function&) const + {} + + // Implementors should apply their scaling to the result model (the source model is also + // available). Any computationally expensive scaling steps should be performed via + // the `ScalingCache`. + virtual void implApplyScalingStep( + ScalingCache&, + const ScalingParameters&, + const OpenSim::Model&, + OpenSim::Model&) const + {} + + // Implementors should return any validation warnings/errors related to this scaling step + // (e.g. incorrect property value, missing external data, etc.). + virtual std::vector implValidate( + ScalingCache&, + const ScalingParameters&, + const OpenSim::Model&) const + { + return {}; // i.e. by default, return no validation errors. + } + }; + + // A `ScalingStep` that scales the masses of bodies in the model. + class BodyMassesScalingStep final : public ScalingStep { + OpenSim_DECLARE_CONCRETE_OBJECT(BodyMassesScalingStep, ScalingStep); + public: + BodyMassesScalingStep() : + ScalingStep{"Scale Body Masses to Subject Mass"} + { + setDescription("Scales the masses of bodies in the model to match the subject's mass"); + } + + private: + void implForEachScalingParameterDeclaration(const std::function& callback) const final + { + callback(ScalingParameterDeclaration{"blending_factor", 1.0}); + callback(ScalingParameterDeclaration{"subject_mass", 75.0}); + } + }; + + // A `ScalingStep` that scales an `OpenSim::Mesh` in the source model by + // using the Thin-Plate Spline (TPS) warping algorithm on landmark pairs + // loaded from associated files. + class ThinPlateSplineMeshScalingStep final : public ScalingStep { + OpenSim_DECLARE_CONCRETE_OBJECT(ThinPlateSplineMeshScalingStep, ScalingStep); + + OpenSim_DECLARE_PROPERTY(mesh, std::string, "Component path, relative to the model, that locates the mesh that should be scaled by this scaling step (e.g. /bodyset/torso/torso_geom_4)"); + OpenSim_DECLARE_PROPERTY(source_landmarks_file, std::string, "Filesystem path, relative to the mesh's `mesh_file` path, where a CSV containing the source landmarks can be loaded from (e.g. torso.landmarks.csv). The variable `mesh_filename` is exposed to this property, and can be used as a stand-in for the mesh's `mesh_file` path, without file extensions (e.g. ${mesh_filename}.landmarks.csv)"); + OpenSim_DECLARE_PROPERTY(destination_landmarks_file, std::string, "Filesystem path, relative to the mesh's `mesh_file` path, where a CSV containing the destination landmarks can be loaded from (e.g. ../DestinationGeometry/torso.landmarks.csv). The variable 'mesh_filename' is exposed to this property and can be used as a stand-in for the mesh's `mesh_file` path, without file extensions (e.g. ../DestinationGeometry/${mesh_filename}.landmarks.csv)"); + + public: + explicit ThinPlateSplineMeshScalingStep() : + ScalingStep{"Apply Thin-Plate Spline Warp to Mesh"} + { + setDescription("Warps a mesh in the source model in a non-uniform way by applying a Thin-Plate Spline (TPS) warp to each vertex in the souce mesh."); + constructProperty_mesh(""); + constructProperty_source_landmarks_file("${mesh_filename}.landmarks.csv"); + constructProperty_destination_landmarks_file("../DestinationGeometry/${mesh_filename}.landmarks.csv"); + } + + private: + void implForEachScalingParameterDeclaration(const std::function& callback) const final + { + callback(ScalingParameterDeclaration{ "blending_factor", 1.0 }); + } + + std::vector implValidate( + ScalingCache&, + const ScalingParameters&, + const OpenSim::Model& sourceModel) const final + { + std::vector messages; + + if (not FindComponent(sourceModel, get_mesh())) { + std::stringstream msg; + msg << get_mesh() << ": cannot find this mesh in the source model"; + messages.emplace_back(ScalingStepValidationState::Error, std::move(msg).str()); + } + + // check that the source landmarks file exists + if (const auto resolvedFilepath = calcResolvedFilepath(get_source_landmarks_file()); + not std::filesystem::exists(resolvedFilepath)) { + + std::stringstream msg; + msg << resolvedFilepath << ": cannot find source landmarks file"; + messages.emplace_back(ScalingStepValidationState::Error, std::move(msg).str()); + } + + // check that the destination landmarks file exists + if (const auto resolvedFilepath = calcResolvedFilepath(get_destination_landmarks_file()); + not std::filesystem::exists(resolvedFilepath)) { + + std::stringstream msg; + msg << resolvedFilepath << ": cannot find destination landmarks file"; + messages.emplace_back(ScalingStepValidationState::Error, std::move(msg).str()); + } + + return messages; + } + + std::string calcResolvedFilepath(const std::string& unresolvedProperty) const + { + const std::string filename = std::filesystem::path{get_mesh()}.replace_extension().filename().string(); + return replace(unresolvedProperty, "${mesh_filename}", filename); + } + }; + + // A `ScalingStep` that applies the Thin-Plate Spline (TPS) warp to any `OpenSim::Station`s it + // can find via the `stations` search string. Note: muscle points in the model are usually + // `OpenSim::Station`s, so this can also be used to warp muscle points. + class ThinPlateSplineStationScalingStep final : public ScalingStep { + OpenSim_DECLARE_CONCRETE_OBJECT(ThinPlateSplineStationScalingStep, ScalingStep); + + OpenSim_DECLARE_LIST_PROPERTY(stations, std::string, "Query paths (e.g. `/forceset/*`) that the engine should use to find meshes in the source model that should be warped by this scaling step."); + public: + ThinPlateSplineStationScalingStep() : + ScalingStep{"Apply Thin-Plate Spline to Stations"} + { + setDescription("Scales the masses of bodies in the model to match the subject's mass"); + constructProperty_stations(); + } + private: + void implForEachScalingParameterDeclaration(const std::function& callback) const final + { + callback(ScalingParameterDeclaration{"blending_factor", 1.0}); + } + }; + + // Returns a list of `ScalingStep` prototypes, so that downstream code is able to present + // them as available options etc. + const auto& getScalingStepPrototypes() + { + static const auto s_Prototypes = std::to_array>({ + std::make_unique(), + std::make_unique(), + std::make_unique(), + }); + return s_Prototypes; + } + + // Top-level document that describes a sequence of `ScalingStep`s that can be applied to + // the source model in order to yield a scaled model. + class ModelWarperV3Document final : + public OpenSim::Component, + public IVersionedComponentAccessor { + + OpenSim_DECLARE_CONCRETE_OBJECT(ModelWarperV3Document, OpenSim::Component); + + OpenSim_DECLARE_LIST_PROPERTY(parameter_defaults, ScalingParameterDefault, "A list of scaling parameter defaults that should be shown to the user. These override the defaults produced by each `ScalingStep`'s implementation."); + public: + ModelWarperV3Document() + { + constructProperty_parameter_defaults(); + } + + bool hasScalingSteps() const + { + if (getNumImmediateSubcomponents() == 0) { + return 0; + } + const auto lst = getComponentList(); + return lst.begin() != lst.end(); + } + + auto iterateScalingSteps() const + { + return getComponentList(); + } + + void addScalingStep(std::unique_ptr step) + { + addComponent(step.release()); + } + + void removeScalingStep(ScalingStep& step) + { + if (not step.hasOwner()) { + return; + } + if (&step.getOwner() != this) { + return; + } + + auto& componentsProp = updProperty_components(); + if (int idx = componentsProp.findIndex(step); idx != -1) { + componentsProp.removeValueAtIndex(idx); + } + + finalizeConnections(*this); + } + + bool hasScalingParameters() const + { + if (not hasScalingSteps()) { + return false; + } + for (const ScalingStep& step : iterateScalingSteps()) { + bool called = false; + step.forEachScalingParameterDeclaration([&called](const ScalingParameterDeclaration&) { called = true; }); + if (called) { + return true; + } + } + return false; + } + + void forEachScalingParameterDefault(const std::function callback) const + { + if (not hasScalingSteps()) { + return; + } + + // Merge scaling parameter declarations accross steps. + std::map mergedDefaults; + for (const ScalingStep& step : iterateScalingSteps()) { + step.forEachScalingParameterDeclaration([&step, &mergedDefaults](const ScalingParameterDeclaration& decl) + { + const auto [it, inserted] = mergedDefaults.try_emplace(decl.name(), decl.default_value()); + if (not inserted and it->second.index() != decl.default_value().index()) { + std::stringstream msg; + msg << step.getAbsolutePath() << ": declares a scaling parameter (" << decl.name() << ") that has the same name as another scaling parameter, but different type: the engine cannot figure out how to rectify this difference. The parameter should have a different name, or a disambiguating prefix added to it"; + throw std::runtime_error{std::move(msg).str()}; + } + }); + } + for (const auto& [name, value] : mergedDefaults) { + callback(ScalingParameterDefault{name, to_string(value)}); + } + } + + private: + const OpenSim::Component& implGetComponent() const final + { + return *this; + } + + bool implCanUpdComponent() const + { + return true; + } + + OpenSim::Component& implUpdComponent() + { + throw std::runtime_error{ "component updating not implemented for this IComponentAccessor" }; + } + }; + + // Top-level shared UI state that the tab is manipulating. + class ModelWarperV3UIState final { + public: + ModelWarperV3UIState() + { + m_ScalingDocument->finalizeConnections(*m_ScalingDocument); + } + + // lifecycle stuff + void on_tick() + { + for (auto& deferredAction : m_DeferredActions) { + deferredAction(*this); + } + m_DeferredActions.clear(); + } + + std::shared_ptr getDocumentPtr() { return m_ScalingDocument; } + + // scaling step stuff + bool hasScalingSteps() const { return m_ScalingDocument->hasScalingSteps(); } + auto iterateScalingSteps() const { return m_ScalingDocument->iterateScalingSteps(); } + void addScalingStepDeferred(std::unique_ptr step) + { + m_DeferredActions.push_back([s = std::shared_ptr{std::move(step)}](ModelWarperV3UIState& state) mutable + { + state.m_ScalingDocument->addScalingStep(std::unique_ptr{s->clone()}); + }); + } + void eraseScalingStepDeferred(const ScalingStep& step) + { + m_DeferredActions.push_back([path = step.getAbsolutePath()](ModelWarperV3UIState& state) + { + if (auto* step = FindComponentMut(*state.m_ScalingDocument, path)) { + state.m_ScalingDocument->removeScalingStep(*step); + } + }); + } + std::vector validateStep(const ScalingStep& step) + { + return step.validate( + m_ScalingCache, + m_ScalingParameters, + *m_SourceModel + ); + } + + // scaling parameter stuff + bool hasScalingParameters() const { return m_ScalingDocument->hasScalingParameters(); } + void forEachScalingParameterDefault(const std::function callback) const { m_ScalingDocument->forEachScalingParameterDefault(callback); } + + // model stuff + std::shared_ptr sourceModel() { return m_SourceModel; } + std::shared_ptr scaledModel() { return m_ScaledModel; } + + // camera stuff + bool isCameraLinked() const { return m_LinkCameras; } + void setCameraLinked(bool v) { m_LinkCameras = v; } + bool isOnlyCameraRotationLinked() const { return m_OnlyLinkRotation; } + void setOnlyCameraRotationLinked(bool v) { m_OnlyLinkRotation = v; } + const PolarPerspectiveCamera& getLinkedCamera() const { return m_LinkedCamera; } + void setLinkedCamera(const PolarPerspectiveCamera& camera) { m_LinkedCamera = camera; } + + // actions + void actionOpenOsimOrPromptUser(std::optional path) + { + if (not path) { + path = prompt_user_to_select_file({"osim"}); + } + + if (path) { + App::singleton()->push_back(*path); + m_SourceModel = std::make_shared(std::move(path).value()); + } + } + void actionAppendEntryToScalingStepStringListProperty(const ScalingStep& step, const OpenSim::Property& prop) + { + auto* mutableStep = FindComponentMut(*m_ScalingDocument, GetAbsolutePath(step)); + if (not mutableStep) { + return; + } + + auto* mutableProperty = FindSimplePropertyMut(*mutableStep, prop.getName()); + if (not mutableProperty) { + return; + } + + mutableProperty->appendValue(""); + + m_ScalingDocument->finalizeConnections(*m_ScalingDocument); + } + + void actionSetStringListPropertyValueButDontCommit(const ScalingStep& step, const OpenSim::Property& prop, int i, const std::string& value) + { + auto* mutableStep = FindComponentMut(*m_ScalingDocument, GetAbsolutePath(step)); + if (not mutableStep) { + return; + } + + auto* mutableProperty = FindSimplePropertyMut(*mutableStep, prop.getName()); + if (not mutableProperty) { + return; + } + + mutableProperty->setValue(i, value); + } + + void actionCommitCurrentPropertyValues() + { + m_ScalingDocument->finalizeConnections(*m_ScalingDocument); + } + + void actionApplyObjectEditToScalingDocument(ObjectPropertyEdit edit) + { + OpenSim::Component* component = FindComponentMut(*m_ScalingDocument, edit.getComponentAbsPath()); + if (not component) { + return; + } + OpenSim::AbstractProperty* property = FindPropertyMut(*component, edit.getPropertyName()); + if (not property) { + return; + } + edit.apply(*property); + } + private: + std::shared_ptr m_SourceModel = std::make_shared(); + std::shared_ptr m_ScaledModel = m_SourceModel; + std::shared_ptr m_ScalingDocument = std::make_shared(); + ScalingCache m_ScalingCache; + ScalingParameters m_ScalingParameters; + std::vector> m_DeferredActions; + + bool m_LinkCameras = true; + bool m_OnlyLinkRotation = false; + PolarPerspectiveCamera m_LinkedCamera; + }; + + Color ui_color(const ScalingStepValidationMessage& message) + { + switch (message.getState()) { + case ScalingStepValidationState::Warning: return Color::orange(); + case ScalingStepValidationState::Error: return Color::muted_red(); + default: return Color::muted_red(); + } + } + + // source model 3D viewer + class ModelWarperV3SourceModelViewerPanel final : public ModelViewerPanel { + public: + ModelWarperV3SourceModelViewerPanel(std::string_view label, std::shared_ptr state) : + ModelViewerPanel{label, ModelViewerPanelParameters{state->sourceModel()}, ModelViewerPanelFlag::NoHittest}, + m_State{std::move(state)} + {} + + private: + void impl_draw_content() final + { + if (m_State->isCameraLinked()) { + if (m_State->isOnlyCameraRotationLinked()) { + auto camera = getCamera(); + camera.phi = m_State->getLinkedCamera().phi; + camera.theta = m_State->getLinkedCamera().theta; + setCamera(camera); + } + else { + setCamera(m_State->getLinkedCamera()); + } + } + + setModelState(m_State->sourceModel()); + ModelViewerPanel::impl_draw_content(); + + // draw may have updated the camera, so flash is back + if (m_State->isCameraLinked()) { + if (m_State->isOnlyCameraRotationLinked()) { + auto camera = m_State->getLinkedCamera(); + camera.phi = getCamera().phi; + camera.theta = getCamera().theta; + m_State->setLinkedCamera(camera); + } + else { + m_State->setLinkedCamera(getCamera()); + } + } + } + + std::shared_ptr m_State; + }; + + // result model 3D viewer + class ModelWarperV3ResultModelViewerPanel final : public ModelViewerPanel { + public: + ModelWarperV3ResultModelViewerPanel(std::string_view label, std::shared_ptr state) : + ModelViewerPanel{label, ModelViewerPanelParameters{state->sourceModel()}, ModelViewerPanelFlag::NoHittest}, + m_State{std::move(state)} + {} + + private: + void impl_draw_content() final + { + if (auto warped = m_State->scaledModel()) { + // handle camera linking + if (m_State->isCameraLinked()) { + if (m_State->isOnlyCameraRotationLinked()) { + auto camera = getCamera(); + camera.phi = m_State->getLinkedCamera().phi; + camera.theta = m_State->getLinkedCamera().theta; + setCamera(camera); + } + else { + setCamera(m_State->getLinkedCamera()); + } + } + + setModelState(warped); + ModelViewerPanel::impl_draw_content(); + + // draw may have updated the camera, so flash is back + if (m_State->isCameraLinked()) { + if (m_State->isOnlyCameraRotationLinked()) { + auto camera = m_State->getLinkedCamera(); + camera.phi = getCamera().phi; + camera.theta = getCamera().theta; + m_State->setLinkedCamera(camera); + } + else { + m_State->setLinkedCamera(getCamera()); + } + } + } + else { + ui::begin_panel(name()); + ui::draw_text_panel_centered("cannot show result: model is not warpable"); + ui::end_panel(); + } + } + + std::shared_ptr m_State; + }; + + // main toolbar + class ModelWarperV3Toolbar final { + public: + ModelWarperV3Toolbar(std::string_view label, std::shared_ptr state) : + m_Label{label}, + m_State{std::move(state)} + {} + + void on_draw() + { + if (BeginToolbar(m_Label)) { + draw_content(); + } + ui::end_panel(); + } + private: + void draw_content() + { + DrawOpenModelButtonWithRecentFilesDropdown([this](auto maybeSelection) + { + m_State->actionOpenOsimOrPromptUser(std::move(maybeSelection)); + }); + + ui::same_line(); + + { + bool v = m_State->isCameraLinked(); + if (ui::draw_checkbox("link cameras", &v)) { + m_State->setCameraLinked(v); + } + } + + ui::same_line(); + { + bool v = m_State->isOnlyCameraRotationLinked(); + if (ui::draw_checkbox("only link rotation", &v)) { + m_State->setOnlyCameraRotationLinked(v); + } + } + } + + std::string m_Label; + std::shared_ptr m_State; + }; + + // control panel (design, set parameters, etc.) + class ModelWarperV3ControlPanel final : public Panel { + public: + ModelWarperV3ControlPanel(std::string_view panelName, std::shared_ptr state) : + Panel{nullptr, panelName}, + m_State{std::move(state)} + {} + + private: + void impl_draw_content() + { + draw_design_mode_scaling_mode_toggler(); + + ui::draw_dummy({0.0f, 0.5f*ui::get_text_line_height()}); + + if (m_IsInDesignMode) { + draw_design_mode_content(); + } + else { + draw_user_mode_content(); + } + } + + void draw_design_mode_scaling_mode_toggler() + { + constexpr Color activeButtonColor = Color::dark_green(); + + constexpr float spacing = 1.0f; + const float w = ui::calc_button_width("Design Mode") + spacing + ui::calc_button_width("Scaling Mode"); + const float lhs = 0.5f*(ui::get_content_region_available().x - w); + + ui::set_cursor_pos_x(lhs); + + { + int stylesPushed = 0; + if (m_IsInDesignMode) { + ui::push_style_color(ui::ColorVar::Button, activeButtonColor); + ui::push_style_color(ui::ColorVar::ButtonHovered, multiply_luminance(activeButtonColor, 1.1f)); + ui::push_style_color(ui::ColorVar::ButtonActive, multiply_luminance(activeButtonColor, 1.2f)); + stylesPushed += 3; + } + if (ui::draw_button(m_IsInDesignMode ? "Design Mode" OSC_ICON_CHECK : "Design Mode")) { + m_IsInDesignMode = true; + } + ui::pop_style_color(stylesPushed); + } + + ui::same_line(0.0f, spacing); + + { + int stylesPushed = 0; + if (not m_IsInDesignMode) { + ui::push_style_color(ui::ColorVar::Button, activeButtonColor); + ui::push_style_color(ui::ColorVar::ButtonHovered, multiply_luminance(activeButtonColor, 1.2f)); + ui::push_style_color(ui::ColorVar::ButtonActive, multiply_luminance(activeButtonColor, 1.1f)); + stylesPushed += 3; + } + + if (ui::draw_button(not m_IsInDesignMode ? "Scaling Mode" OSC_ICON_CHECK : "Scaling Mode")) { + m_IsInDesignMode = false; + } + ui::pop_style_color(stylesPushed); + } + } + + void draw_design_mode_content() + { + draw_design_mode_scaling_parameters(); + ui::draw_dummy({0.0f, 0.75f*ui::get_text_line_height()}); + draw_design_mode_scaling_steps(); + } + + void draw_design_mode_scaling_parameters() + { + ui::draw_text_centered("Scaling Parameters"); + ui::draw_separator(); + ui::draw_dummy({0.0f, 0.5f*ui::get_text_line_height()}); + if (m_State->hasScalingParameters()) { + if (ui::begin_table("##ScalingParameters", 2)) { + ui::table_setup_column("Parameter Name"); + ui::table_setup_column("Default Value"); + ui::table_headers_row(); + + m_State->forEachScalingParameterDefault([](const ScalingParameterDefault& decl) + { + ui::table_next_row(); + ui::table_set_column_index(0); + ui::draw_text(decl.get_parameter_name()); + ui::table_set_column_index(1); + ui::draw_text(decl.get_default_value()); + }); + ui::end_table(); + } + } + else { + ui::draw_text_disabled_and_centered("No Scaling Parameters."); + ui::draw_text_disabled_and_centered("(scaling parameters are normally implicitly added by scaling steps)"); + } + } + + void draw_design_mode_scaling_steps() + { + ui::draw_text_centered("Scaling Steps"); + ui::draw_separator(); + ui::draw_dummy({0.0f, 0.5f*ui::get_text_line_height()}); + + if (m_State->hasScalingSteps()) { + size_t i = 0; + for (const ScalingStep& step : m_State->iterateScalingSteps()) { + ui::push_id(i); + draw_design_mode_scaling_step(i, step); + ui::pop_id(); + ++i; + } + } + else { + ui::draw_text_disabled_and_centered("No scaling steps."); + ui::draw_text_disabled_and_centered("(the model will be left unmodified)"); + } + + ui::draw_dummy({0.0f, 0.25f*ui::get_text_line_height()}); + draw_design_mode_add_scaling_step_context_button(); + } + + void draw_design_mode_scaling_step(size_t stepIndex, const ScalingStep& step) + { + // draw header, help marker, etc. + ui::draw_text("#%zu: %s", stepIndex + 1, step.label().c_str()); + ui::same_line(); + ui::draw_help_marker(step.getDescription()); + + // draw deletion button + { + constexpr auto deletionButtonIcon = OSC_ICON_TRASH; + + ui::same_line(); + + const Vec2 oldCursorPos = ui::get_cursor_pos(); + const float endX = oldCursorPos.x + ui::get_content_region_available().x; + + const Vec2 newCursorPos = {endX - ui::calc_button_size(deletionButtonIcon).x, oldCursorPos.y}; + ui::set_cursor_pos(newCursorPos); + if (ui::draw_small_button(deletionButtonIcon)) { + m_State->eraseScalingStepDeferred(step); + } + } + + // draw validation messages + { + const auto messages = m_State->validateStep(step); + if (not messages.empty()) { + ui::draw_dummy({0.0f, 0.2f * ui::get_text_line_height()}); + ui::indent(); + for (const ScalingStepValidationMessage& message : messages) { + ui::push_style_color(ui::ColorVar::Text, ui_color(message)); + ui::draw_bullet_point(); + if (const auto propName = message.tryGetPropertyName()) { + ui::draw_text("%s: %s", propName->c_str(), message.getMessage()); + } + else { + ui::draw_text(message.getMessage()); + } + ui::pop_style_color(); + } + ui::unindent(); + ui::draw_dummy({0.0f, 0.2f * ui::get_text_line_height()}); + } + } + + // draw property editors + ui::indent(1.0f*ui::get_text_line_height()); + { + const auto path = step.getAbsolutePathString(); + const auto docPtr = m_State->getDocumentPtr(); + const auto [it, inserted] = m_StepPropertyEditors.try_emplace( + path, + this, + docPtr, + [docPtr, path] { return FindComponent(*docPtr, path); } + ); + if (inserted) { + it->second.insertInBlacklist("components"); + } + if (auto objectEdit = it->second.onDraw()) { + m_State->actionApplyObjectEditToScalingDocument(std::move(objectEdit).value()); + } + } + ui::unindent(1.0f*ui::get_text_line_height()); + ui::draw_dummy({0.0f, 0.5f*ui::get_text_line_height()}); + } + + void draw_design_mode_add_scaling_step_context_button() + { + ui::draw_button(OSC_ICON_PLUS "Add Scaling Step", {ui::get_content_region_available().x, ui::calc_button_size("").y}); + if (ui::begin_popup_context_menu("##AddScalingStepPopupMenu", ui::PopupFlag::MouseButtonLeft)) { + for (const auto& ptr : getScalingStepPrototypes()) { + ui::push_id(ptr.get()); + if (ui::draw_selectable(ptr->label())) { + m_State->addScalingStepDeferred(std::unique_ptr{ptr->clone()}); + } + ui::draw_tooltip_if_item_hovered(ptr->label(), ptr->getDescription(), ui::HoveredFlag::DelayNormal); + ui::pop_id(); + } + ui::end_popup(); + } + } + + void draw_user_mode_content() + { + if (not m_State->hasScalingSteps()) { + ui::draw_text_disabled_and_centered("No scaling steps."); + ui::draw_text_disabled_and_centered("(the model will be left unmodified)"); + ui::draw_text_disabled_and_centered("Switch to design mode to add scaling steps"); + } + } + + bool m_IsInDesignMode = true; + std::shared_ptr m_State; + std::unordered_map m_StepPropertyEditors; + }; +} + +class osc::ModelWarperV3Tab::Impl final : public TabPrivate { +public: + static CStringView static_label() { return "OpenSim/ModelWarperV3"; } + + Impl(Tab& owner, Widget* parent) : + TabPrivate{owner, parent, static_label()} + { + m_PanelManager->register_toggleable_panel("Control Panel", [state = m_State](std::string_view panelName) + { + return std::make_shared(panelName, state); + }); + m_PanelManager->register_toggleable_panel("Source Model", [state = m_State](std::string_view panelName) + { + return std::make_shared(panelName, state); + }); + m_PanelManager->register_toggleable_panel("Result Model", [state = m_State](std::string_view panelName) + { + return std::make_shared(panelName, state); + }); + m_PanelManager->register_toggleable_panel("Log", [](std::string_view panelName) + { + return std::make_shared(panelName); + }); + m_PanelManager->register_toggleable_panel("Performance", [](std::string_view panelName) + { + return std::make_shared(panelName); + }); + } + + void on_mount() + { + m_PanelManager->on_mount(); + } + + void on_unmount() + { + m_PanelManager->on_unmount(); + } + + void on_tick() + { + m_State->on_tick(); + m_PanelManager->on_tick(); + } + + void on_draw_main_menu() + { + m_WindowMenu.on_draw(); + m_AboutTab.onDraw(); + } + + void on_draw() + { + ui::enable_dockspace_over_main_viewport(); + m_PanelManager->on_draw(); + m_Toolbar.on_draw(); + } + +private: + std::shared_ptr m_State = std::make_shared(); + + std::shared_ptr m_PanelManager = std::make_shared(); + WindowMenu m_WindowMenu{m_PanelManager}; + MainMenuAboutTab m_AboutTab; + ModelWarperV3Toolbar m_Toolbar{"##ModelWarperV3Toolbar", m_State}; +}; + +CStringView osc::ModelWarperV3Tab::id() { return Impl::static_label(); } + +osc::ModelWarperV3Tab::ModelWarperV3Tab(Widget& parent) : + Tab{std::make_unique(*this, &parent)} +{} +void osc::ModelWarperV3Tab::impl_on_mount() { private_data().on_mount(); } +void osc::ModelWarperV3Tab::impl_on_unmount() { private_data().on_unmount(); } +void osc::ModelWarperV3Tab::impl_on_tick() { private_data().on_tick(); } +void osc::ModelWarperV3Tab::impl_on_draw_main_menu() { private_data().on_draw_main_menu(); } +void osc::ModelWarperV3Tab::impl_on_draw() { private_data().on_draw(); } diff --git a/src/OpenSimCreator/UI/ModelWarperV3/ModelWarperV3Tab.h b/src/OpenSimCreator/UI/ModelWarperV3/ModelWarperV3Tab.h new file mode 100644 index 000000000..5d402cc81 --- /dev/null +++ b/src/OpenSimCreator/UI/ModelWarperV3/ModelWarperV3Tab.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +namespace osc { class Widget; } + +namespace osc +{ + class ModelWarperV3Tab final : public Tab { + public: + static CStringView id(); + + explicit ModelWarperV3Tab(Widget&); + + private: + void impl_on_mount() final; + void impl_on_unmount() final; + void impl_on_tick() final; + void impl_on_draw_main_menu() final; + void impl_on_draw() final; + + class Impl; + OSC_WIDGET_DATA_GETTERS(Impl); + }; +} diff --git a/src/OpenSimCreator/UI/OpenSimCreatorTabs.h b/src/OpenSimCreator/UI/OpenSimCreatorTabs.h index edd8c55ee..dfb724412 100644 --- a/src/OpenSimCreator/UI/OpenSimCreatorTabs.h +++ b/src/OpenSimCreator/UI/OpenSimCreatorTabs.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +23,7 @@ namespace osc mi::MeshImporterTab, ModelEditorTab, mow::ModelWarperTab, + ModelWarperV3Tab, FrameDefinitionTab, MeshHittestTab, RendererGeometryShaderTab, diff --git a/src/OpenSimCreator/UI/Shared/FunctionCurveViewerPopup.cpp b/src/OpenSimCreator/UI/Shared/FunctionCurveViewerPopup.cpp index e2e98674a..670523314 100644 --- a/src/OpenSimCreator/UI/Shared/FunctionCurveViewerPopup.cpp +++ b/src/OpenSimCreator/UI/Shared/FunctionCurveViewerPopup.cpp @@ -1,6 +1,6 @@ #include "FunctionCurveViewerPopup.h" -#include +#include #include #include @@ -31,27 +31,25 @@ class osc::FunctionCurveViewerPanel::Impl final : public PanelPrivate { Impl( FunctionCurveViewerPanel& owner, std::string_view popupName, - std::shared_ptr targetModel, + std::shared_ptr targetComponent, std::function functionGetter) : PanelPrivate{owner, nullptr, popupName, ui::WindowFlag::AlwaysAutoResize}, - m_Model{std::move(targetModel)}, + m_Component{std::move(targetComponent)}, m_FunctionGetter{std::move(functionGetter)} {} private: class FunctionParameters final { public: - explicit FunctionParameters(const IModelStatePair& model) : - modelVerison{model.getModelVersion()}, - stateVersion{model.getStateVersion()} + explicit FunctionParameters(const IVersionedComponentAccessor& component) : + componentVersion{component.getComponentVersion()} {} friend bool operator==(const FunctionParameters& lhs, const FunctionParameters& rhs) = default; - void setVersionFromModel(const IModelStatePair& model) + void setVersionFromComponent(const IVersionedComponentAccessor& component) { - modelVerison = model.getModelVersion(); - stateVersion = model.getStateVersion(); + componentVersion = component.getComponentVersion(); } ClosedInterval getInputRange() const { return inputRange; } @@ -60,8 +58,7 @@ class osc::FunctionCurveViewerPanel::Impl final : public PanelPrivate { int& updNumPoints() { return numPoints; } private: - UID modelVerison; - UID stateVersion; + UID componentVersion; ClosedInterval inputRange = {-1.0f, 1.0f}; int numPoints = 100; }; @@ -106,7 +103,7 @@ class osc::FunctionCurveViewerPanel::Impl final : public PanelPrivate { void draw_content() { // update parameter state and check if replotting is necessary - m_LatestParameters.setVersionFromModel(*m_Model); + m_LatestParameters.setVersionFromComponent(*m_Component); if (m_LatestParameters != m_PlottedParameters) { m_PlottedParameters = m_LatestParameters; m_PlotPoints = generatePlotPoints(m_LatestParameters); @@ -162,7 +159,7 @@ class osc::FunctionCurveViewerPanel::Impl final : public PanelPrivate { const OpenSim::Function* function = m_FunctionGetter(); if (not function) { - m_Error = "could not get the function from the model (maybe the model was edited, or the function was deleted?)"; + m_Error = "could not get the function from the component (maybe the component was edited, or the function was deleted?)"; return rv; } @@ -208,9 +205,9 @@ class osc::FunctionCurveViewerPanel::Impl final : public PanelPrivate { } } - std::shared_ptr m_Model; + std::shared_ptr m_Component; std::function m_FunctionGetter; - FunctionParameters m_LatestParameters{*m_Model}; + FunctionParameters m_LatestParameters{*m_Component}; std::optional m_PlottedParameters; PlotPoints m_PlotPoints; std::optional m_Error; @@ -218,9 +215,9 @@ class osc::FunctionCurveViewerPanel::Impl final : public PanelPrivate { osc::FunctionCurveViewerPanel::FunctionCurveViewerPanel( std::string_view panelName, - std::shared_ptr targetModel, + std::shared_ptr targetComponent, std::function functionGetter) : - Panel{std::make_unique(*this, panelName, std::move(targetModel), std::move(functionGetter))} + Panel{std::make_unique(*this, panelName, std::move(targetComponent), std::move(functionGetter))} {} void osc::FunctionCurveViewerPanel::impl_draw_content() { private_data().draw_content(); } diff --git a/src/OpenSimCreator/UI/Shared/FunctionCurveViewerPopup.h b/src/OpenSimCreator/UI/Shared/FunctionCurveViewerPopup.h index 8a44b0d38..532146161 100644 --- a/src/OpenSimCreator/UI/Shared/FunctionCurveViewerPopup.h +++ b/src/OpenSimCreator/UI/Shared/FunctionCurveViewerPopup.h @@ -7,7 +7,7 @@ #include namespace OpenSim { class Function; } -namespace osc { class IModelStatePair; } +namespace osc { class IVersionedComponentAccessor; } namespace osc { @@ -16,7 +16,7 @@ namespace osc public: explicit FunctionCurveViewerPanel( std::string_view panelName, - std::shared_ptr targetModel, + std::shared_ptr targetComponent, std::function functionGetter ); diff --git a/src/OpenSimCreator/UI/Shared/GeometryPathEditorPopup.cpp b/src/OpenSimCreator/UI/Shared/GeometryPathEditorPopup.cpp index 7d9d6c606..1f1acbcf5 100644 --- a/src/OpenSimCreator/UI/Shared/GeometryPathEditorPopup.cpp +++ b/src/OpenSimCreator/UI/Shared/GeometryPathEditorPopup.cpp @@ -1,11 +1,11 @@ #include "GeometryPathEditorPopup.h" -#include +#include #include +#include #include #include -#include #include #include #include @@ -119,12 +119,12 @@ class osc::GeometryPathEditorPopup::Impl final : public StandardPopup { public: Impl( std::string_view popupName_, - std::shared_ptr targetModel_, + std::shared_ptr targetComponent_, std::function geometryPathGetter_, std::function onLocalCopyEdited_) : StandardPopup{popupName_, {768.0f, 0.0f}, ui::WindowFlag::AlwaysAutoResize}, - m_TargetModel{std::move(targetModel_)}, + m_TargetComponent{std::move(targetComponent_)}, m_GeometryPathGetter{std::move(geometryPathGetter_)}, m_OnLocalCopyEdited{std::move(onLocalCopyEdited_)}, m_EditedGeometryPath{CopyOrDefaultGeometryPath(m_GeometryPathGetter)} @@ -144,7 +144,7 @@ class osc::GeometryPathEditorPopup::Impl final : public StandardPopup { } // else: the geometry path exists, but this UI should edit the cached // `m_EditedGeometryPath`, which is independent of the original data - // and the target model (so that edits can be applied transactionally) + // and the target component (so that edits can be applied transactionally) ui::draw_text("Path Points:"); ui::draw_separator(); @@ -241,7 +241,7 @@ class osc::GeometryPathEditorPopup::Impl final : public StandardPopup { ui::same_line(); ui::push_style_color(ui::ColorVar::Text, Color{0.7f, 0.0f, 0.0f}); - if (ui::draw_small_button(OSC_ICON_TIMES)) + if (ui::draw_small_button(OSC_ICON_TRASH)) { m_RequestedAction = RequestedAction{RequestedAction::Type::Delete, i}; } @@ -299,7 +299,7 @@ class osc::GeometryPathEditorPopup::Impl final : public StandardPopup { ui::set_next_item_width(width); if (ui::begin_combobox("##framesel", label)) { - for (const OpenSim::Frame& frame : m_TargetModel->getModel().getComponentList()) + for (const OpenSim::Frame& frame : m_TargetComponent->getComponent().getComponentList()) { const std::string absPath = frame.getAbsolutePathString(); if (ui::draw_selectable(absPath)) @@ -354,7 +354,7 @@ class osc::GeometryPathEditorPopup::Impl final : public StandardPopup { m_RequestedAction.reset(); // action handled: resets } - std::shared_ptr m_TargetModel; + std::shared_ptr m_TargetComponent; std::function m_GeometryPathGetter; std::function m_OnLocalCopyEdited; @@ -365,37 +365,19 @@ class osc::GeometryPathEditorPopup::Impl final : public StandardPopup { osc::GeometryPathEditorPopup::GeometryPathEditorPopup( std::string_view popupName_, - std::shared_ptr targetModel_, + std::shared_ptr targetComponent_, std::function geometryPathGetter_, std::function onLocalCopyEdited_) : - m_Impl{std::make_unique(popupName_, std::move(targetModel_), std::move(geometryPathGetter_), std::move(onLocalCopyEdited_))} + m_Impl{std::make_unique(popupName_, std::move(targetComponent_), std::move(geometryPathGetter_), std::move(onLocalCopyEdited_))} {} osc::GeometryPathEditorPopup::GeometryPathEditorPopup(GeometryPathEditorPopup&&) noexcept = default; osc::GeometryPathEditorPopup& osc::GeometryPathEditorPopup::operator=(GeometryPathEditorPopup&&) noexcept = default; osc::GeometryPathEditorPopup::~GeometryPathEditorPopup() noexcept = default; -bool osc::GeometryPathEditorPopup::impl_is_open() const -{ - return m_Impl->is_open(); -} -void osc::GeometryPathEditorPopup::impl_open() -{ - m_Impl->open(); -} -void osc::GeometryPathEditorPopup::impl_close() -{ - m_Impl->close(); -} -bool osc::GeometryPathEditorPopup::impl_begin_popup() -{ - return m_Impl->begin_popup(); -} -void osc::GeometryPathEditorPopup::impl_on_draw() -{ - m_Impl->on_draw(); -} -void osc::GeometryPathEditorPopup::impl_end_popup() -{ - m_Impl->end_popup(); -} +bool osc::GeometryPathEditorPopup::impl_is_open() const { return m_Impl->is_open(); } +void osc::GeometryPathEditorPopup::impl_open() { m_Impl->open(); } +void osc::GeometryPathEditorPopup::impl_close() { m_Impl->close(); } +bool osc::GeometryPathEditorPopup::impl_begin_popup() { return m_Impl->begin_popup(); } +void osc::GeometryPathEditorPopup::impl_on_draw() { m_Impl->on_draw(); } +void osc::GeometryPathEditorPopup::impl_end_popup() { m_Impl->end_popup(); } diff --git a/src/OpenSimCreator/UI/Shared/GeometryPathEditorPopup.h b/src/OpenSimCreator/UI/Shared/GeometryPathEditorPopup.h index 299ec876b..5e8187550 100644 --- a/src/OpenSimCreator/UI/Shared/GeometryPathEditorPopup.h +++ b/src/OpenSimCreator/UI/Shared/GeometryPathEditorPopup.h @@ -7,7 +7,7 @@ #include namespace OpenSim { class GeometryPath; } -namespace osc { class IModelStatePair; } +namespace osc { class IComponentAccessor; } namespace osc { @@ -16,7 +16,7 @@ namespace osc public: GeometryPathEditorPopup( std::string_view popupName_, - std::shared_ptr targetModel_, + std::shared_ptr targetComponent_, std::function geometryPathGetter_, std::function onLocalCopyEdited_ ); diff --git a/src/OpenSimCreator/UI/Shared/ObjectPropertiesEditor.cpp b/src/OpenSimCreator/UI/Shared/ObjectPropertiesEditor.cpp index 497be19d6..3336b8786 100644 --- a/src/OpenSimCreator/UI/Shared/ObjectPropertiesEditor.cpp +++ b/src/OpenSimCreator/UI/Shared/ObjectPropertiesEditor.cpp @@ -1,6 +1,7 @@ #include "ObjectPropertiesEditor.h" #include +#include #include #include #include @@ -16,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -45,6 +45,7 @@ #include #include #include +#include #include using namespace osc; @@ -334,7 +335,7 @@ namespace // construction-time arguments for the property editor struct PropertyEditorArgs final { LifetimedPtr parent; - std::shared_ptr model; + std::shared_ptr component; std::function objectAccessor; std::function propertyAccessor; }; @@ -381,32 +382,32 @@ namespace return Traits::TryGetDowncasted(tryGetProperty()); } - const std::function& getPropertyAccessor() const - { - return m_Args.propertyAccessor; - } - std::function getDowncastedPropertyAccessor() const { - return [inner = getPropertyAccessor()]() + return [inner = m_Args.propertyAccessor]() { return Traits::TryGetDowncasted(inner()); }; } - const OpenSim::Model& getModel() const + const OpenSim::Component& getRootComponent() const { - return m_Args.model->getModel(); + return m_Args.component->getComponent(); } - std::shared_ptr getModelPtr() const + std::shared_ptr tryGetComponentSharedPtr() const { - return m_Args.model; + return m_Args.component; } - const SimTK::State& getState() const + const SimTK::State* tryGetState() const { - return m_Args.model->getState(); + if (auto* msp = dynamic_cast(m_Args.component.get())) { + return &msp->getState(); + } + else { + return nullptr; + } } const OpenSim::Object* tryGetObject() const @@ -427,7 +428,10 @@ namespace return GetAbsolutePath(*component); } - Widget& parentWidget() { return *m_Args.parent; } + Widget* tryGetParentWidget() + { + return m_Args.parent.get(); + } private: bool implIsCompatibleWith(const OpenSim::AbstractProperty& prop) const final @@ -450,27 +454,27 @@ namespace private: std::optional> implOnDraw() final { - const property_type* maybeProp = tryGetDowncastedProperty(); - if (not maybeProp) { + // fetch property from object + const property_type* prop = tryGetDowncastedProperty(); + if (not prop) { return std::nullopt; } - const property_type& prop = *maybeProp; // update any cached data - if (prop != m_OriginalProperty) { - m_OriginalProperty = prop; - m_EditedProperty = prop; + if (*prop != m_OriginalProperty) { + m_OriginalProperty = *prop; + m_EditedProperty = *prop; } ui::draw_separator(); - // draw name of the property in left-hand column + // draw the name of the property in left-hand column DrawPropertyName(m_EditedProperty); ui::next_column(); // draw `n` editors in right-hand column std::optional> rv; - for (int idx = 0; idx < max(m_EditedProperty.size(), 1); ++idx) { + for (int idx = 0; idx < m_EditedProperty.size(); ++idx) { ui::push_id(idx); std::optional> editorRv = drawIthEditor(idx); ui::pop_id(); @@ -479,6 +483,37 @@ namespace rv = std::move(editorRv); } } + + // draw "Add Entry" or "Populate" buttons + if (m_EditedProperty.isListProperty()) { + // it's a list property, so draw an "Add Entry" button + // + // users can use this to add a blank entry to this editor. The entry isn't + // emitted to the rest of the UI until the user edits it. + + // disable the button if at the maximum list size or the last entry in the + // list is a blank (probably from the last time the user clicked "Add Entry") + const bool disabled = + (m_EditedProperty.size() >= m_EditedProperty.getMaxListSize()) or + (m_EditedProperty.size() > 0 and m_EditedProperty[m_EditedProperty.size() - 1] == std::string{}); + + if (disabled) { + ui::begin_disabled(); + } + if (ui::draw_button(OSC_ICON_PLUS " Add Entry", { ui::get_content_region_available().x, ui::calc_button_size("").y })) { + m_EditedProperty.appendValue(std::string{}); // append blank entry (don't emit upstream until user edits it) + } + if (disabled) { + ui::end_disabled(); + } + } + else if (m_EditedProperty.isOptionalProperty() and m_EditedProperty.empty()) { + // it's an optional property, so draw a "Populate" button if it's unoccupied + if (ui::draw_button(OSC_ICON_PLUS " Populate", { ui::get_content_region_available().x, ui::calc_button_size("").y })) { + m_EditedProperty.appendValue(std::string{}); // append blank entry (don't emit upstream until user edits it) + } + } + ui::next_column(); return rv; @@ -489,20 +524,15 @@ namespace { std::optional> rv; - // draw trash can that can delete an element from the property's list - if (m_EditedProperty.isListProperty()) { - if (ui::draw_button(OSC_ICON_TRASH)) { - rv = MakeSimplePropertyElementDeleter(idx); - } - ui::same_line(); - } + // calculate space taken by deletion button at end of line (if necessary) + const float deletionButtonWidth = m_EditedProperty.size() > m_EditedProperty.getMinListSize() ? + ui::calc_button_size(OSC_ICON_TRASH).x : + 0.0f; // read stored value from edited property - // - // care: optional properties have size==0, so perform a range check - std::string value = idx < m_EditedProperty.size() ? m_EditedProperty.getValue(idx) : std::string{}; + std::string value = m_EditedProperty.getValue(idx); - ui::set_next_item_width(ui::get_content_region_available().x); + ui::set_next_item_width(ui::get_content_region_available().x - deletionButtonWidth); if (ui::draw_string_input("##stringeditor", value)) { // update the edited property - don't rely on ImGui to remember edits m_EditedProperty.setValue(idx, value); @@ -515,6 +545,14 @@ namespace rv = MakePropertyValueSetter(idx, m_EditedProperty.getValue(idx)); } + // if applicable, add deletion button + if (m_EditedProperty.size() > m_EditedProperty.getMinListSize()) { + ui::same_line(); + if (ui::draw_button(OSC_ICON_TRASH)) { + rv = MakeSimplePropertyElementDeleter(idx); + } + } + return rv; } @@ -568,7 +606,7 @@ namespace { std::optional> rv; - // draw trash can that can delete an element from the property's list + // draw deletion button that can delete an element from the property's list if (m_EditedProperty.isListProperty()) { if (ui::draw_button(OSC_ICON_TRASH)) { rv = MakeSimplePropertyElementDeleter(idx); @@ -652,7 +690,7 @@ namespace { std::optional> rv; - // draw trash can that can delete an element from the property's list + // draw deletion button that can delete an element from the property's list if (m_EditedProperty.isListProperty()) { if (ui::draw_button(OSC_ICON_TRASH)) { rv = MakeSimplePropertyElementDeleter(idx); @@ -699,25 +737,25 @@ namespace class ValueConverter final { public: ValueConverter( - float modelToEditedValueScaler_, - SimTK::Transform modelToEditedTransform_) : + float propertyToEditedValueScaler_, + SimTK::Transform propertyToEditedTransform_) : - m_ModelToEditedValueScaler{modelToEditedValueScaler_}, - m_ModelToEditedTransform{std::move(modelToEditedTransform_)} + m_PropertyToEditedValueScaler{propertyToEditedValueScaler_}, + m_PropertyToEditedTransform{std::move(propertyToEditedTransform_)} {} - Vec3 modelValueToEditedValue(const Vec3& modelValue) const + Vec3 propertyValueToEditedValue(const Vec3& propertyValue) const { - return to(static_cast(m_ModelToEditedValueScaler) * (m_ModelToEditedTransform * to(modelValue))); + return to(static_cast(m_PropertyToEditedValueScaler) * (m_PropertyToEditedTransform * to(propertyValue))); } - Vec3 editedValueToModelValue(const Vec3& editedValue) const + Vec3 editedValueToPropertyValue(const Vec3& editedValue) const { - return to(m_ModelToEditedTransform.invert() * to(editedValue/m_ModelToEditedValueScaler)); + return to(m_PropertyToEditedTransform.invert() * to(editedValue/m_PropertyToEditedValueScaler)); } private: - float m_ModelToEditedValueScaler; - SimTK::Transform m_ModelToEditedTransform; + float m_PropertyToEditedValueScaler; + SimTK::Transform m_PropertyToEditedTransform; }; // returns `true` if the Vec3 property is edited in radians @@ -740,8 +778,8 @@ namespace return nullptr; // the object isn't an OpenSim component } - if (&component->getRoot() != &getModel()) { - return nullptr; // the object is not within the tree of the model (#800) + if (&component->getRoot() != &getRootComponent()) { + return nullptr; // the object is not within the tree of the root component (#800) } const auto positionPropName = TryGetPositionalPropertyName(*component); @@ -765,12 +803,17 @@ namespace // property's value to ground std::optional getParentToGroundTransform() const { - if (const OpenSim::PhysicalFrame* frame = tryGetParentFrame()) { - return frame->getTransformInGround(getState()); + const SimTK::State* state = tryGetState(); + if (not state) { + return std::nullopt; } - else { + + const OpenSim::PhysicalFrame* frame = tryGetParentFrame(); + if (not frame) { return std::nullopt; } + + return frame->getTransformInGround(*state); } // if the user has selected a different frame in which to edit 3D quantities, then @@ -782,9 +825,17 @@ namespace return std::nullopt; } - const OpenSim::Model& model = getModel(); - const auto* frame = FindComponent(model, *m_MaybeUserSelectedFrameAbsPath); - return frame->getTransformInGround(getState()).invert(); + const SimTK::State* state = tryGetState(); + if (not state) { + return std::nullopt; + } + + const auto* frame = FindComponent(getRootComponent(), *m_MaybeUserSelectedFrameAbsPath); + if (not frame) { + return std::nullopt; + } + + return frame->getTransformInGround(*state).invert(); } ValueConverter getValueConverter() const @@ -881,8 +932,8 @@ namespace ui::draw_separator(); } - // draw selectable for each frame in the model - for (const OpenSim::Frame& frame : getModel().getComponentList()) { + // draw selectable for each frame in the component tree + for (const OpenSim::Frame& frame : getRootComponent().getComponentList()) { const OpenSim::ComponentPath frameAbsPath = GetAbsolutePath(frame); ui::push_id(imguiID++); @@ -907,7 +958,7 @@ namespace { std::optional> rv; - // draw trash can that can delete an element from the property's list + // draw deletion button that can delete an element from the property's list if (m_EditedProperty.isListProperty()) { if (ui::draw_button(OSC_ICON_TRASH)) { rv = MakeSimplePropertyElementDeleter(idx); @@ -919,7 +970,7 @@ namespace // // care: optional properties have size==0, so perform a range check const Vec3 rawValue = to(idx < m_EditedProperty.size() ? m_EditedProperty.getValue(idx) : SimTK::Vec3{0.0}); - const Vec3 editedValue = valueConverter.modelValueToEditedValue(rawValue); + const Vec3 editedValue = valueConverter.propertyValueToEditedValue(rawValue); // draw an editor for each component of the Vec3 bool shouldSave = false; @@ -957,7 +1008,7 @@ namespace if (drawRV.wasEdited) { // un-convert the value on save - const Vec3 savedValue = valueConverter.editedValueToModelValue(editedValue); + const Vec3 savedValue = valueConverter.editedValueToPropertyValue(editedValue); m_EditedProperty.setValue(idx, to(savedValue)); } @@ -1042,7 +1093,7 @@ namespace { std::optional> rv; - // draw trash can that can delete an element from the property's list + // draw deletion button that can delete an element from the property's list if (m_EditedProperty.isListProperty()) { if (ui::draw_button(OSC_ICON_TRASH)) { rv = MakeSimplePropertyElementDeleter(idx); @@ -1128,7 +1179,7 @@ namespace { std::optional> rv; - // draw trash can that can delete an element from the property's list + // draw deletion button that can delete an element from the property's list if (m_EditedProperty.isListProperty()) { if (ui::draw_button(OSC_ICON_TRASH)) { rv = MakeSimplePropertyElementDeleter(idx); @@ -1299,7 +1350,9 @@ namespace // update cached editors, if necessary if (not m_MaybeNestedEditor) { - m_MaybeNestedEditor.emplace(parentWidget(), getModelPtr(), [¶ms]() { return ¶ms; }); + if (const auto componentPtr = tryGetComponentSharedPtr()) { + m_MaybeNestedEditor.emplace(tryGetParentWidget(), componentPtr, [¶ms]() { return ¶ms; }); + } } ObjectPropertiesEditor& nestedEditor = *m_MaybeNestedEditor; @@ -1349,8 +1402,17 @@ namespace ui::draw_separator(); DrawPropertyName(prop); ui::next_column(); - if (ui::draw_button(OSC_ICON_EDIT)) { - App::post_event(parentWidget(), createGeometryPathEditorPopup()); + { + Widget* parentWidget = tryGetParentWidget(); + const auto componentPtr = tryGetComponentSharedPtr(); + if (parentWidget and componentPtr) { + if (ui::draw_button(OSC_ICON_EDIT)) { + App::post_event(*parentWidget, createGeometryPathEditorPopup(componentPtr)); + } + } + else { + ui::draw_text(prop.toString()); + } } ui::next_column(); @@ -1364,12 +1426,12 @@ namespace } } - std::unique_ptr createGeometryPathEditorPopup() + std::unique_ptr createGeometryPathEditorPopup(std::shared_ptr componentPtr) { - auto accessor = getDowncastedPropertyAccessor(); + const auto accessor = getDowncastedPropertyAccessor(); return std::make_unique( "Edit Geometry Path", - getModelPtr(), + componentPtr, [accessor]() -> const OpenSim::GeometryPath* { const property_type* p = accessor(); @@ -1434,39 +1496,48 @@ namespace DrawPropertyName(*prop); ui::next_column(); - - if (ui::draw_button(OSC_ICON_EYE)) { - - // care: the accessor here differs from the default because the user's selection - // can change the accessor's behavior. This is a panel, so it should stick to - // whatever was selected when the panel was spawned. - auto panel = std::make_unique( - generatePopupName(*prop), - getModelPtr(), - [model = getModelPtr(), parentPath = tryGetObjectAbsPath(), propname = prop->getName()]() -> const OpenSim::Function* - { - auto* parentComponent = FindComponent(*model, parentPath); - if (not parentComponent) { - return nullptr; - } - if (not parentComponent->hasProperty(propname)) { - return nullptr; - } - auto& prop = parentComponent->getPropertyByName(propname); - if (prop.empty()) { - return nullptr; - } - if (not prop.isObjectProperty()) { - return nullptr; - } - if (prop.empty()) { - return nullptr; - } - return dynamic_cast(&prop.getValueAsObject(0)); + { + Widget* parentWidget = tryGetParentWidget(); + const auto componentPtr = tryGetComponentSharedPtr(); + if (parentWidget and componentPtr) { + if (ui::draw_button(OSC_ICON_EYE)) { + + // care: the accessor here differs from the default because the user's selection + // can change the accessor's behavior. This is a panel, so it should stick to + // whatever was selected when the panel was spawned. + auto panel = std::make_unique( + generatePopupName(*prop), + componentPtr, + [component = componentPtr, parentPath = tryGetObjectAbsPath(), propname = prop->getName()]() -> const OpenSim::Function* + { + auto* parentComponent = FindComponent(*component, parentPath); + if (not parentComponent) { + return nullptr; + } + if (not parentComponent->hasProperty(propname)) { + return nullptr; + } + auto& prop = parentComponent->getPropertyByName(propname); + if (prop.empty()) { + return nullptr; + } + if (not prop.isObjectProperty()) { + return nullptr; + } + if (prop.empty()) { + return nullptr; + } + return dynamic_cast(&prop.getValueAsObject(0)); + } + ); + App::post_event(*parentWidget, std::move(panel)); } - ); - App::post_event(parentWidget(), std::move(panel)); + } + else { + ui::draw_text(prop->toString()); + } } + ui::draw_tooltip_if_item_hovered("View Function", OSC_ICON_MAGIC " Experimental Feature " OSC_ICON_MAGIC ": currently, plots the `OpenSim::Function`, but it doesn't know what the X or Y axes are, or what values might be reasonable for either. It also doesn't spawn a non-modal panel, which would be handy if you wanted to view multiple functions at the same time - I should work on that ;)"); ui::same_line(); ui::draw_text(prop->getTypeName()); @@ -1586,18 +1657,18 @@ namespace class osc::ObjectPropertiesEditor::Impl final { public: Impl( - Widget& parent, - std::shared_ptr targetModel_, - std::function objectGetter_) : + Widget* parentWidget, + std::shared_ptr targetComponent, + std::function objectGetter) : - m_Parent{parent.weak_ref()}, - m_TargetModel{std::move(targetModel_)}, - m_ObjectGetter{std::move(objectGetter_)} + m_ParentWidget{parentWidget ? parentWidget->weak_ref() : nullptr}, + m_TargetComponent{std::move(targetComponent)}, + m_ObjectGetter{std::move(objectGetter)} {} std::optional onDraw() { - const bool disabled = m_TargetModel->isReadonly(); + const bool disabled = m_TargetComponent->isReadonly(); if (disabled) { ui::begin_disabled(); } @@ -1613,6 +1684,11 @@ class osc::ObjectPropertiesEditor::Impl final { return rv; } + + void insertInBlacklist(std::string_view propertyName) + { + m_Blacklist.insert(std::string{propertyName}); + } private: // draws all property editors for the given object @@ -1655,6 +1731,9 @@ class osc::ObjectPropertiesEditor::Impl final { // be manipulated via socket, rather than property, editors return std::nullopt; } + if (m_Blacklist.contains(prop.getName())) { + return std::nullopt; + } else if (IPropertyEditor* maybeEditor = tryGetPropertyEditor(prop)) { return drawPropertyEditor(obj, prop, *maybeEditor); } @@ -1702,8 +1781,8 @@ class osc::ObjectPropertiesEditor::Impl final { // need to create a new editor because either it hasn't been made yet or the existing // editor is for a different type it->second = c_Registry.tryCreateEditor({ - .parent = m_Parent, - .model = m_TargetModel, + .parent = m_ParentWidget, + .component = m_TargetComponent, .objectAccessor = m_ObjectGetter, .propertyAccessor = MakePropertyAccessor(m_ObjectGetter, prop.getName()), }); @@ -1712,22 +1791,24 @@ class osc::ObjectPropertiesEditor::Impl final { return it->second.get(); } - LifetimedPtr m_Parent; - std::shared_ptr m_TargetModel; + LifetimedPtr m_ParentWidget; + std::shared_ptr m_TargetComponent; std::function m_ObjectGetter; + std::unordered_set m_Blacklist; const OpenSim::Object* m_PreviousObject = nullptr; std::unordered_map> m_PropertyEditorsByName; }; osc::ObjectPropertiesEditor::ObjectPropertiesEditor( - Widget& parent_, - std::shared_ptr targetModel_, - std::function objectGetter_) : + Widget* parentWidget, + std::shared_ptr targetComponent, + std::function objectGetter) : - m_Impl{std::make_unique(parent_, std::move(targetModel_), std::move(objectGetter_))} + m_Impl{std::make_unique(parentWidget, std::move(targetComponent), std::move(objectGetter))} {} osc::ObjectPropertiesEditor::ObjectPropertiesEditor(ObjectPropertiesEditor&&) noexcept = default; osc::ObjectPropertiesEditor& osc::ObjectPropertiesEditor::operator=(ObjectPropertiesEditor&&) noexcept = default; osc::ObjectPropertiesEditor::~ObjectPropertiesEditor() noexcept = default; +void osc::ObjectPropertiesEditor::insertInBlacklist(std::string_view propertyName) { m_Impl->insertInBlacklist(propertyName); } std::optional osc::ObjectPropertiesEditor::onDraw() { return m_Impl->onDraw(); } diff --git a/src/OpenSimCreator/UI/Shared/ObjectPropertiesEditor.h b/src/OpenSimCreator/UI/Shared/ObjectPropertiesEditor.h index 40f725f78..311fef838 100644 --- a/src/OpenSimCreator/UI/Shared/ObjectPropertiesEditor.h +++ b/src/OpenSimCreator/UI/Shared/ObjectPropertiesEditor.h @@ -7,16 +7,16 @@ #include namespace OpenSim { class Object; } -namespace osc { class IModelStatePair; } +namespace osc { class IVersionedComponentAccessor; } namespace osc { class Widget; } namespace osc { class ObjectPropertiesEditor final { public: - ObjectPropertiesEditor( - Widget&, - std::shared_ptr targetModel, + explicit ObjectPropertiesEditor( + Widget* parentWidget, + std::shared_ptr targetComponent, std::function objectGetter ); ObjectPropertiesEditor(const ObjectPropertiesEditor&) = delete; @@ -25,6 +25,8 @@ namespace osc ObjectPropertiesEditor& operator=(ObjectPropertiesEditor&&) noexcept; ~ObjectPropertiesEditor() noexcept; + void insertInBlacklist(std::string_view propertyName); + // does not actually apply any property changes - the caller should check+apply the return value std::optional onDraw(); diff --git a/src/OpenSimCreator/UI/Shared/PropertiesPanel.cpp b/src/OpenSimCreator/UI/Shared/PropertiesPanel.cpp index a0a536cb3..7e0b87fdf 100644 --- a/src/OpenSimCreator/UI/Shared/PropertiesPanel.cpp +++ b/src/OpenSimCreator/UI/Shared/PropertiesPanel.cpp @@ -122,7 +122,7 @@ class osc::PropertiesPanel::Impl final : public PanelPrivate { PanelPrivate{owner, &parent, panelName}, m_Model{std::move(model)}, - m_SelectionPropertiesEditor{parent, m_Model, [model = m_Model](){ return model->getSelected(); }} + m_SelectionPropertiesEditor{&parent, m_Model, [model = m_Model](){ return model->getSelected(); }} {} void draw_content() diff --git a/src/OpenSimCreator/UI/Simulation/SimulationOutputPlot.cpp b/src/OpenSimCreator/UI/Simulation/SimulationOutputPlot.cpp index 16e5b09fc..846c51602 100644 --- a/src/OpenSimCreator/UI/Simulation/SimulationOutputPlot.cpp +++ b/src/OpenSimCreator/UI/Simulation/SimulationOutputPlot.cpp @@ -52,7 +52,7 @@ namespace const OutputExtractor& output) { if (env.hasUserOutputExtractor(output)) { - if (ui::draw_menu_item(OSC_ICON_TRASH " Stop Watching")) { + if (ui::draw_menu_item(OSC_ICON_TIMES " Stop Watching")) { env.removeUserOutputExtractor(output); } } diff --git a/src/OpenSimCreator/UI/SplashTab.cpp b/src/OpenSimCreator/UI/SplashTab.cpp index 323588ea5..718b5947d 100644 --- a/src/OpenSimCreator/UI/SplashTab.cpp +++ b/src/OpenSimCreator/UI/SplashTab.cpp @@ -287,7 +287,7 @@ class osc::SplashTab::Impl final : public TabPrivate { } App::upd().add_frame_annotation("SplashTab/ModelWarpingMenuItem", ui::get_last_drawn_item_screen_rect()); - if (ui::draw_menu_item(OSC_ICON_ARROWS_ALT " Frame Definition (" OSC_ICON_TRASH " deprecated)")) { + if (ui::draw_menu_item(OSC_ICON_ARROWS_ALT " Frame Definition (" OSC_ICON_TIMES " deprecated)")) { auto tab = std::make_unique(*parent()); App::post_event(*parent(), std::move(tab)); } diff --git a/src/oscar/UI/oscimgui.cpp b/src/oscar/UI/oscimgui.cpp index e4fd4be4e..f335c6054 100644 --- a/src/oscar/UI/oscimgui.cpp +++ b/src/oscar/UI/oscimgui.cpp @@ -2018,7 +2018,12 @@ void osc::ui::draw_help_marker(CStringView content) bool osc::ui::draw_string_input(CStringView label, std::string& edited_string, TextInputFlags flags) { - return ImGui::InputText(label.c_str(), &edited_string, to(flags)); // uses `imgui_stdlib` + return ImGui::InputText(label.c_str(), &edited_string, to(flags)); +} + +bool osc::ui::draw_string_input_with_hint(CStringView label, CStringView hint, std::string& edited_string, TextInputFlags flags) +{ + return ImGui::InputTextWithHint(label.c_str(), hint.c_str(), & edited_string, to(flags)); } bool osc::ui::draw_float_meters_input(CStringView label, float& v, float step, float step_fast, TextInputFlags flags) diff --git a/src/oscar/UI/oscimgui.h b/src/oscar/UI/oscimgui.h index 83baed2d9..fa4645113 100644 --- a/src/oscar/UI/oscimgui.h +++ b/src/oscar/UI/oscimgui.h @@ -724,6 +724,14 @@ namespace osc::ui TextInputFlags = {} ); + // draws a text input that contains `hint` as a placeholder and manipulates a `std::string` + bool draw_string_input_with_hint( + CStringView label, + CStringView hint, + std::string& edited_string, + TextInputFlags = {} + ); + // behaves like `ui::draw_float_input`, but understood to manipulate the scene scale bool draw_float_meters_input( CStringView label, diff --git a/src/oscar/Utils/StringHelpers.cpp b/src/oscar/Utils/StringHelpers.cpp index 06cbb76c2..0bdfbfa4d 100644 --- a/src/oscar/Utils/StringHelpers.cpp +++ b/src/oscar/Utils/StringHelpers.cpp @@ -244,3 +244,18 @@ std::optional osc::try_parse_hex_chars_as_byte(char a, char b) return std::nullopt; } } + +std::string osc::replace(std::string_view str, std::string_view from, std::string_view to) +{ + if (const auto pos = str.find(from); pos != std::string_view::npos) { + std::string rv; + rv.reserve(str.size() + to.size() - from.size()); + rv.insert(0, str.substr(0, pos)); + rv.insert(rv.size(), to); + rv.insert(rv.size(), str.substr(pos + from.size())); + return rv; + } + else { + return std::string{str}; + } +} diff --git a/src/oscar/Utils/StringHelpers.h b/src/oscar/Utils/StringHelpers.h index a7f005b47..08d3a4dc4 100644 --- a/src/oscar/Utils/StringHelpers.h +++ b/src/oscar/Utils/StringHelpers.h @@ -87,6 +87,8 @@ namespace osc return std::move(ss).str(); } + // returns a string that contains a string-ified version of each element in `r` joined + // with a given `delimiter`. template requires OutputStreamable> std::string join(R&& r, std::string_view delimiter) @@ -100,4 +102,8 @@ namespace osc } return std::move(ss).str(); } + + // Returns a copy of `str`'s content, but with the first instance of `from` replaced + // with `to` (if any). + std::string replace(std::string_view str, std::string_view from, std::string_view to); } diff --git a/tests/testoscar/Utils/TestStringHelpers.cpp b/tests/testoscar/Utils/TestStringHelpers.cpp index 5f98c66b3..c619949ec 100644 --- a/tests/testoscar/Utils/TestStringHelpers.cpp +++ b/tests/testoscar/Utils/TestStringHelpers.cpp @@ -11,11 +11,7 @@ #include #include -using osc::try_parse_hex_chars_as_byte; -using osc::strip_whitespace; -using osc::to_hex_chars; -using osc::is_valid_identifier; -using osc::join; +using namespace osc; TEST(strip_whitespace, works_as_expected) { @@ -347,3 +343,23 @@ TEST(join, works_with_three_elements) { ASSERT_EQ(join(std::to_array({5, 4, 3}), ", "), "5, 4, 3"); } + +TEST(replace, works_as_intended) +{ + struct TestCase final { + std::string_view str; + std::string_view from; + std::string_view to; + std::string_view expected_output; + }; + constexpr auto test_cases = std::to_array({ + // input // search string // replacement // expected result + {"hello, ${var}!" , "${var}", "world", "hello, world!" }, + {"${var}, ${var}!", "${var}", "hello", "hello, ${var}!"}, // i.e. it replaces the first instance, it isn't `replace_all` + {"hello, world!" , "unused", "nope" , "hello, world!" }, + }); + for (const auto& test_case : test_cases) { + const std::string output = replace(test_case.str, test_case.from, test_case.to); + ASSERT_EQ(output, test_case.expected_output); + } +} diff --git a/third_party/osim b/third_party/osim index 77b3e9026..625347c1d 160000 --- a/third_party/osim +++ b/third_party/osim @@ -1 +1 @@ -Subproject commit 77b3e90266877752885317583b7f342bba232746 +Subproject commit 625347c1d3f7522347bb6833ad7efb04f2ff61ad