diff --git a/agave_app/AppearanceDockWidget2.cpp b/agave_app/AppearanceDockWidget2.cpp index 7523a385..1c25f68c 100644 --- a/agave_app/AppearanceDockWidget2.cpp +++ b/agave_app/AppearanceDockWidget2.cpp @@ -5,7 +5,7 @@ QAppearanceDockWidget2::QAppearanceDockWidget2(QWidget* pParent, ViewerWindow* vw, AppearanceObject* ado) : QDockWidget(pParent) - , m_AppearanceWidget(nullptr, rs, ado) + , m_AppearanceWidget(nullptr, rs, vw, ado) { setWindowTitle("Appearance"); diff --git a/agave_app/AppearanceWidget.cpp b/agave_app/AppearanceWidget.cpp index 87b825a9..1babe2fa 100644 --- a/agave_app/AppearanceWidget.cpp +++ b/agave_app/AppearanceWidget.cpp @@ -32,33 +32,33 @@ QAppearanceWidget2::QAppearanceWidget2(QWidget* pParent, RenderSettings* rs, Vie // } // } - QComboBox* rendererType = addRow(*m_appearanceDataObject->getRendererTypeUiInfo()); - m_MainLayout.addRow("Renderer", rendererType); - QComboBox* shadingType = addRow(*m_appearanceDataObject->getShadingTypeUiInfo()); - m_MainLayout.addRow("Shading Type", shadingType); - QNumericSlider* densityScale = addRow(*m_appearanceDataObject->getDensityScaleUiInfo()); - m_MainLayout.addRow("Scattering Density", densityScale); - QNumericSlider* gradientFactor = addRow(*m_appearanceDataObject->getGradientFactorUiInfo()); - m_MainLayout.addRow("Shading Type Mixture", gradientFactor); - QNumericSlider* stepSizePrimaryRay = addRow(*m_appearanceDataObject->getStepSizePrimaryRayUiInfo()); - m_MainLayout.addRow("Step Size Primary Ray", stepSizePrimaryRay); - QNumericSlider* stepSizeSecondaryRay = addRow(*m_appearanceDataObject->getStepSizeSecondaryRayUiInfo()); - m_MainLayout.addRow("Step Size Secondary Ray", stepSizeSecondaryRay); - QCheckBox* interpolateCheckBox = addRow(*m_appearanceDataObject->getInterpolateUiInfo()); - m_MainLayout.addRow("Interpolate", interpolateCheckBox); - QColorPushButton* backgroundColorButton = addRow(*m_appearanceDataObject->getBackgroundColorUiInfo()); - m_MainLayout.addRow("Background Color", backgroundColorButton); - QCheckBox* showBoundingBoxCheckBox = addRow(*m_appearanceDataObject->getShowBoundingBoxUiInfo()); - m_MainLayout.addRow("Show Bounding Box", showBoundingBoxCheckBox); - QColorPushButton* boundingBoxColorButton = addRow(*m_appearanceDataObject->getBoundingBoxColorUiInfo()); - m_MainLayout.addRow("Bounding Box Color", boundingBoxColorButton); - QCheckBox* showScaleBarCheckBox = addRow(*m_appearanceDataObject->getShowScaleBarUiInfo()); - m_MainLayout.addRow("Show Scale Bar", showScaleBarCheckBox); + // QComboBox* rendererType = addRow(*m_appearanceDataObject->getRendererTypeUiInfo()); + // m_MainLayout.addRow("Renderer", rendererType); + // QComboBox* shadingType = addRow(*m_appearanceDataObject->getShadingTypeUiInfo()); + // m_MainLayout.addRow("Shading Type", shadingType); + // QNumericSlider* densityScale = addRow(*m_appearanceDataObject->getDensityScaleUiInfo()); + // m_MainLayout.addRow("Scattering Density", densityScale); + // QNumericSlider* gradientFactor = addRow(*m_appearanceDataObject->getGradientFactorUiInfo()); + // m_MainLayout.addRow("Shading Type Mixture", gradientFactor); + // QNumericSlider* stepSizePrimaryRay = addRow(*m_appearanceDataObject->getStepSizePrimaryRayUiInfo()); + // m_MainLayout.addRow("Step Size Primary Ray", stepSizePrimaryRay); + // QNumericSlider* stepSizeSecondaryRay = addRow(*m_appearanceDataObject->getStepSizeSecondaryRayUiInfo()); + // m_MainLayout.addRow("Step Size Secondary Ray", stepSizeSecondaryRay); + // QCheckBox* interpolateCheckBox = addRow(*m_appearanceDataObject->getInterpolateUiInfo()); + // m_MainLayout.addRow("Interpolate", interpolateCheckBox); + // QColorPushButton* backgroundColorButton = addRow(*m_appearanceDataObject->getBackgroundColorUiInfo()); + // m_MainLayout.addRow("Background Color", backgroundColorButton); + // QCheckBox* showBoundingBoxCheckBox = addRow(*m_appearanceDataObject->getShowBoundingBoxUiInfo()); + // m_MainLayout.addRow("Show Bounding Box", showBoundingBoxCheckBox); + // QColorPushButton* boundingBoxColorButton = addRow(*m_appearanceDataObject->getBoundingBoxColorUiInfo()); + // m_MainLayout.addRow("Bounding Box Color", boundingBoxColorButton); + // QCheckBox* showScaleBarCheckBox = addRow(*m_appearanceDataObject->getShowScaleBarUiInfo()); + // m_MainLayout.addRow("Show Scale Bar", showScaleBarCheckBox); - QObject::connect(rendererType, &QComboBox::currentIndexChanged, [this, vw](int index) { vw->setRenderer(index); }); - QObject::connect(shadingType, &QComboBox::currentIndexChanged, [this, gradientFactor](int index) { - gradientFactor->setEnabled(index == 2); - }); + // QObject::connect(rendererType, &QComboBox::currentIndexChanged, [this, vw](int index) { vw->setRenderer(index); }); + // QObject::connect(shadingType, &QComboBox::currentIndexChanged, [this, gradientFactor](int index) { + // gradientFactor->setEnabled(index == 2); + // }); } QSize diff --git a/agave_app/AppearanceWidget.h b/agave_app/AppearanceWidget.h index 6e943119..a0fa71ac 100644 --- a/agave_app/AppearanceWidget.h +++ b/agave_app/AppearanceWidget.h @@ -32,5 +32,5 @@ class QAppearanceWidget2 : public QWidget RenderSettings* m_renderSettings; private: - AppearanceObject* m_appearanceDataObject; + AppearanceObject* m_appearanceObject; }; diff --git a/agave_app/CMakeLists.txt b/agave_app/CMakeLists.txt index 5ca90c0e..e05e471a 100644 --- a/agave_app/CMakeLists.txt +++ b/agave_app/CMakeLists.txt @@ -37,10 +37,6 @@ target_sources(agaveapp PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/cgiparser.h" "${CMAKE_CURRENT_SOURCE_DIR}/commandBuffer.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/commandBuffer.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Film.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Film.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Focus.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Focus.h" "${CMAKE_CURRENT_SOURCE_DIR}/GLView3D.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/GLView3D.h" "${CMAKE_CURRENT_SOURCE_DIR}/loadDialog.cpp" diff --git a/agave_app/CameraDockWidget.cpp b/agave_app/CameraDockWidget.cpp index 28a7eb77..78ec36a6 100644 --- a/agave_app/CameraDockWidget.cpp +++ b/agave_app/CameraDockWidget.cpp @@ -2,7 +2,7 @@ QCameraDockWidget::QCameraDockWidget(QWidget* pParent, RenderSettings* rs, CameraObject* cdo) : QDockWidget(pParent) - , m_CameraWidget(nullptr, cam, rs, cdo) + , m_CameraWidget(nullptr, rs, cdo) { setWindowTitle("Camera"); diff --git a/agave_app/CameraWidget.h b/agave_app/CameraWidget.h index 35ed6fdb..2bb56e31 100644 --- a/agave_app/CameraWidget.h +++ b/agave_app/CameraWidget.h @@ -1,6 +1,5 @@ #pragma once -#include "Camera.h" #include "qtControls/Controls.h" // #include "renderlib/core/prty/prtyProperty.h" diff --git a/agave_app/GLView3D.cpp b/agave_app/GLView3D.cpp index 8cf085ba..c142f375 100644 --- a/agave_app/GLView3D.cpp +++ b/agave_app/GLView3D.cpp @@ -140,8 +140,6 @@ GLView3D::onNewImage(Scene* scene) GLView3D::~GLView3D() { - delete m_appearanceDataObject; - makeCurrent(); check_gl("view dtor makecurrent"); // doneCurrent(); diff --git a/agave_app/GLView3D.h b/agave_app/GLView3D.h index c8018c33..f53da57a 100644 --- a/agave_app/GLView3D.h +++ b/agave_app/GLView3D.h @@ -12,8 +12,8 @@ #include #include -class AppearanceDataObject; -class CameraDataObject; +class AppearanceObject; +class CameraObject; class CStatus; class ImageXYZC; class IRenderWindow; @@ -70,7 +70,6 @@ class GLView3D : public QOpenGLWidget // tied to the above camera. CCamera must outlive this: // CameraObject* getCameraDataObject() { return m_cameraObject; } void setCameraObject(CameraObject* cameraObject); - AppearanceObject* getAppearanceDataObject() { return m_appearanceDataObject; } void fromViewerState(const Serialize::ViewerState& s); diff --git a/agave_app/agaveGui.cpp b/agave_app/agaveGui.cpp index 3ee18c4e..5ceef7e9 100644 --- a/agave_app/agaveGui.cpp +++ b/agave_app/agaveGui.cpp @@ -6,6 +6,8 @@ #include "agaveGui.h" #include "renderlib/AppScene.h" +#include "renderlib/AreaLightObject.hpp" +#include "renderlib/SkyLightObject.hpp" #include "renderlib/ImageXYZC.h" #include "renderlib/Logging.h" #include "renderlib/Status.h" @@ -13,13 +15,14 @@ #include "renderlib/io/FileReader.h" #include "renderlib/version.hpp" #include "renderlib/CameraObject.hpp" +#include "renderlib/ClipPlaneObject.hpp" #include "renderlib/AppearanceObject.hpp" +#include "renderlib/serialize/docWriterJson.h" +#include "renderlib/serialize/docReaderJson.h" #include "AppearanceDockWidget.h" #include "AppearanceDockWidget2.h" -#include "ArealightDockWidget.h" #include "CameraDockWidget.h" -#include "SkylightDockWidget.h" #include "Serialize.h" #include "StatisticsDockWidget.h" #include "TimelineDockWidget.h" @@ -87,10 +90,8 @@ agaveGui::agaveGui(QWidget* parent) : QMainWindow(parent) { // create our document objects - // render settings - m_appearanceObject = std::make_unique(); - // scene objects m_cameraObject = std::make_unique(); + m_appearanceObject = std::make_unique(); m_areaLightObject = std::make_unique(); m_areaLightObject->getSceneLight()->m_light = Scene::defaultAreaLight(); m_areaLightObject->setDirtyCallback( @@ -99,6 +100,9 @@ agaveGui::agaveGui(QWidget* parent) m_skyLightObject->getSceneLight()->m_light = Scene::defaultSkyLight(); m_skyLightObject->setDirtyCallback( [this]() { m_appearanceObject->getRenderSettings()->m_DirtyFlags.SetFlag(LightsDirty); }); + m_clipPlaneObject = std::make_unique(); + m_clipPlaneObject->setDirtyCallback( + [this]() { m_appearanceObject->getRenderSettings()->m_DirtyFlags.SetFlag(RoiDirty); }); m_ui.setupUi(this); @@ -140,11 +144,9 @@ agaveGui::agaveGui(QWidget* parent) // create camera ui window now that there is an actual camera. setupCameraDock(m_cameraObject.get()); - // setupAreaLightDock(m_areaLightObject.get()); - // setupSkyLightDock(m_skyLightObject.get()); setupTimelineDock(); setupStatisticsDock(); - setupAppearanceDock(m_glView->getAppearanceDataObject()); + setupAppearanceDock(m_appearanceObject.get()); addDockItemsToViewMenu(); @@ -376,17 +378,17 @@ void agaveGui::setupAreaLightDock(AreaLightObject* cdo) { // TODO enable changing/resetting the area light data object shown in this dock? - m_areaLightDock = new QAreaLightDockWidget(this, m_appearanceObject->getRenderSettings().get(), cdo); - m_areaLightDock->setAllowedAreas(Qt::AllDockWidgetAreas); - addDockWidget(Qt::RightDockWidgetArea, m_areaLightDock); + // m_areaLightDock = new QAreaLightDockWidget(this, m_appearanceObject->getRenderSettings().get(), cdo); + // m_areaLightDock->setAllowedAreas(Qt::AllDockWidgetAreas); + // addDockWidget(Qt::RightDockWidgetArea, m_areaLightDock); } void agaveGui::setupSkyLightDock(SkyLightObject* cdo) { // TODO enable changing/resetting the skylight data object shown in this dock? - m_skylightDock = new QSkyLightDockWidget(this, m_appearanceObject->getRenderSettings().get(), cdo); - m_skylightDock->setAllowedAreas(Qt::AllDockWidgetAreas); - addDockWidget(Qt::RightDockWidgetArea, m_skylightDock); + // m_skylightDock = new QSkyLightDockWidget(this, m_appearanceObject->getRenderSettings().get(), cdo); + // m_skylightDock->setAllowedAreas(Qt::AllDockWidgetAreas); + // addDockWidget(Qt::RightDockWidgetArea, m_skylightDock); } void @@ -421,7 +423,7 @@ agaveGui::setupAppearanceDock(AppearanceObject* ado) } void -agaveGui::createDockWindows() +agaveGui::setupTimelineDock() { m_timelinedock = new QTimelineDockWidget(this, &m_qrendersettings); @@ -440,6 +442,11 @@ agaveGui::setupStatisticsDock() addDockWidget(Qt::RightDockWidgetArea, m_statisticsDockWidget); } +void +agaveGui::addDockItemsToViewMenu() +{ + m_viewMenu->addSeparator(); + m_viewMenu->addAction(m_cameradock->toggleViewAction()); m_viewMenu->addSeparator(); m_viewMenu->addAction(m_timelinedock->toggleViewAction()); m_viewMenu->addSeparator(); @@ -717,6 +724,352 @@ agaveGui::onRenderAction() rdialog->onZoomFitClicked(); } +void +agaveGui::writeDocument(std::string filepath) +{ + docWriter* writer = new docWriterJson(); + + writer->beginDocument(filepath); + + writer->beginObject("_AGAVE", "AgaveDocument", 1); + // write agave version at least + writer->writeProperty("Version", std::string(AICS_VERSION_STRING)); + writer->endObject(); + + writer->beginList("_camera"); + // list of all cameras + m_cameraObject->toDocument(writer); + writer->endList(); + + writer->beginList("_light"); + // list of all lights + m_skyLightObject->toDocument(writer); + m_areaLightObject->toDocument(writer); + writer->endList(); + + writer->beginList("_clipPlane"); + // list of all clip planes + m_clipPlaneObject->toDocument(writer); + writer->endList(); + + writer->beginList("_renderSettings"); + // list of all render settings objects + m_appearanceObject->toDocument(writer); + writer->endList(); + + writer->beginList("_captureSettings"); + // list of capture settings objects (only one?) + writer->endList(); + writer->beginObject("_geometry", "GeometryObject", 1); + writer->beginList("_volume"); + // list of all volumes + writer->endList(); + + writer->beginList("_mesh"); + // list of all meshes + writer->endList(); + writer->endObject(); + + writer->beginObject("_scene", "SceneObject", 1); + // one and only active scene. + // this includes references to selections of the above objects. + // other objects should not be cross referencing each other. + // so scene needs to be set up after other objects are defined. + + writer->endObject(); + + writer->endDocument(); + delete writer; +} + +void +agaveGui::readDocument(std::string filepath) +{ + docReader* reader = new docReaderJson(); + if (reader->beginDocument(filepath)) { + if (reader->beginObject("_AGAVE")) { + // read agave version at least + reader->endObject(); + } + + if (reader->beginList("_camera")) { + // list of all cameras + // v1, read only the first camera + // Iterate through objects in the list + while (reader->beginObject("")) { + + CameraObject* camObj = new CameraObject(); + camObj->fromDocument(reader); + reader->endObject(); + + camObj->updateObjectFromProps(); + + // install camObj into m_cameraObject??? + m_cameraObject = std::unique_ptr(camObj); + // follow through to other parts of the app that need to know about camera change + m_glView->setCameraObject(m_cameraObject.get()); + // m_cameradock->setCameraObject(m_cameraObject); + + break; // only read first camera for now + } + + reader->endList(); + } + if (reader->beginList("_light")) { + // list of all lights + while (reader->beginObject("")) { + std::string objType = reader->peekObjectType(); + if (objType == "SkyLightObject") { + SkyLightObject* skyLightObj = new SkyLightObject(); + // skyLightObj->fromDocument(reader); + reader->endObject(); + // install skyLightObj into m_skyLightObject??? + m_skyLightObject = std::unique_ptr(skyLightObj); + // follow through to other parts of the app that need to know about skylight change + m_appScene.initLights(m_skyLightObject->getSceneLight(), m_areaLightObject->getSceneLight()); + // m_skylightDock->setSkyLightObject(m_skyLightObject); + } else if (objType == "AreaLightObject") { + AreaLightObject* areaLightObj = new AreaLightObject(); + // areaLightObj->fromDocument(reader); + reader->endObject(); + // install areaLightObj into m_areaLightObject??? + m_areaLightObject = std::unique_ptr(areaLightObj); + // follow through to other parts of the app that need to know about area light change + m_appScene.initLights(m_skyLightObject->getSceneLight(), m_areaLightObject->getSceneLight()); + // m_areaLightDock->setAreaLightObject(m_areaLightObject); + } else { + LOG_WARNING << "Unknown light object type: " << objType; + reader->endObject(); + } + } + reader->endList(); + } + if (reader->beginList("_clipPlane")) { + // list of all clip planes + while (reader->beginObject("")) { + ClipPlaneObject* clipPlaneObj = new ClipPlaneObject(); + clipPlaneObj->fromDocument(reader); + reader->endObject(); + + clipPlaneObj->updateObjectFromProps(); + + // std::string objType = reader->peekObjectType(); + // if (objType == "ClipPlaneObject") { + // // TODO implement clip plane loading + // reader->endObject(); + // } else { + // LOG_WARNING << "Unknown clip plane object type: " << objType; + // reader->endObject(); + // } + } + reader->endList(); + } + if (reader->beginList("_renderSettings")) { + // list of all render settings objects + while (reader->beginObject("")) { + std::string objType = reader->peekObjectType(); + if (objType == "RenderSettingsObject") { + AppearanceObject* appObj = new AppearanceObject(); + appObj->fromDocument(reader); + reader->endObject(); + // install appObj into m_appearanceObject??? + m_appearanceObject = std::unique_ptr(appObj); + // follow through to other parts of the app that need to know about appearance change + m_appearanceObject->updateObjectFromProps(); + // m_appearanceDockWidget->setAppearanceObject(m_appearanceObject); + } else { + LOG_WARNING << "Unknown render settings object type: " << objType; + reader->endObject(); + } + } + reader->endList(); + } + if (reader->beginList("_captureSettings")) { + // list of capture settings objects (only one?) + while (reader->beginObject("")) { + std::string objType = reader->peekObjectType(); + if (objType == "CaptureSettingsObject") { + // TODO implement capture settings loading + reader->endObject(); + } else { + LOG_WARNING << "Unknown capture settings object type: " << objType; + reader->endObject(); + } + } + reader->endList(); + } + + if (reader->beginObject("_geometry")) { + if (reader->beginList("_volume")) { + // list of all volumes + while (reader->beginObject("")) { + std::string objType = reader->peekObjectType(); + if (objType == "VolumeObject") { + // TODO implement volume loading + reader->endObject(); + } else { + LOG_WARNING << "Unknown volume object type: " << objType; + reader->endObject(); + } + } + reader->endList(); + } + if (reader->beginList("_mesh")) { + // list of all meshes + reader->endList(); + } + reader->endObject(); + } + + if (reader->beginObject("_scene")) { + // one and only active scene. + reader->endObject(); + } + + reader->endDocument(); + +////////////////////////////////// +#if 0 + docReader* reader = new docReaderJson(); + if (!reader->beginDocument(file.toStdString())) { + qWarning("Failed to parse JSON document."); + delete reader; + return; + } + + // Read AGAVE metadata + if (reader->beginObject("_AGAVE")) { + if (reader->hasKey("_version")) { + std::string agaveVersion = reader->readString("_version"); + LOG_INFO << "Loading AGAVE file version: " << agaveVersion; + } + reader->endObject(); + } + + // Read cameras + if (reader->beginList("_camera")) { + if (reader->beginObject("_camera0")) { + // Check version + if (reader->hasKey("_version")) { + uint32_t version = reader->readUint32("_version"); + LOG_INFO << "Camera object version: " << version; + if (version != 1) { + LOG_WARNING << "Unknown camera version " << version << ", attempting to read anyway"; + } + } + // Read all camera properties + reader->readProperties(m_cameraObject.get()); + // Update the actual camera from properties + m_cameraObject->updateObjectFromProps(); + reader->endObject(); + } + reader->endList(); + } + + // Read lights + if (reader->beginList("_light")) { + // Sky light + if (reader->beginObject("_skylight0")) { + if (reader->hasKey("_version")) { + uint32_t version = reader->readUint32("_version"); + LOG_INFO << "SkyLight object version: " << version; + if (version != 1) { + LOG_WARNING << "Unknown skylight version " << version << ", attempting to read anyway"; + } + } + reader->readProperties(m_skyLightObject.get()); + // m_skyLightObject->updateObjectFromProps(); + reader->endObject(); + } + + // Area light + if (reader->beginObject("_arealight0")) { + if (reader->hasKey("_version")) { + uint32_t version = reader->readUint32("_version"); + LOG_INFO << "AreaLight object version: " << version; + if (version != 1) { + LOG_WARNING << "Unknown arealight version " << version << ", attempting to read anyway"; + } + } + reader->readProperties(m_areaLightObject.get()); + // m_areaLightObject->updateObjectFromProps(); + reader->endObject(); + } + reader->endList(); + } + + // Read clip planes + if (reader->beginList("_clipPlane")) { + if (reader->beginObject("_clipPlane0")) { + if (reader->hasKey("_version")) { + uint32_t version = reader->readUint32("_version"); + LOG_INFO << "ClipPlane object version: " << version; + } + // TODO: read clip plane properties when implemented + reader->endObject(); + } + reader->endList(); + } + + // Read render settings + if (reader->beginList("_renderSettings")) { + if (reader->beginObject("_renderSettings0")) { + if (reader->hasKey("_version")) { + uint32_t version = reader->readUint32("_version"); + LOG_INFO << "RenderSettings object version: " << version; + if (version != 1) { + LOG_WARNING << "Unknown render settings version " << version << ", attempting to read anyway"; + } + } + reader->readProperties(m_appearanceObject.get()); + m_appearanceObject->updateObjectFromProps(); + reader->endObject(); + } + reader->endList(); + } + + // Read capture settings + if (reader->beginList("_captureSettings")) { + // TODO: implement capture settings + reader->endList(); + } + + // Read geometry + if (reader->beginObject("_geometry")) { + if (reader->hasKey("_version")) { + uint32_t version = reader->readUint32("_version"); + LOG_INFO << "Geometry object version: " << version; + } + + if (reader->beginList("_volume")) { + // TODO: implement volume loading + reader->endList(); + } + + if (reader->beginList("_mesh")) { + // TODO: implement mesh loading + reader->endList(); + } + + reader->endObject(); + } + + // Read scene + if (reader->beginObject("_scene")) { + if (reader->hasKey("_version")) { + uint32_t version = reader->readUint32("_version"); + LOG_INFO << "Scene object version: " << version; + } + // TODO: implement scene state loading + reader->endObject(); + } + + reader->endDocument(); +#endif + delete reader; + } +} + void agaveGui::saveJson() { @@ -739,6 +1092,26 @@ agaveGui::saveJson() } } +void +agaveGui::loadJson() +{ + QFileDialog::Options options; +#ifdef __linux__ + options |= QFileDialog::DontUseNativeDialog; +#endif + QString file = QFileDialog::getOpenFileName(this, tr("Load JSON"), QString(), tr("JSON (*.json)"), nullptr, options); + if (!file.isEmpty()) { + + QFile loadFile(file); + if (!loadFile.open(QIODevice::ReadOnly)) { + qWarning("Couldn't open load file."); + return; + } + + LOG_INFO << "Successfully loaded JSON file: " << file.toStdString(); + } +} + void agaveGui::onImageLoaded(std::shared_ptr image, const LoadSpec& loadSpec, diff --git a/agave_app/agaveGui.h b/agave_app/agaveGui.h index 10e4b67f..b29e8d83 100644 --- a/agave_app/agaveGui.h +++ b/agave_app/agaveGui.h @@ -23,6 +23,7 @@ class QTimelineDockWidget; class AreaLightObject; class CameraObject; +class ClipPlaneObject; class SkyLightObject; class IFileReader; class ViewToolbar; @@ -78,6 +79,7 @@ private slots: void openMesh(const QString& file); void saveImage(); void saveJson(); + void loadJson(); void savePython(); void onRenderAction(); void OnUpdateRenderer(); @@ -119,6 +121,9 @@ private slots: void writeRecentDirectory(const QString& directory); QString readRecentDirectory(); + void readDocument(std::string filepath); + void writeDocument(std::string filepath); + QMenu* m_fileMenu; QMenu* m_viewMenu; QMenu* m_helpMenu; @@ -198,6 +203,7 @@ private slots: std::unique_ptr m_cameraObject; std::unique_ptr m_areaLightObject; std::unique_ptr m_skyLightObject; + std::unique_ptr m_clipPlaneObject; std::string m_currentFilePath; // TODO remove the above m_currentFilePath and use this instead diff --git a/agave_app/qtControls/CMakeLists.txt b/agave_app/qtControls/CMakeLists.txt index 506089fd..8e8f55a0 100644 --- a/agave_app/qtControls/CMakeLists.txt +++ b/agave_app/qtControls/CMakeLists.txt @@ -3,6 +3,8 @@ target_include_directories(agaveapp PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}" ) target_sources(agaveapp PRIVATE +"${CMAKE_CURRENT_SOURCE_DIR}/controlFactory.cpp" +"${CMAKE_CURRENT_SOURCE_DIR}/controlFactory.h" "${CMAKE_CURRENT_SOURCE_DIR}/Controls.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Controls.h" "${CMAKE_CURRENT_SOURCE_DIR}/RangeWidget.cpp" diff --git a/agave_app/qtControls/controlFactory.cpp b/agave_app/qtControls/controlFactory.cpp index 7e4821e1..1c042c74 100644 --- a/agave_app/qtControls/controlFactory.cpp +++ b/agave_app/qtControls/controlFactory.cpp @@ -7,26 +7,6 @@ #include -QNumericSlider* -addRow(const FloatSliderSpinnerUiInfo& info) -{ - QNumericSlider* slider = new QNumericSlider(); - slider->setStatusTip(QString::fromStdString(info->GetStatusTip())); - slider->setToolTip(QString::fromStdString(info->GetToolTip())); - slider->setRange(info->min, info->max); - slider->setDecimals(info->decimals); - slider->setSingleStep(info->singleStep); - slider->setNumTickMarks(info->numTickMarks); - slider->setSuffix(QString::fromStdString(info->suffix)); - - slider->setValue(prop->GetValue(), true); - QObject::connect( - slider, &QNumericSlider::valueChanged, [slider, prop](double value) { prop->SetValue(value, true); }); - // TODO how would this capture the "previous" value, for undo? - // QObject::connect(slider, &QNumericSlider::valueChangeCommit, [slider, prop]() { prop->NotifyAll(true); }); - - return slider; -} QNumericSlider* create(const IntSliderSpinnerUiInfo* info, std::shared_ptr prop) { @@ -46,33 +26,33 @@ create(const IntSliderSpinnerUiInfo* info, std::shared_ptr prop) return slider; } -QCheckBox* -create(const CheckBoxUiInfo* info, std::shared_ptr prop) -{ - QCheckBox* checkBox = new QCheckBox(); - checkBox->setStatusTip(QString::fromStdString(info->GetStatusTip())); - checkBox->setToolTip(QString::fromStdString(info->GetToolTip())); - // checkBox->setText(QString::fromStdString(info->formLabel)); - checkBox->setCheckState(prop->GetValue() ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); - QObject::connect(checkBox, &QCheckBox::stateChanged, [checkBox, prop](int state) { - prop->SetValue(state == Qt::CheckState::Checked, true); - }); - return checkBox; -} -QComboBox* -create(const ComboBoxUiInfo* info, std::shared_ptr prop) -{ - QComboBox* comboBox = new QComboBox(); - comboBox->setStatusTip(QString::fromStdString(info->GetStatusTip())); - comboBox->setToolTip(QString::fromStdString(info->GetToolTip())); - for (const auto& item : info->items) { - comboBox->addItem(QString::fromStdString(item)); - } - comboBox->setCurrentIndex(prop->GetValue()); - QObject::connect( - comboBox, &QComboBox::currentIndexChanged, [comboBox, prop](int index) { prop->SetValue(index, true); }); - return comboBox; -} +// QCheckBox* +// create(const CheckBoxUiInfo* info, std::shared_ptr prop) +// { +// QCheckBox* checkBox = new QCheckBox(); +// checkBox->setStatusTip(QString::fromStdString(info->GetStatusTip())); +// checkBox->setToolTip(QString::fromStdString(info->GetToolTip())); +// // checkBox->setText(QString::fromStdString(info->formLabel)); +// checkBox->setCheckState(prop->GetValue() ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); +// QObject::connect(checkBox, &QCheckBox::stateChanged, [checkBox, prop](int state) { +// prop->SetValue(state == Qt::CheckState::Checked, true); +// }); +// return checkBox; +// } +// QComboBox* +// create(const ComboBoxUiInfo* info, std::shared_ptr prop) +// { +// QComboBox* comboBox = new QComboBox(); +// comboBox->setStatusTip(QString::fromStdString(info->GetStatusTip())); +// comboBox->setToolTip(QString::fromStdString(info->GetToolTip())); +// for (const auto& item : info->items) { +// comboBox->addItem(QString::fromStdString(item)); +// } +// comboBox->setCurrentIndex(prop->GetValue()); +// QObject::connect( +// comboBox, &QComboBox::currentIndexChanged, [comboBox, prop](int index) { prop->SetValue(index, true); }); +// return comboBox; +// } QNumericSlider* addRow(const FloatSliderSpinnerUiInfo& info) @@ -238,8 +218,9 @@ addRow(const ComboBoxUiInfo& info) QComboBox* comboBox = new QComboBox(); comboBox->setStatusTip(QString::fromStdString(info.GetStatusTip())); comboBox->setToolTip(QString::fromStdString(info.GetToolTip())); - for (const auto& item : info.items) { - comboBox->addItem(QString::fromStdString(item)); + auto* prop = static_cast(info.GetProperty(0)); + for (int i = 0; i < prop->GetNumTags(); ++i) { + comboBox->addItem(QString::fromStdString(prop->GetEnumTag(i))); } comboBox->setCurrentIndex(prop->GetValue()); auto conn = QObject::connect( @@ -378,6 +359,8 @@ createFlatList(LayoutType* mainLayout, prtyObject* object) } } + +// Explicit template instantiations for createFlatList template void createFlatList(QFormLayout* mainLayout, prtyObject* object); template void diff --git a/agave_app/qtControls/controlFactory.h b/agave_app/qtControls/controlFactory.h index 4947b258..19a713e4 100644 --- a/agave_app/qtControls/controlFactory.h +++ b/agave_app/qtControls/controlFactory.h @@ -3,9 +3,9 @@ #include "renderlib/uiInfo.hpp" #include "renderlib/core/prty/prtyColor.hpp" #include "renderlib/core/prty/prtyBoolean.hpp" +#include "renderlib/core/prty/prtyEnum.hpp" #include "renderlib/core/prty/prtyFloat.hpp" -#include "renderlib/core/prty/prtyInt32.hpp" -#include "renderlib/core/prty/prtyInt8.hpp" +#include "renderlib/core/prty/prtyIntegerTemplate.hpp" #include "renderlib/core/prty/prtyObject.hpp" #include "renderlib/glm.h" @@ -24,9 +24,6 @@ addRow(const FloatSliderSpinnerUiInfo& info); QNumericSlider* addRow(const IntSliderSpinnerUiInfo& info); -QComboBox* -create(const ComboBoxUiInfo* info, std::shared_ptr prop); - QNumericSlider* addRow(const FloatSliderSpinnerUiInfo& info); diff --git a/agave_app/tfeditor/gradients.cpp b/agave_app/tfeditor/gradients.cpp index dd21eff9..a48f2d0c 100644 --- a/agave_app/tfeditor/gradients.cpp +++ b/agave_app/tfeditor/gradients.cpp @@ -1,699 +1,1090 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the demonstration applications of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:BSD$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** BSD License Usage -** Alternatively, you may use this file under the terms of the BSD license -** as follows: -** -** "Redistribution and use in source and binary forms, with or without -** modification, are permitted provided that the following conditions are -** met: -** * Redistributions of source code must retain the above copyright -** notice, this list of conditions and the following disclaimer. -** * Redistributions in binary form must reproduce the above copyright -** notice, this list of conditions and the following disclaimer in -** the documentation and/or other materials provided with the -** distribution. -** * Neither the name of The Qt Company Ltd nor the names of its -** contributors may be used to endorse or promote products derived -** from this software without specific prior written permission. -** -** -** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#include "gradients.h" -#include "hoverpoints.h" - -#include "qtControls/Controls.h" -#include "renderlib/Defines.h" -#include "renderlib/Logging.h" -#include "renderlib/MathUtil.h" - -#include - -std::vector -gradientStopsToVector(QGradientStops& stops) -{ - std::vector v; - for (int i = 0; i < stops.size(); ++i) { - v.push_back(LutControlPoint(stops.at(i).first, stops.at(i).second.alphaF())); - } - return v; -} - -QGradientStops -vectorToGradientStops(std::vector& v) -{ - QGradientStops stops; - for (int i = 0; i < v.size(); ++i) { - stops.push_back( - QPair(v[i].first, QColor::fromRgbF(v[i].second, v[i].second, v[i].second, v[i].second))); - } - return stops; -} - -ShadeWidget::ShadeWidget(const Histogram& histogram, ShadeType type, QWidget* parent) - : QWidget(parent) - , m_shade_type(type) - , m_alpha_gradient(QLinearGradient(0, 0, 0, 0)) - , m_histogram(histogram) -{ - // Checkers background - if (m_shade_type == ARGBShade) { - QPixmap pm(20, 20); - QPainter pmp(&pm); - pmp.fillRect(0, 0, 10, 10, Qt::lightGray); - pmp.fillRect(10, 10, 10, 10, Qt::lightGray); - pmp.fillRect(0, 10, 10, 10, Qt::darkGray); - pmp.fillRect(10, 0, 10, 10, Qt::darkGray); - pmp.end(); - QPalette pal = palette(); - pal.setBrush(backgroundRole(), QBrush(pm)); - setAutoFillBackground(true); - setPalette(pal); - - } else { - setAttribute(Qt::WA_OpaquePaintEvent); - } - - QPolygonF points; - points << QPointF(0.0f, 0.0f) << QPointF(1.0f, 1.0f); - - m_hoverPoints = new HoverPoints(this, HoverPoints::CircleShape); - // m_hoverPoints->setConnectionType(HoverPoints::LineConnection); - m_hoverPoints->setPoints(points); - m_hoverPoints->setPointLock(0, HoverPoints::LockToLeft); - m_hoverPoints->setPointLock(1, HoverPoints::LockToRight); - m_hoverPoints->setSortType(HoverPoints::XSort); - - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); - - connect(m_hoverPoints, &HoverPoints::pointsChanged, this, &ShadeWidget::colorsChanged); -} - -QPolygonF -ShadeWidget::points() const -{ - return m_hoverPoints->points(); -} - -void -ShadeWidget::setEditable(bool editable) -{ - m_hoverPoints->setEditable(editable); -} - -uint -ShadeWidget::colorAt(int x) -{ - generateShade(); - if (m_shade.isNull()) { - return 0; - } - - QPolygonF pts = m_hoverPoints->points(); - for (int i = 1; i < pts.size(); ++i) { - if (pts.at(i - 1).x() <= x && pts.at(i).x() >= x) { - QLineF l(pts.at(i - 1), pts.at(i)); - l.setLength(l.length() * ((x - l.x1()) / l.dx())); - return m_shade.pixel(qRound(qMin(l.x2(), (qreal(m_shade.width() - 1)))), - qRound(qMin(l.y2(), qreal(m_shade.height() - 1)))); - } - } - return 0; -} - -void -ShadeWidget::setGradientStops(const QGradientStops& stops) -{ - if (m_shade_type == ARGBShade) { - m_alpha_gradient = QLinearGradient(0, 0, width(), 0); - - for (int i = 0; i < stops.size(); ++i) { - QColor c = stops.at(i).second; - m_alpha_gradient.setColorAt(stops.at(i).first, QColor(c.red(), c.green(), c.blue())); - } - - m_shade = QImage(); - generateShade(); - update(); - } -} - -void -ShadeWidget::paintEvent(QPaintEvent*) -{ - generateShade(); - - QPainter p(this); - p.drawImage(0, 0, m_shade); - /* - qreal barWidth = width() / (qreal)m_histogram.size(); - - for (int i = 0; i < m_histogram.size(); ++i) { - qreal h = m_histogram[i] * height(); - // draw level - painter.fillRect(barWidth * i, height() - h, barWidth * (i + 1), height(), Qt::red); - // clear the rest of the control - painter.fillRect(barWidth * i, 0, barWidth * (i + 1), height() - h, Qt::black); - } - */ - p.setPen(QColor(146, 146, 146)); - p.drawRect(0, 0, width() - 1, height() - 1); -} - -void -ShadeWidget::drawHistogram(QPainter& p, int w, int h) -{ - size_t nbins = m_histogram._bins.size(); - int maxbinsize = m_histogram._bins[m_histogram._maxBin]; - for (size_t i = 0; i < nbins; ++i) { - float binheight = (float)m_histogram._bins[i] * (float)(h - 1) / (float)maxbinsize; - p.fillRect( - QRectF((float)i * (float)(w - 1) / (float)nbins, h - 1 - binheight, (float)(w - 1) / (float)nbins, binheight), - QColor(0, 0, 0, 255)); - } -} - -void -ShadeWidget::generateShade() -{ - if (m_shade.isNull() || m_shade.size() != size()) { - - QRect qrect = rect(); - QSize qsize = size(); - if (m_shade_type == ARGBShade) { - m_shade = QImage(qsize, QImage::Format_ARGB32_Premultiplied); - if (m_shade.isNull()) { - return; - } - m_shade.fill(0); - - QPainter p(&m_shade); - p.fillRect(qrect, m_alpha_gradient); - - p.setCompositionMode(QPainter::CompositionMode_DestinationIn); - QLinearGradient fade(0, 0, 0, height() - 1); - fade.setColorAt(0, QColor(255, 255, 255, 255)); - fade.setColorAt(1, QColor(0, 0, 0, 0)); - p.fillRect(qrect, fade); - - p.setCompositionMode(QPainter::CompositionMode_SourceOver); - - drawHistogram(p, qsize.width(), qsize.height()); - - } else { - m_shade = QImage(qsize, QImage::Format_RGB32); - if (m_shade.isNull()) { - return; - } - QLinearGradient shade(0, 0, 0, height()); - shade.setColorAt(1, Qt::black); - - if (m_shade_type == RedShade) - shade.setColorAt(0, Qt::red); - else if (m_shade_type == GreenShade) - shade.setColorAt(0, Qt::green); - else - shade.setColorAt(0, Qt::blue); - - QPainter p(&m_shade); - p.fillRect(qrect, shade); - - p.setCompositionMode(QPainter::CompositionMode_SourceOver); - - drawHistogram(p, qsize.width(), qsize.height()); - } - } -} - -GradientEditor::GradientEditor(const Histogram& histogram, QWidget* parent) - : QWidget(parent) -{ - QVBoxLayout* vbox = new QVBoxLayout(this); - vbox->setSpacing(1); - // vbox->setMargin(1); - - m_alpha_shade = new ShadeWidget(histogram, ShadeWidget::ARGBShade, this); - - vbox->addWidget(m_alpha_shade); - - connect(m_alpha_shade, &ShadeWidget::colorsChanged, this, &GradientEditor::pointsUpdated); -} - -inline static bool -x_less_than(const QPointF& p1, const QPointF& p2) -{ - return p1.x() < p2.x(); -} - -inline static bool -controlpoint_x_less_than(const LutControlPoint& p1, const LutControlPoint& p2) -{ - return p1.first < p2.first; -} - -QGradientStops -pointsToGradientStops(QPolygonF points) -{ - QGradientStops stops; - std::sort(points.begin(), points.end(), x_less_than); - - for (int i = 0; i < points.size(); ++i) { - qreal x = points.at(i).x(); - if (i + 1 < points.size() && x == points.at(i + 1).x()) - continue; - float pixelvalue = points.at(i).y(); - // TODO future: let each point in m_alpha_shade have a full RGBA color and use a color picker to assign it via dbl - // click or some other means - // unsigned int pixelvalue = m_alpha_shade->colorAt(int(x)); - // unsigned int r = (0x00ff0000 & pixelvalue) >> 16; - // unsigned int g = (0x0000ff00 & pixelvalue) >> 8; - // unsigned int b = (0x000000ff & pixelvalue); - // unsigned int a = (0xff000000 & pixelvalue) >> 24; - // QColor color(r, g, b, a); - - QColor color = QColor::fromRgbF(pixelvalue, pixelvalue, pixelvalue, pixelvalue); - if (x > 1) { - LOG_ERROR << "control point x greater than 1"; - return stops; - } - - stops << QGradientStop(x, color); - } - return stops; -} - -void -GradientEditor::pointsUpdated() -{ - // qreal w = m_alpha_shade->width(); - - QGradientStops stops = pointsToGradientStops(m_alpha_shade->points()); - - m_alpha_shade->setGradientStops(stops); - - emit gradientStopsChanged(stops); -} - -static void -set_shade_points(const QPolygonF& points, ShadeWidget* shade) -{ - if (points.size() < 2) { - return; - } - - QGradientStops stops = pointsToGradientStops(points); - shade->setGradientStops(stops); - - shade->hoverPoints()->setPoints(points); - shade->hoverPoints()->setPointLock(0, HoverPoints::LockToLeft); - shade->hoverPoints()->setPointLock(points.size() - 1, HoverPoints::LockToRight); - shade->update(); -} - -void -GradientEditor::setControlPoints(const std::vector& points) -{ - QPolygonF pts_alpha; - - for (auto p : points) { - pts_alpha << QPointF(p.first, p.second); - } - - set_shade_points(pts_alpha, m_alpha_shade); -} - -void -GradientEditor::wheelEvent(QWheelEvent* event) -{ - // wheel does nothing here! - event->ignore(); -} - -GradientWidget::GradientWidget(const Histogram& histogram, GradientData* dataObject, QWidget* parent) - : QWidget(parent) - , m_histogram(histogram) - , m_gradientData(dataObject) -{ - QVBoxLayout* mainGroupLayout = new QVBoxLayout(this); - - // setWindowTitle(tr("Gradients")); - - // QGroupBox* editorGroup = new QGroupBox(this); - // editorGroup->setTitle(tr("Color Editor")); - m_editor = new GradientEditor(m_histogram, this); - mainGroupLayout->addWidget(m_editor); - - auto* sectionLayout = Controls::createAgaveFormLayout(); - - QButtonGroup* btnGroup = new QButtonGroup(this); - QPushButton* minMaxButton = new QPushButton("Min/Max"); - minMaxButton->setToolTip(tr("Min/Max")); - minMaxButton->setStatusTip(tr("Choose Min/Max mode")); - QPushButton* windowLevelButton = new QPushButton("Wnd/Lvl"); - windowLevelButton->setToolTip(tr("Window/Level")); - windowLevelButton->setStatusTip(tr("Choose Window/Level mode")); - QPushButton* isoButton = new QPushButton("Iso"); - isoButton->setToolTip(tr("Isovalue")); - isoButton->setStatusTip(tr("Choose Isovalue mode")); - QPushButton* pctButton = new QPushButton("Pct"); - pctButton->setToolTip(tr("Histogram Percentiles")); - pctButton->setStatusTip(tr("Choose Histogram percentiles mode")); - QPushButton* customButton = new QPushButton("Custom"); - customButton->setToolTip(tr("Custom")); - customButton->setStatusTip(tr("Choose Custom editing mode")); - - static const int WINDOW_LEVEL_BTNID = 1; - static const int ISO_BTNID = 2; - static const int PCT_BTNID = 3; - static const int CUSTOM_BTNID = 4; - static const int MINMAX_BTNID = 5; - static std::map btnIdToGradientMode = { { WINDOW_LEVEL_BTNID, GradientEditMode::WINDOW_LEVEL }, - { ISO_BTNID, GradientEditMode::ISOVALUE }, - { PCT_BTNID, GradientEditMode::PERCENTILE }, - { MINMAX_BTNID, GradientEditMode::MINMAX }, - { CUSTOM_BTNID, GradientEditMode::CUSTOM } }; - static std::map gradientModeToBtnId = { { GradientEditMode::WINDOW_LEVEL, WINDOW_LEVEL_BTNID }, - { GradientEditMode::ISOVALUE, ISO_BTNID }, - { GradientEditMode::PERCENTILE, PCT_BTNID }, - { GradientEditMode::MINMAX, MINMAX_BTNID }, - { GradientEditMode::CUSTOM, CUSTOM_BTNID } }; - static std::map btnIdToStackedPage = { - { WINDOW_LEVEL_BTNID, 1 }, { ISO_BTNID, 2 }, { PCT_BTNID, 3 }, { MINMAX_BTNID, 0 }, { CUSTOM_BTNID, 4 } - }; - btnGroup->addButton(minMaxButton, MINMAX_BTNID); - btnGroup->addButton(windowLevelButton, WINDOW_LEVEL_BTNID); - btnGroup->addButton(isoButton, ISO_BTNID); - btnGroup->addButton(pctButton, PCT_BTNID); - btnGroup->addButton(customButton, CUSTOM_BTNID); - QHBoxLayout* hbox = new QHBoxLayout(); - hbox->setSpacing(0); - - int initialButtonId = WINDOW_LEVEL_BTNID; - GradientEditMode m = m_gradientData->m_activeMode; - initialButtonId = gradientModeToBtnId[m]; - - for (auto btn : btnGroup->buttons()) { - btn->setCheckable(true); - // set checked state initially. - int btnid = btnGroup->id(btn); - if (btnid == initialButtonId) { - btn->setChecked(true); - } - hbox->addWidget(btn); - } - mainGroupLayout->addLayout(hbox); - - QWidget* firstPageWidget = new QWidget; - auto* section0Layout = Controls::createAgaveFormLayout(); - firstPageWidget->setLayout(section0Layout); - - QWidget* secondPageWidget = new QWidget; - auto* section1Layout = Controls::createAgaveFormLayout(); - secondPageWidget->setLayout(section1Layout); - - QWidget* thirdPageWidget = new QWidget; - auto* section2Layout = Controls::createAgaveFormLayout(); - thirdPageWidget->setLayout(section2Layout); - - QWidget* fourthPageWidget = new QWidget; - auto* section3Layout = Controls::createAgaveFormLayout(); - fourthPageWidget->setLayout(section3Layout); - - QWidget* fifthPageWidget = new QWidget; - auto* section4Layout = Controls::createAgaveFormLayout(); - fifthPageWidget->setLayout(section4Layout); - - QStackedLayout* stackedLayout = new QStackedLayout(mainGroupLayout); - stackedLayout->addWidget(firstPageWidget); - stackedLayout->addWidget(secondPageWidget); - stackedLayout->addWidget(thirdPageWidget); - stackedLayout->addWidget(fourthPageWidget); - stackedLayout->addWidget(fifthPageWidget); - - int initialStackedPageIndex = btnIdToStackedPage[initialButtonId]; - stackedLayout->setCurrentIndex(initialStackedPageIndex); - // if this is not custom mode, then disable the gradient editor - m_editor->setEditable(m == GradientEditMode::CUSTOM); - - connect(btnGroup, - QOverload::of(&QButtonGroup::buttonClicked), - [this, btnGroup, stackedLayout](QAbstractButton* button) { - int id = btnGroup->id(button); - GradientEditMode modeToSet = btnIdToGradientMode[id]; - // if mode is not changing, we are done. - if (modeToSet == this->m_gradientData->m_activeMode) { - return; - } - this->m_gradientData->m_activeMode = modeToSet; - - stackedLayout->setCurrentIndex(btnIdToStackedPage[id]); - - // if this is not custom mode, then disable the gradient editor - m_editor->setEditable(modeToSet == GradientEditMode::CUSTOM); - - this->forceDataUpdate(); - }); - - QIntSlider* minu16Slider = new QIntSlider(); - minu16Slider->setStatusTip(tr("Minimum u16 value")); - minu16Slider->setToolTip(tr("Set minimum u16 value")); - minu16Slider->setRange(0, 65535); - minu16Slider->setSingleStep(1); - minu16Slider->setValue(m_gradientData->m_minu16); - section0Layout->addRow("Min u16", minu16Slider); - QIntSlider* maxu16Slider = new QIntSlider(); - maxu16Slider->setStatusTip(tr("Maximum u16 value")); - maxu16Slider->setToolTip(tr("Set maximum u16 value")); - maxu16Slider->setRange(0, 65535); - maxu16Slider->setSingleStep(1); - maxu16Slider->setValue(m_gradientData->m_maxu16); - section0Layout->addRow("Max u16", maxu16Slider); - connect(minu16Slider, &QIntSlider::valueChanged, [this, maxu16Slider](int i) { - this->m_gradientData->m_minu16 = i; - this->onSetMinMax(i, this->m_gradientData->m_maxu16); - }); - connect(maxu16Slider, &QIntSlider::valueChanged, [this, minu16Slider](int i) { - this->m_gradientData->m_maxu16 = i; - this->onSetMinMax(this->m_gradientData->m_minu16, i); - }); - - QNumericSlider* windowSlider = new QNumericSlider(); - windowSlider->setStatusTip(tr("Window")); - windowSlider->setToolTip(tr("Set size of range of intensities")); - windowSlider->setRange(0.0, 1.0); - windowSlider->setSingleStep(0.01); - windowSlider->setDecimals(3); - windowSlider->setValue(m_gradientData->m_window); - section1Layout->addRow("Window", windowSlider); - QNumericSlider* levelSlider = new QNumericSlider(); - levelSlider->setStatusTip(tr("Level")); - levelSlider->setToolTip(tr("Set level of mid intensity")); - levelSlider->setRange(0.0, 1.0); - levelSlider->setSingleStep(0.01); - levelSlider->setDecimals(3); - levelSlider->setValue(m_gradientData->m_level); - section1Layout->addRow("Level", levelSlider); - connect(windowSlider, &QNumericSlider::valueChanged, [this, levelSlider](double d) { - this->m_gradientData->m_window = d; - this->onSetWindowLevel(d, levelSlider->value()); - }); - connect(levelSlider, &QNumericSlider::valueChanged, [this, windowSlider](double d) { - this->m_gradientData->m_level = d; - this->onSetWindowLevel(windowSlider->value(), d); - }); - - QNumericSlider* isovalueSlider = new QNumericSlider(); - isovalueSlider->setStatusTip(tr("Isovalue")); - isovalueSlider->setToolTip(tr("Set Isovalue")); - isovalueSlider->setRange(0.0, 1.0); - isovalueSlider->setSingleStep(0.01); - isovalueSlider->setDecimals(3); - isovalueSlider->setValue(m_gradientData->m_isovalue); - section2Layout->addRow("Isovalue", isovalueSlider); - QNumericSlider* isorangeSlider = new QNumericSlider(); - isorangeSlider->setStatusTip(tr("Isovalue range")); - isorangeSlider->setToolTip(tr("Set range above and below isovalue")); - isorangeSlider->setRange(0.0, 1.0); - isorangeSlider->setSingleStep(0.01); - isorangeSlider->setDecimals(3); - isorangeSlider->setValue(m_gradientData->m_isorange); - section2Layout->addRow("Iso-range", isorangeSlider); - connect(isovalueSlider, &QNumericSlider::valueChanged, [this, isorangeSlider](double d) { - this->m_gradientData->m_isovalue = d; - this->onSetIsovalue(d, isorangeSlider->value()); - }); - connect(isorangeSlider, &QNumericSlider::valueChanged, [this, isovalueSlider](double d) { - this->m_gradientData->m_isorange = d; - this->onSetIsovalue(isovalueSlider->value(), d); - }); - - QNumericSlider* pctLowSlider = new QNumericSlider(); - pctLowSlider->setStatusTip(tr("Low percentile")); - pctLowSlider->setToolTip(tr("Set bottom percentile")); - pctLowSlider->setRange(0.0, 1.0); - pctLowSlider->setSingleStep(0.01); - pctLowSlider->setDecimals(3); - pctLowSlider->setValue(m_gradientData->m_pctLow); - section3Layout->addRow("Pct Min", pctLowSlider); - QNumericSlider* pctHighSlider = new QNumericSlider(); - pctHighSlider->setStatusTip(tr("High percentile")); - pctHighSlider->setToolTip(tr("Set top percentile")); - pctHighSlider->setRange(0.0, 1.0); - pctHighSlider->setSingleStep(0.01); - pctHighSlider->setDecimals(3); - pctHighSlider->setValue(m_gradientData->m_pctHigh); - section3Layout->addRow("Pct Max", pctHighSlider); - connect(pctLowSlider, &QNumericSlider::valueChanged, [this, pctHighSlider](double d) { - this->m_gradientData->m_pctLow = d; - this->onSetHistogramPercentiles(d, pctHighSlider->value()); - }); - connect(pctHighSlider, &QNumericSlider::valueChanged, [this, pctLowSlider](double d) { - this->m_gradientData->m_pctHigh = d; - this->onSetHistogramPercentiles(pctLowSlider->value(), d); - }); - - mainGroupLayout->addLayout(sectionLayout); - mainGroupLayout->addStretch(1); - - connect(m_editor, &GradientEditor::gradientStopsChanged, this, &GradientWidget::onGradientStopsChanged); - - forceDataUpdate(); -} - -void -GradientWidget::forceDataUpdate() -{ - GradientEditMode mode = this->m_gradientData->m_activeMode; - - switch (mode) { - case GradientEditMode::WINDOW_LEVEL: - this->onSetWindowLevel(this->m_gradientData->m_window, this->m_gradientData->m_level); - break; - case GradientEditMode::ISOVALUE: - this->onSetIsovalue(this->m_gradientData->m_isovalue, this->m_gradientData->m_isorange); - break; - case GradientEditMode::PERCENTILE: - this->onSetHistogramPercentiles(this->m_gradientData->m_pctLow, this->m_gradientData->m_pctHigh); - break; - case GradientEditMode::MINMAX: - this->onSetMinMax(this->m_gradientData->m_minu16, this->m_gradientData->m_maxu16); - break; - case GradientEditMode::CUSTOM: { - m_editor->setControlPoints(this->m_gradientData->m_customControlPoints); - QGradientStops stops = vectorToGradientStops(this->m_gradientData->m_customControlPoints); - emit gradientStopsChanged(stops); - } break; - default: - LOG_ERROR << "Bad gradient editor mode"; - break; - } -} - -void -GradientWidget::onGradientStopsChanged(const QGradientStops& stops) -{ - // update the data stored in m_gradientData - m_gradientData->m_customControlPoints.clear(); - for (int i = 0; i < stops.size(); ++i) { - m_gradientData->m_customControlPoints.push_back(LutControlPoint(stops.at(i).first, stops.at(i).second.alphaF())); - } - - emit gradientStopsChanged(stops); -} - -void -GradientWidget::onSetHistogramPercentiles(float pctLow, float pctHigh) -{ - float window, level; - m_histogram.computeWindowLevelFromPercentiles(pctLow, pctHigh, window, level); - this->onSetWindowLevel(window, level); -} - -void -GradientWidget::onSetWindowLevel(float window, float level) -{ - std::vector points; - static const float epsilon = 0.000001f; - window = std::max(window, epsilon); - float lowEnd = level - window * 0.5f; - float highEnd = level + window * 0.5f; - if (lowEnd <= 0.0f) { - float val = -lowEnd / (highEnd - lowEnd); - points.push_back({ 0.0f, val }); - } else { - points.push_back({ 0.0f, 0.0f }); - points.push_back({ lowEnd, 0.0f }); - } - if (highEnd >= 1.0f) { - float val = (1.0f - lowEnd) / (highEnd - lowEnd); - points.push_back({ 1.0f, val }); - } else { - points.push_back({ highEnd, 1.0f }); - points.push_back({ 1.0f, 1.0f }); - } - m_editor->setControlPoints(points); - emit gradientStopsChanged(vectorToGradientStops(points)); -} - -void -GradientWidget::onSetMinMax(uint16_t minu16, uint16_t maxu16) -{ - float relativeMin = normalizeInt(minu16); - float relativeMax = normalizeInt(maxu16); - relativeMin = std::max(relativeMin, 0.0f); - relativeMax = std::min(relativeMax, 1.0f); - if (relativeMin >= relativeMax) { - LOG_ERROR << "Min value is greater than or equal to max value: " << minu16 << " >= " << maxu16 - << ", datarange=" << m_histogram.dataRange(); - return; - } - float window = relativeMax - relativeMin; - float level = (relativeMax + relativeMin) / 2.0f; - this->onSetWindowLevel(window, level); -} - -void -GradientWidget::onSetIsovalue(float isovalue, float width) -{ - std::vector points; - float lowEnd = isovalue - width * 0.5f; - float highEnd = isovalue + width * 0.5f; - static const float epsilon = 0.00001f; - points.push_back({ 0.0f, 0.0f }); - points.push_back({ lowEnd - epsilon, 0.0f }); - points.push_back({ lowEnd + epsilon, 1.0f }); - points.push_back({ highEnd - epsilon, 1.0f }); - points.push_back({ highEnd + epsilon, 0.0f }); - points.push_back({ 1.0f, 0.0f }); - m_editor->setControlPoints(points); - emit gradientStopsChanged(vectorToGradientStops(points)); -} +#include "gradients.h" + +#include "Controls.h" +#include "qcustomplot.h" +#include "renderlib/Defines.h" +#include "renderlib/Logging.h" +#include "renderlib/MathUtil.h" + +#include + +std::vector +gradientStopsToVector(QGradientStops& stops) +{ + std::vector v; + for (int i = 0; i < stops.size(); ++i) { + v.push_back(LutControlPoint(stops.at(i).first, stops.at(i).second.alphaF())); + } + return v; +} + +QGradientStops +vectorToGradientStops(std::vector& v) +{ + QGradientStops stops; + for (int i = 0; i < v.size(); ++i) { + stops.push_back( + QPair(v[i].first, QColor::fromRgbF(v[i].second, v[i].second, v[i].second, v[i].second))); + } + return stops; +} + +static void +bound_point(double x, double y, const QRectF& bounds, int lock, double& out_x, double& out_y) +{ + qreal left = bounds.left(); + qreal right = bounds.right(); + // notice top/bottom switch here. + qreal bottom = bounds.top(); + qreal top = bounds.bottom(); + + out_x = x; + out_y = y; + + if (x <= left || (lock & GradientEditor::LockToLeft)) + out_x = left; + else if (x >= right || (lock & GradientEditor::LockToRight)) + out_x = right; + + if (y >= top || (lock & GradientEditor::LockToTop)) + out_y = top; + else if (y <= bottom || (lock & GradientEditor::LockToBottom)) + out_y = bottom; +} + +static constexpr double SCATTERSIZE = 10.0; + +GradientEditor::GradientEditor(const Histogram& histogram, QWidget* parent) + : QWidget(parent) + , m_histogram(histogram) +{ + QVBoxLayout* vbox = new QVBoxLayout(this); + vbox->setSpacing(1); + + m_customPlot = new QCustomPlot(this); + + // first graph will be histogram + QPalette pal = m_customPlot->palette(); + QColor histFillColor = pal.color(QPalette::Link).lighter(150); + m_histogramBars = new QCPBars(m_customPlot->xAxis, m_customPlot->yAxis); + QBrush barBrush = m_histogramBars->brush(); + barBrush.setColor(histFillColor); + m_histogramBars->setBrush(barBrush); + m_histogramBars->setPen(Qt::NoPen); + m_histogramBars->setWidthType(QCPBars::wtPlotCoords); + float firstBinCenter, lastBinCenter, binSize; + histogram.bin_range(histogram._bins.size(), firstBinCenter, lastBinCenter, binSize); + m_histogramBars->setWidth(binSize); + QVector keyData; + QVector valueData; + static constexpr double MIN_BAR_HEIGHT = 0.01; // Minimum height for nonzero bins (0.1% of max) + for (size_t i = 0; i < histogram._bins.size(); ++i) { + keyData << firstBinCenter + i * binSize; + if (histogram._bins[i] == 0) { + // Zero bins get zero height + valueData << 0.0; + } else { + // Nonzero bins get at least the minimum height + double normalizedHeight = (double)histogram._bins[i] / (double)histogram._bins[histogram._maxBin]; + valueData << std::max(normalizedHeight, MIN_BAR_HEIGHT); + } + } + m_histogramBars->setData(keyData, valueData); + m_histogramBars->setSelectable(QCP::stNone); + + // first added graph will the the piecewise linear transfer function + m_customPlot->addGraph(); + m_customPlot->graph(0)->setPen(QPen(Qt::black)); + QPen scatterPen(Qt::black); + scatterPen.setWidthF(1.0); + m_customPlot->graph(0)->setScatterStyle( + QCPScatterStyle(QCPScatterStyle::ssCircle, scatterPen, Qt::NoBrush, SCATTERSIZE)); + m_customPlot->graph(0)->setSelectable(QCP::stSingleData); + + // give the axes some labels: + m_customPlot->xAxis->setLabel(""); + m_customPlot->yAxis->setLabel(""); + + // set axes ranges, so we see all data: + m_customPlot->xAxis->setRange(histogram._dataMin, histogram._dataMax); + m_customPlot->xAxis->ticker()->setTickCount(4); + m_customPlot->xAxis->ticker()->setTickOrigin(histogram._dataMin); + auto tickLabelFont = m_customPlot->xAxis->tickLabelFont(); + tickLabelFont.setPointSize((float)tickLabelFont.pointSize() * 0.75); + m_customPlot->xAxis->setTickLabelFont(tickLabelFont); + QPen penx = m_customPlot->xAxis->basePen(); + penx.setWidthF(1.0); + m_customPlot->xAxis->setBasePen(penx); + + // increasing this will extend the Y axis up and down, so that the scatter handles are not clipped. + static constexpr double AXIS_OFFSET_FRACTION = 0.0; + m_customPlot->xAxis->setOffset(AXIS_OFFSET_FRACTION); + + m_customPlot->yAxis->setRange(0 - AXIS_OFFSET_FRACTION, 1 + AXIS_OFFSET_FRACTION); + m_customPlot->yAxis->ticker()->setTickCount(1); + tickLabelFont = m_customPlot->yAxis->tickLabelFont(); + tickLabelFont.setPointSize((float)tickLabelFont.pointSize() * 0.75); + m_customPlot->yAxis->setTickLabelFont(tickLabelFont); + QPen peny = m_customPlot->yAxis->basePen(); + peny.setWidthF(1.0); + m_customPlot->yAxis->setBasePen(peny); + + m_customPlot->xAxis->grid()->setVisible(true); + m_customPlot->xAxis->grid()->setSubGridVisible(true); + m_customPlot->yAxis->grid()->setVisible(true); + m_customPlot->yAxis->grid()->setSubGridVisible(true); + + m_customPlot->setInteractions( + QCP::iRangeDrag | QCP::iRangeZoom | + QCP::iSelectPlottables); // allow user to drag axis ranges with mouse, zoom with mouse wheel + m_customPlot->axisRect()->setRangeDrag(Qt::Horizontal); + m_customPlot->axisRect()->setRangeZoom(Qt::Horizontal); + + m_customPlot->replot(); + + connect(m_customPlot, &QCustomPlot::mousePress, this, &GradientEditor::onPlotMousePress); + connect(m_customPlot, &QCustomPlot::mouseMove, this, &GradientEditor::onPlotMouseMove); + connect(m_customPlot, &QCustomPlot::mouseRelease, this, &GradientEditor::onPlotMouseRelease); + connect(m_customPlot, &QCustomPlot::mouseWheel, this, &GradientEditor::onPlotMouseWheel); + connect(m_customPlot, &QCustomPlot::mouseDoubleClick, this, &GradientEditor::onPlotMouseDoubleClick); + + vbox->addWidget(m_customPlot); +} + +void +GradientEditor::changeEvent(QEvent* event) +{ + // This might be too many event types to check, but ThemeChange only seems to work on the QMainWindow. + // At least on Windows, changing dark mode to light mode incurs StyleChange and PaletteChange events too. + if (event->type() == QEvent::ThemeChange || event->type() == QEvent::ApplicationPaletteChange || + event->type() == QEvent::StyleChange || event->type() == QEvent::PaletteChange) { + // check for dark or light mode + auto sh = QGuiApplication::styleHints(); + auto colorScheme = sh->colorScheme(); + QColor plotLineColor; + QColor backgroundColor; + QColor barsColor; + QColor gridColor; + QColor subgridColor; + if (colorScheme == Qt::ColorScheme::Dark) { + barsColor = Qt::magenta; + barsColor.setAlphaF(0.5); + plotLineColor = this->palette().color(QPalette::Text); + backgroundColor = this->palette().color(QPalette::Window); + gridColor = plotLineColor.darker(150); + subgridColor = plotLineColor.darker(170); + } else if (colorScheme == Qt::ColorScheme::Light) { + barsColor = Qt::magenta; + barsColor.setAlphaF(0.25); + plotLineColor = this->palette().color(QPalette::Text); + backgroundColor = this->palette().color(QPalette::Window); + gridColor = plotLineColor.lighter(150); + subgridColor = plotLineColor.lighter(170); + } + m_customPlot->graph(0)->setPen(QPen(plotLineColor)); + QPen scatterPen(plotLineColor); + scatterPen.setWidthF(1.0); + m_customPlot->graph(0)->setScatterStyle( + QCPScatterStyle(QCPScatterStyle::ssCircle, scatterPen, Qt::NoBrush, SCATTERSIZE)); + m_customPlot->setBackground(QBrush(backgroundColor)); + m_customPlot->axisRect(); + + QPen basepen = m_customPlot->xAxis->basePen(); + basepen.setColor(plotLineColor); + m_customPlot->xAxis->setBasePen(basepen); + m_customPlot->yAxis->setBasePen(basepen); + + QPen gridpen = m_customPlot->xAxis->grid()->pen(); + gridpen.setColor(gridColor); + m_customPlot->xAxis->grid()->setPen(gridpen); + m_customPlot->yAxis->grid()->setPen(gridpen); + QPen subgridpen = m_customPlot->xAxis->grid()->subGridPen(); + subgridpen.setColor(subgridColor); + m_customPlot->xAxis->grid()->setSubGridPen(subgridpen); + m_customPlot->yAxis->grid()->setSubGridPen(subgridpen); + m_customPlot->xAxis->grid()->setAntialiasedSubGrid(true); + m_customPlot->yAxis->grid()->setAntialiasedSubGrid(true); + + QPen axisTickPen = m_customPlot->xAxis->tickPen(); + axisTickPen.setColor(plotLineColor); + m_customPlot->xAxis->setTickPen(axisTickPen); + m_customPlot->yAxis->setTickPen(axisTickPen); + m_customPlot->xAxis->setTickLabelColor(plotLineColor); + m_customPlot->yAxis->setTickLabelColor(plotLineColor); + QPen axisSubTickPen = m_customPlot->xAxis->subTickPen(); + axisSubTickPen.setColor(plotLineColor); + m_customPlot->xAxis->setSubTickPen(axisSubTickPen); + m_customPlot->yAxis->setSubTickPen(axisSubTickPen); + + m_histogramBars->setPen(Qt::NoPen); // QPen(barsColor)); + m_histogramBars->setBrush(QBrush(barsColor)); + + m_customPlot->replot(); + } + QWidget::changeEvent(event); +} + +void +GradientEditor::onPlotMousePress(QMouseEvent* event) +{ + // in custom mode, any click is either ON a point or creating a new point? + bool isCustomMode = (m_currentEditMode == GradientEditMode::CUSTOM); + bool isMinMaxMode = (m_currentEditMode == GradientEditMode::MINMAX); + bool isWindowLevelMode = (m_currentEditMode == GradientEditMode::WINDOW_LEVEL); + bool isPercentileMode = (m_currentEditMode == GradientEditMode::PERCENTILE); + bool isInteractiveMode = isCustomMode || isMinMaxMode || isWindowLevelMode || isPercentileMode; + + if (!isInteractiveMode) { + return; + } + + // let's look to see if a data point was clicked. + + int indexOfDataPoint = -1; + double dist = 1E+9; + + auto graph = m_customPlot->graph(0); + for (int n = 0; n < (graph->data()->size()); n++) { + // get xy of each data pt in pixels. compare with scattersize. + // first hit wins. + double x = (graph->data()->begin() + n)->key; + double y = (graph->data()->begin() + n)->value; + double px = m_customPlot->xAxis->coordToPixel(x); + double py = m_customPlot->yAxis->coordToPixel(y); + double dx = (px - (double)event->pos().x()); + double dy = (py - (double)event->pos().y()); + dist = sqrt(dx * dx + dy * dy); + if (dist < SCATTERSIZE / 2.0) { + indexOfDataPoint = n; + // remember dist! + break; + } + } + + if (event->button() == Qt::LeftButton) { + + // if we didn't click on a point, then we could add a point (only in custom mode): + if (indexOfDataPoint == -1 && isCustomMode) { + // this checks to see if user clicked along the line anywhere close. + QCPGraph* plottable = m_customPlot->plottableAt(event->pos(), true, &indexOfDataPoint); + if (plottable != nullptr && indexOfDataPoint > -1) { + // create a new point at x, y + double x = m_customPlot->xAxis->pixelToCoord(event->pos().x()); + double y = m_customPlot->yAxis->pixelToCoord(event->pos().y()); + // find first point above x to know the index? + for (int n = 0; n < (graph->data()->size()); n++) { + // get xy of each data pt in pixels. compare with scattersize. + // first hit wins. + double xn = (graph->data()->begin() + n)->key; + if (x < xn) { + // the index of x will be n. + indexOfDataPoint = n; + break; + } + } + m_locks.insert(indexOfDataPoint, 0); + graph->addData(x, y); + graph->data()->sort(); + m_customPlot->replot(); + } + } + + if (indexOfDataPoint > -1) { + // In MINMAX, Window/Level, and Percentile modes, only allow dragging of second and third points (threshold + // points) + if (isMinMaxMode || isWindowLevelMode || isPercentileMode) { + int dataSize = graph->data()->size(); + if (indexOfDataPoint != 1 && indexOfDataPoint != 2) { + // Not the second or third point (threshold points), don't allow dragging + return; + } + } + + m_isDraggingPoint = true; + m_currentPointIndex = indexOfDataPoint; + // turn off axis dragging while we are dragging a point + m_customPlot->axisRect()->setRangeDrag((Qt::Orientations)0); + // swallow this event so it doesn't propagate to the plot + event->accept(); + } + } else if (event->button() == Qt::RightButton) { + // Only allow point deletion in custom mode + if (indexOfDataPoint >= 0 && isCustomMode) { + if (m_locks[indexOfDataPoint] == 0) { + m_locks.remove(indexOfDataPoint); + // remove data pt from plot + graph->data()->remove((graph->data()->begin() + indexOfDataPoint)->key); + + // update the stuff because we did something + emit gradientStopsChanged(this->buildStopsFromPlot()); + m_customPlot->replot(); + event->accept(); + } + } + } +} +void +GradientEditor::onPlotMouseMove(QMouseEvent* event) +{ + bool isCustomMode = (m_currentEditMode == GradientEditMode::CUSTOM); + bool isMinMaxMode = (m_currentEditMode == GradientEditMode::MINMAX); + bool isWindowLevelMode = (m_currentEditMode == GradientEditMode::WINDOW_LEVEL); + bool isPercentileMode = (m_currentEditMode == GradientEditMode::PERCENTILE); + bool isInteractiveMode = isCustomMode || isMinMaxMode || isWindowLevelMode || isPercentileMode; + + if (!isInteractiveMode) { + return; + } + + if (m_isDraggingPoint && m_currentPointIndex >= 0) { + if (event->buttons() & Qt::LeftButton) { + auto graph = m_customPlot->graph(0); + double evx = m_customPlot->xAxis->pixelToCoord(event->pos().x()); + double evy = m_customPlot->yAxis->pixelToCoord(event->pos().y()); + + // Handle threshold-based modes (MINMAX, Window/Level, Percentile) differently + if (isMinMaxMode || isWindowLevelMode || isPercentileMode) { + // In threshold-based modes, keep Y value constant and only allow horizontal movement + double originalY = (graph->data()->begin() + m_currentPointIndex)->value; + evy = originalY; // Keep Y constant + + static constexpr double OVERLAP_EPSILON = 0.001; + // Apply additional constraints for min/max threshold points + if (m_currentPointIndex == 1) { + // This is the min threshold point - don't let it go past the max threshold point + if (graph->data()->size() > 2) { + double maxThresholdX = (graph->data()->begin() + 2)->key; + evx = std::min(evx, maxThresholdX - OVERLAP_EPSILON); // Small epsilon to prevent overlap + } + // Also don't let it go before the first fixed point + if (graph->data()->size() > 0) { + double firstPointX = (graph->data()->begin())->key; + evx = std::max(evx, firstPointX + OVERLAP_EPSILON); + } + } else if (m_currentPointIndex == 2) { + // This is the max threshold point - don't let it go past the min threshold point + if (graph->data()->size() > 1) { + double minThresholdX = (graph->data()->begin() + 1)->key; + evx = std::max(evx, minThresholdX + OVERLAP_EPSILON); // Small epsilon to prevent overlap + } + // Also don't let it go past the last fixed point + if (graph->data()->size() > 3) { + double lastPointX = (graph->data()->begin() + 3)->key; + evx = std::min(evx, lastPointX - OVERLAP_EPSILON); + } + } + } + + // see hoverpoints.cpp. + // this will make sure we don't move past locked edges in the bounding rectangle. + double px = evx, py = evy; + bound_point(evx, + evy, + // we really want clipRect() here? to capture zoomed region + QRectF(m_histogram._dataMin, 0.0f, m_histogram._dataMax - m_histogram._dataMin, 1.0f), + m_locks.at(m_currentPointIndex), + px, + py); + + // if we are dragging a point then move it + (graph->data()->begin() + m_currentPointIndex)->value = py; + (graph->data()->begin() + m_currentPointIndex)->key = px; + + // In MINMAX mode, don't sort - keep points in their fixed positions + if (!isMinMaxMode) { + // The point may have moved past other points, so sort, + // and account for current point index possibly changing. + // TODO should we always sort on every move? Or can we tell if we crossed another point? + graph->data().data()->sort(); + // find new index of current point + // find first point above x to know the index? + int indexOfDataPoint = m_currentPointIndex; + for (int n = 0; n < (graph->data()->size()); n++) { + // get xy of each data pt in pixels. compare with scattersize. + // first hit wins. + double xn = (graph->data()->begin() + n)->key; + if (px == xn) { + // the index of x will be n. + indexOfDataPoint = n; + break; + } + } + if (indexOfDataPoint != m_currentPointIndex) { + m_currentPointIndex = indexOfDataPoint; + } + } + + // In threshold-based modes, emit interactivePointsChanged for real-time slider updates + // Use the second and third points (indices 1 and 2) as the threshold points + if ((isMinMaxMode || isWindowLevelMode || isPercentileMode) && graph->data()->size() >= 4) { + double minThresholdX = (graph->data()->begin() + 1)->key; + double maxThresholdX = (graph->data()->begin() + 2)->key; + emit interactivePointsChanged(minThresholdX, maxThresholdX); + } + + // emit( DataChanged() ); + + emit gradientStopsChanged(this->buildStopsFromPlot()); + + m_customPlot->replot(); + } + } +} + +QGradientStops +GradientEditor::buildStopsFromPlot() +{ + // build up coords from the customplot into the form of gradient stops + QGradientStops stops; + + auto graph = m_customPlot->graph(0); + for (int n = 0; n < (graph->data()->size()); n++) { + auto dataIter = graph->data()->begin() + n; + double x = dataIter->key; + // skip duplicates? + if (n + 1 < graph->data()->size() && x == graph->data()->at(n + 1)->key) + continue; + + // rescale x to 0-1 range. + x = (x - m_histogram._dataMin) / (m_histogram._dataMax - m_histogram._dataMin); + double y = dataIter->value; + + QColor color = QColor::fromRgbF(y, y, y, y); + if (x > 1.0) { + LOG_ERROR << "control point x greater than 1"; + return stops; + } + + stops << QGradientStop(x, color); + } + return stops; +} + +void +GradientEditor::onPlotMouseRelease(QMouseEvent* event) +{ + Q_UNUSED(event); + if (m_currentEditMode != GradientEditMode::CUSTOM && m_currentEditMode != GradientEditMode::MINMAX && + m_currentEditMode != GradientEditMode::WINDOW_LEVEL && m_currentEditMode != GradientEditMode::PERCENTILE) { + return; + } + // if we were dragging a point then stop + m_isDraggingPoint = false; + m_currentPointIndex = -1; + // re-enable axis dragging + m_customPlot->axisRect()->setRangeDrag(Qt::Horizontal); +} + +void +GradientEditor::onPlotMouseDoubleClick(QMouseEvent* event) +{ + // double click should reset zoom + this->m_customPlot->rescaleAxes(); + this->m_customPlot->replot(); +} + +void +GradientEditor::onPlotMouseWheel(QWheelEvent* event) +{ + Q_UNUSED(event); +} + +inline static bool +x_less_than(const QPointF& p1, const QPointF& p2) +{ + return p1.x() < p2.x(); +} + +inline static bool +controlpoint_x_less_than(const LutControlPoint& p1, const LutControlPoint& p2) +{ + return p1.first < p2.first; +} + +QGradientStops +pointsToGradientStops(QPolygonF points) +{ + QGradientStops stops; + std::sort(points.begin(), points.end(), x_less_than); + + for (int i = 0; i < points.size(); ++i) { + qreal x = points.at(i).x(); + if (i + 1 < points.size() && x == points.at(i + 1).x()) + continue; + float pixelvalue = points.at(i).y(); + // TODO future: let each point have a full RGBA color and use a color picker to assign it via dbl + // click or some other means + + QColor color = QColor::fromRgbF(pixelvalue, pixelvalue, pixelvalue, pixelvalue); + if (x > 1) { + return stops; + } + + stops << QGradientStop(x, color); + } + return stops; +} + +void +GradientEditor::set_shade_points(const QPolygonF& points, QCustomPlot* plot, const Histogram& histogram) +{ + if (points.size() < 2) { + return; + } + + QGradientStops stops = pointsToGradientStops(points); + + m_locks.clear(); + if (points.size() > 0) { + m_locks.resize(points.size()); + m_locks.fill(0); + } + m_locks[0] = GradientEditor::LockToLeft; + m_locks[points.size() - 1] = GradientEditor::LockToRight; + + QVector x, y; + for (int i = 0; i < points.size(); ++i) { + // incoming points x values are in 0-1 range which is normalized to histogram data range + float dx = histogram._dataMin + points.at(i).x() * (histogram._dataMax - histogram._dataMin); + x << dx; + y << points.at(i).y(); + } + plot->graph(0)->setData(x, y); + plot->replot(); +} + +void +GradientEditor::setControlPoints(const std::vector& points) +{ + QPolygonF pts_alpha; + + for (auto p : points) { + pts_alpha << QPointF(p.first, p.second); + } + + set_shade_points(pts_alpha, m_customPlot, m_histogram); +} + +void +GradientEditor::wheelEvent(QWheelEvent* event) +{ + // wheel does nothing here! + event->ignore(); +} + +GradientWidget::GradientWidget(const Histogram& histogram, GradientData* dataObject, QWidget* parent) + : QWidget(parent) + , m_histogram(histogram) + , m_gradientData(dataObject) +{ + QVBoxLayout* mainGroupLayout = new QVBoxLayout(this); + + m_editor = new GradientEditor(m_histogram, this); + mainGroupLayout->addWidget(m_editor); + + auto* sectionLayout = Controls::createAgaveFormLayout(); + + QButtonGroup* btnGroup = new QButtonGroup(this); + QPushButton* minMaxButton = new QPushButton("Min/Max"); + minMaxButton->setToolTip(tr("Min/Max")); + minMaxButton->setStatusTip(tr("Choose Min/Max mode")); + QPushButton* windowLevelButton = new QPushButton("Wnd/Lvl"); + windowLevelButton->setToolTip(tr("Window/Level")); + windowLevelButton->setStatusTip(tr("Choose Window/Level mode")); + QPushButton* isoButton = new QPushButton("Iso"); + isoButton->setToolTip(tr("Isovalue")); + isoButton->setStatusTip(tr("Choose Isovalue mode")); + QPushButton* pctButton = new QPushButton("Pct"); + pctButton->setToolTip(tr("Histogram Percentiles")); + pctButton->setStatusTip(tr("Choose Histogram percentiles mode")); + QPushButton* customButton = new QPushButton("Custom"); + customButton->setToolTip(tr("Custom")); + customButton->setStatusTip(tr("Choose Custom editing mode")); + + static const int WINDOW_LEVEL_BTNID = 1; + static const int ISO_BTNID = 2; + static const int PCT_BTNID = 3; + static const int CUSTOM_BTNID = 4; + static const int MINMAX_BTNID = 5; + static std::map btnIdToGradientMode = { { WINDOW_LEVEL_BTNID, GradientEditMode::WINDOW_LEVEL }, + { ISO_BTNID, GradientEditMode::ISOVALUE }, + { PCT_BTNID, GradientEditMode::PERCENTILE }, + { MINMAX_BTNID, GradientEditMode::MINMAX }, + { CUSTOM_BTNID, GradientEditMode::CUSTOM } }; + static std::map gradientModeToBtnId = { { GradientEditMode::WINDOW_LEVEL, WINDOW_LEVEL_BTNID }, + { GradientEditMode::ISOVALUE, ISO_BTNID }, + { GradientEditMode::PERCENTILE, PCT_BTNID }, + { GradientEditMode::MINMAX, MINMAX_BTNID }, + { GradientEditMode::CUSTOM, CUSTOM_BTNID } }; + static std::map btnIdToStackedPage = { + { WINDOW_LEVEL_BTNID, 1 }, { ISO_BTNID, 2 }, { PCT_BTNID, 3 }, { MINMAX_BTNID, 0 }, { CUSTOM_BTNID, 4 } + }; + btnGroup->addButton(minMaxButton, MINMAX_BTNID); + btnGroup->addButton(windowLevelButton, WINDOW_LEVEL_BTNID); + btnGroup->addButton(isoButton, ISO_BTNID); + btnGroup->addButton(pctButton, PCT_BTNID); + btnGroup->addButton(customButton, CUSTOM_BTNID); + QHBoxLayout* hbox = new QHBoxLayout(); + hbox->setSpacing(0); + + int initialButtonId = WINDOW_LEVEL_BTNID; + GradientEditMode m = m_gradientData->m_activeMode; + initialButtonId = gradientModeToBtnId[m]; + + for (auto btn : btnGroup->buttons()) { + btn->setCheckable(true); + // set checked state initially. + int btnid = btnGroup->id(btn); + if (btnid == initialButtonId) { + btn->setChecked(true); + } + hbox->addWidget(btn); + } + mainGroupLayout->addLayout(hbox); + + QWidget* firstPageWidget = new QWidget; + auto* section0Layout = Controls::createAgaveFormLayout(); + firstPageWidget->setLayout(section0Layout); + + QWidget* secondPageWidget = new QWidget; + auto* section1Layout = Controls::createAgaveFormLayout(); + secondPageWidget->setLayout(section1Layout); + + QWidget* thirdPageWidget = new QWidget; + auto* section2Layout = Controls::createAgaveFormLayout(); + thirdPageWidget->setLayout(section2Layout); + + QWidget* fourthPageWidget = new QWidget; + auto* section3Layout = Controls::createAgaveFormLayout(); + fourthPageWidget->setLayout(section3Layout); + + QWidget* fifthPageWidget = new QWidget; + auto* section4Layout = Controls::createAgaveFormLayout(); + fifthPageWidget->setLayout(section4Layout); + + QStackedLayout* stackedLayout = new QStackedLayout(mainGroupLayout); + stackedLayout->addWidget(firstPageWidget); + stackedLayout->addWidget(secondPageWidget); + stackedLayout->addWidget(thirdPageWidget); + stackedLayout->addWidget(fourthPageWidget); + stackedLayout->addWidget(fifthPageWidget); + + int initialStackedPageIndex = btnIdToStackedPage[initialButtonId]; + stackedLayout->setCurrentIndex(initialStackedPageIndex); + // if this is not custom mode, then disable the gradient editor + // m_editor->setEditable(m == GradientEditMode::CUSTOM); + m_editor->setEditMode(m); + + connect(btnGroup, + QOverload::of(&QButtonGroup::buttonClicked), + [this, btnGroup, stackedLayout](QAbstractButton* button) { + int id = btnGroup->id(button); + GradientEditMode modeToSet = btnIdToGradientMode[id]; + // if mode is not changing, we are done. + if (modeToSet == this->m_gradientData->m_activeMode) { + return; + } + this->m_gradientData->m_activeMode = modeToSet; + + stackedLayout->setCurrentIndex(btnIdToStackedPage[id]); + + // if this is not custom mode, then disable the gradient editor + // m_editor->setEditable(modeToSet == GradientEditMode::CUSTOM); + m_editor->setEditMode(modeToSet); + + this->forceDataUpdate(); + }); + + minu16Slider = new QIntSlider(); + minu16Slider->setStatusTip(tr("Minimum u16 value")); + minu16Slider->setToolTip(tr("Set minimum u16 value")); + minu16Slider->setRange(m_histogram._dataMin, m_histogram._dataMax); + minu16Slider->setSingleStep(1); + minu16Slider->setValue(m_gradientData->m_minu16); + section0Layout->addRow("Min u16", minu16Slider); + maxu16Slider = new QIntSlider(); + maxu16Slider->setStatusTip(tr("Maximum u16 value")); + maxu16Slider->setToolTip(tr("Set maximum u16 value")); + maxu16Slider->setRange(m_histogram._dataMin, m_histogram._dataMax); + maxu16Slider->setSingleStep(1); + maxu16Slider->setValue(m_gradientData->m_maxu16); + section0Layout->addRow("Max u16", maxu16Slider); + connect(minu16Slider, &QIntSlider::valueChanged, [this](int i) { + this->m_gradientData->m_minu16 = i; + this->onSetMinMax(i, this->m_gradientData->m_maxu16); + }); + connect(maxu16Slider, &QIntSlider::valueChanged, [this](int i) { + this->m_gradientData->m_maxu16 = i; + this->onSetMinMax(this->m_gradientData->m_minu16, i); + }); + + windowSlider = new QNumericSlider(); + windowSlider->setStatusTip(tr("Window")); + windowSlider->setToolTip(tr("Set size of range of intensities")); + windowSlider->setRange(0.0, 1.0); + windowSlider->setSingleStep(0.01); + windowSlider->setDecimals(3); + windowSlider->setValue(m_gradientData->m_window); + section1Layout->addRow("Window", windowSlider); + levelSlider = new QNumericSlider(); + levelSlider->setStatusTip(tr("Level")); + levelSlider->setToolTip(tr("Set level of mid intensity")); + levelSlider->setRange(0.0, 1.0); + levelSlider->setSingleStep(0.01); + levelSlider->setDecimals(3); + levelSlider->setValue(m_gradientData->m_level); + section1Layout->addRow("Level", levelSlider); + connect(windowSlider, &QNumericSlider::valueChanged, [this](double d) { + this->m_gradientData->m_window = d; + this->onSetWindowLevel(d, levelSlider->value()); + }); + connect(levelSlider, &QNumericSlider::valueChanged, [this](double d) { + this->m_gradientData->m_level = d; + this->onSetWindowLevel(windowSlider->value(), d); + }); + + isovalueSlider = new QNumericSlider(); + isovalueSlider->setStatusTip(tr("Isovalue")); + isovalueSlider->setToolTip(tr("Set Isovalue")); + isovalueSlider->setRange(0.0, 1.0); + isovalueSlider->setSingleStep(0.01); + isovalueSlider->setDecimals(3); + isovalueSlider->setValue(m_gradientData->m_isovalue); + section2Layout->addRow("Isovalue", isovalueSlider); + isorangeSlider = new QNumericSlider(); + isorangeSlider->setStatusTip(tr("Isovalue range")); + isorangeSlider->setToolTip(tr("Set range above and below isovalue")); + isorangeSlider->setRange(0.0, 1.0); + isorangeSlider->setSingleStep(0.01); + isorangeSlider->setDecimals(3); + isorangeSlider->setValue(m_gradientData->m_isorange); + section2Layout->addRow("Iso-range", isorangeSlider); + connect(isovalueSlider, &QNumericSlider::valueChanged, [this](double d) { + this->m_gradientData->m_isovalue = d; + this->onSetIsovalue(d, isorangeSlider->value()); + }); + connect(isorangeSlider, &QNumericSlider::valueChanged, [this](double d) { + this->m_gradientData->m_isorange = d; + this->onSetIsovalue(isovalueSlider->value(), d); + }); + + pctLowSlider = new QNumericSlider(); + pctLowSlider->setStatusTip(tr("Low percentile")); + pctLowSlider->setToolTip(tr("Set bottom percentile")); + pctLowSlider->setRange(0.0, 1.0); + pctLowSlider->setSingleStep(0.01); + pctLowSlider->setDecimals(3); + pctLowSlider->setValue(m_gradientData->m_pctLow); + section3Layout->addRow("Pct Min", pctLowSlider); + pctHighSlider = new QNumericSlider(); + pctHighSlider->setStatusTip(tr("High percentile")); + pctHighSlider->setToolTip(tr("Set top percentile")); + pctHighSlider->setRange(0.0, 1.0); + pctHighSlider->setSingleStep(0.01); + pctHighSlider->setDecimals(3); + pctHighSlider->setValue(m_gradientData->m_pctHigh); + section3Layout->addRow("Pct Max", pctHighSlider); + connect(pctLowSlider, &QNumericSlider::valueChanged, [this](double d) { + this->m_gradientData->m_pctLow = d; + this->onSetHistogramPercentiles(d, pctHighSlider->value()); + }); + connect(pctHighSlider, &QNumericSlider::valueChanged, [this](double d) { + this->m_gradientData->m_pctHigh = d; + this->onSetHistogramPercentiles(pctLowSlider->value(), d); + }); + + mainGroupLayout->addLayout(sectionLayout); + mainGroupLayout->addStretch(1); + + connect(m_editor, &GradientEditor::gradientStopsChanged, this, &GradientWidget::onGradientStopsChanged); + connect(m_editor, &GradientEditor::interactivePointsChanged, this, &GradientWidget::onInteractivePointsChanged); + + forceDataUpdate(); +} + +void +GradientWidget::forceDataUpdate() +{ + GradientEditMode mode = this->m_gradientData->m_activeMode; + + switch (mode) { + case GradientEditMode::WINDOW_LEVEL: + this->onSetWindowLevel(this->m_gradientData->m_window, this->m_gradientData->m_level); + break; + case GradientEditMode::ISOVALUE: + this->onSetIsovalue(this->m_gradientData->m_isovalue, this->m_gradientData->m_isorange); + break; + case GradientEditMode::PERCENTILE: + this->onSetHistogramPercentiles(this->m_gradientData->m_pctLow, this->m_gradientData->m_pctHigh); + break; + case GradientEditMode::MINMAX: + this->onSetMinMax(this->m_gradientData->m_minu16, this->m_gradientData->m_maxu16); + break; + case GradientEditMode::CUSTOM: { + m_editor->setControlPoints(this->m_gradientData->m_customControlPoints); + QGradientStops stops = vectorToGradientStops(this->m_gradientData->m_customControlPoints); + emit gradientStopsChanged(stops); + } break; + default: + LOG_ERROR << "Bad gradient editor mode"; + break; + } +} + +void +GradientWidget::onGradientStopsChanged(const QGradientStops& stops) +{ + // update the data stored in m_gradientData + // depending on our mode: + if (m_gradientData->m_activeMode == GradientEditMode::CUSTOM) { + m_gradientData->m_customControlPoints.clear(); + for (int i = 0; i < stops.size(); ++i) { + m_gradientData->m_customControlPoints.push_back(LutControlPoint(stops.at(i).first, stops.at(i).second.alphaF())); + } + emit gradientStopsChanged(stops); + } else if (m_gradientData->m_activeMode == GradientEditMode::WINDOW_LEVEL) { + // extract window and level from the stops - use second and third points (threshold points) + std::vector points = gradientStopsToVector(const_cast(stops)); + if (points.size() < 4) { + return; + } + std::sort(points.begin(), points.end(), controlpoint_x_less_than); + float low = points[1].first; // Second point (low threshold) + float high = points[2].first; // Third point (high threshold) + float window = high - low; + float level = (high + low) * 0.5f; + m_gradientData->m_window = window; + m_gradientData->m_level = level; + + // update the sliders to match: + windowSlider->setValue(window); + levelSlider->setValue(level); + + } else if (m_gradientData->m_activeMode == GradientEditMode::ISOVALUE) { + // extract isovalue and range from the stops + std::vector points = gradientStopsToVector(const_cast(stops)); + if (points.size() < 2) { + return; + } + std::sort(points.begin(), points.end(), controlpoint_x_less_than); + float low = points[0].first; + float high = points[1].first; + float isovalue = (high + low) * 0.5f; + float isorange = high - low; + m_gradientData->m_isovalue = isovalue; + m_gradientData->m_isorange = isorange; + + // update the sliders to match: + isovalueSlider->setValue(isovalue); + isorangeSlider->setValue(isorange); + } else if (m_gradientData->m_activeMode == GradientEditMode::PERCENTILE) { + // get percentiles from the stops and histogram - use second and third points (threshold points) + std::vector points = gradientStopsToVector(const_cast(stops)); + if (points.size() < 4) { + return; + } + std::sort(points.begin(), points.end(), controlpoint_x_less_than); + float low = points[1].first; // Second point (low threshold) + float high = points[2].first; // Third point (high threshold) + // calculate percentiles from the histogram: + uint16_t ulow = m_histogram._dataMin + static_cast(low * (m_histogram._dataMax - m_histogram._dataMin)); + uint16_t uhigh = m_histogram._dataMin + static_cast(high * (m_histogram._dataMax - m_histogram._dataMin)); + float pctLow = 0.0f, pctHigh = 1.0f; + m_histogram.computePercentile(ulow, pctLow); + m_histogram.computePercentile(uhigh, pctHigh); + m_gradientData->m_pctLow = pctLow; + m_gradientData->m_pctHigh = pctHigh; + + // update the sliders to match: + pctLowSlider->setValue(pctLow); + pctHighSlider->setValue(pctHigh); + } else if (m_gradientData->m_activeMode == GradientEditMode::MINMAX) { + // get absolute min/max from the stops - use second and third points (threshold points) + std::vector points = gradientStopsToVector(const_cast(stops)); + if (points.size() < 4) { + return; + } + std::sort(points.begin(), points.end(), controlpoint_x_less_than); + // turn the second and third points' x values into u16 intensities from the histogram range: + float low = points[1].first; // Second point (min threshold) + float high = points[2].first; // Third point (max threshold) + // calculate percentiles from the histogram: + uint16_t ulow = m_histogram._dataMin + static_cast(low * (m_histogram._dataMax - m_histogram._dataMin)); + uint16_t uhigh = m_histogram._dataMin + static_cast(high * (m_histogram._dataMax - m_histogram._dataMin)); + m_gradientData->m_minu16 = ulow; + m_gradientData->m_maxu16 = uhigh; + + // update the sliders to match: + minu16Slider->setValue(ulow); + maxu16Slider->setValue(uhigh); + } +} + +void +GradientWidget::onSetHistogramPercentiles(float pctLow, float pctHigh) +{ + float window, level; + m_histogram.computeWindowLevelFromPercentiles(pctLow, pctHigh, window, level); + this->onSetWindowLevel(window, level); +} + +void +GradientWidget::onSetWindowLevel(float window, float level) +{ + std::vector points; + static const float epsilon = 0.000001f; + window = std::max(window, epsilon); + float lowEnd = level - window * 0.5f; + float highEnd = level + window * 0.5f; + if (lowEnd <= 0.0f) { + float val = -lowEnd / (highEnd - lowEnd); + points.push_back({ 0.0f, val }); + } else { + points.push_back({ 0.0f, 0.0f }); + points.push_back({ lowEnd, 0.0f }); + } + if (highEnd >= 1.0f) { + float val = (1.0f - lowEnd) / (highEnd - lowEnd); + points.push_back({ 1.0f, val }); + } else { + points.push_back({ highEnd, 1.0f }); + points.push_back({ 1.0f, 1.0f }); + } + m_editor->setControlPoints(points); + emit gradientStopsChanged(vectorToGradientStops(points)); +} + +void +GradientWidget::onSetMinMax(uint16_t minu16, uint16_t maxu16) +{ + // these need to be relative to the data range of the channel, not absolute! + float relativeMin = normalizeInt(minu16, m_histogram._dataMin, m_histogram._dataMax); + float relativeMax = normalizeInt(maxu16, m_histogram._dataMin, m_histogram._dataMax); + relativeMin = std::max(relativeMin, 0.0f); + relativeMax = std::min(relativeMax, 1.0f); + if (relativeMin >= relativeMax) { + LOG_ERROR << "Min value is greater than or equal to max value: " << minu16 << " >= " << maxu16 + << ", datarange=" << m_histogram.dataRange(); + return; + } + float window = relativeMax - relativeMin; + float level = (relativeMax + relativeMin) / 2.0f; + this->onSetWindowLevel(window, level); +} + +void +GradientWidget::onSetIsovalue(float isovalue, float width) +{ + std::vector points; + float lowEnd = isovalue - width * 0.5f; + float highEnd = isovalue + width * 0.5f; + static const float epsilon = 0.00001f; + points.push_back({ 0.0f, 0.0f }); + points.push_back({ lowEnd - epsilon, 0.0f }); + points.push_back({ lowEnd + epsilon, 1.0f }); + points.push_back({ highEnd - epsilon, 1.0f }); + points.push_back({ highEnd + epsilon, 0.0f }); + points.push_back({ 1.0f, 0.0f }); + m_editor->setControlPoints(points); + emit gradientStopsChanged(vectorToGradientStops(points)); +} + +void +GradientWidget::onInteractivePointsChanged(float minIntensity, float maxIntensity) +{ + // Handle different modes appropriately + if (m_gradientData->m_activeMode == GradientEditMode::MINMAX) { + // Convert from graph coordinates (histogram data range) to u16 intensity values + uint16_t minu16 = static_cast(minIntensity); + uint16_t maxu16 = static_cast(maxIntensity); + + // Ensure values are within valid range + minu16 = std::max(minu16, static_cast(m_histogram._dataMin)); + maxu16 = std::min(maxu16, static_cast(m_histogram._dataMax)); + + // Update the data + m_gradientData->m_minu16 = minu16; + m_gradientData->m_maxu16 = maxu16; + + // Update sliders without triggering their signals (to avoid feedback loops) + if (minu16Slider) { + minu16Slider->blockSignals(true); + minu16Slider->setValue(minu16); + minu16Slider->blockSignals(false); + } + if (maxu16Slider) { + maxu16Slider->blockSignals(true); + maxu16Slider->setValue(maxu16); + maxu16Slider->blockSignals(false); + } + } else if (m_gradientData->m_activeMode == GradientEditMode::WINDOW_LEVEL) { + // Convert intensities to normalized values (0-1 range) + uint16_t minInt = static_cast(minIntensity); + uint16_t maxInt = static_cast(maxIntensity); + float relativeMin = normalizeInt(minInt, m_histogram._dataMin, m_histogram._dataMax); + float relativeMax = normalizeInt(maxInt, m_histogram._dataMin, m_histogram._dataMax); + + // Calculate window and level from the threshold points + float window = relativeMax - relativeMin; + float level = (relativeMax + relativeMin) * 0.5f; + + // Update the data + m_gradientData->m_window = window; + m_gradientData->m_level = level; + + // Update sliders without triggering their signals + if (windowSlider) { + windowSlider->blockSignals(true); + windowSlider->setValue(window); + windowSlider->blockSignals(false); + } + if (levelSlider) { + levelSlider->blockSignals(true); + levelSlider->setValue(level); + levelSlider->blockSignals(false); + } + } else if (m_gradientData->m_activeMode == GradientEditMode::PERCENTILE) { + // Convert intensities to u16 values first + uint16_t minu16 = static_cast(std::max(minIntensity, (float)m_histogram._dataMin)); + uint16_t maxu16 = static_cast(std::min(maxIntensity, (float)m_histogram._dataMax)); + + // Calculate percentiles by converting intensity to bin index and using cumulative counts + float pctLow = 0.0f, pctHigh = 1.0f; + + if (m_histogram._pixelCount > 0 && !m_histogram._ccounts.empty()) { + // For low percentile + if (minu16 <= m_histogram._dataMin) { + pctLow = 0.0f; + } else if (minu16 >= m_histogram._dataMax) { + pctLow = 1.0f; + } else { + m_histogram.computePercentile(minu16, pctLow); + } + + // For high percentile + if (maxu16 <= m_histogram._dataMin) { + pctHigh = 0.0f; + } else if (maxu16 >= m_histogram._dataMax) { + pctHigh = 1.0f; + } else { + m_histogram.computePercentile(maxu16, pctHigh); + } + } + + // Update the data + m_gradientData->m_pctLow = pctLow; + m_gradientData->m_pctHigh = pctHigh; + + // Update sliders without triggering their signals + if (pctLowSlider) { + pctLowSlider->blockSignals(true); + pctLowSlider->setValue(pctLow); + pctLowSlider->blockSignals(false); + } + if (pctHighSlider) { + pctHighSlider->blockSignals(true); + pctHighSlider->setValue(pctHigh); + pctHighSlider->blockSignals(false); + } + } +} diff --git a/renderlib/AppearanceDataObject.cpp b/renderlib/AppearanceDataObject.cpp index 82055d9a..b79c2d4e 100644 --- a/renderlib/AppearanceDataObject.cpp +++ b/renderlib/AppearanceDataObject.cpp @@ -2,5 +2,3 @@ #include "Enumerations.h" #include "Logging.h" - -AppearanceDataObject::AppearanceDataObject() {} diff --git a/renderlib/AppearanceDataObject.hpp b/renderlib/AppearanceDataObject.hpp index 02df6f82..543ebc4f 100644 --- a/renderlib/AppearanceDataObject.hpp +++ b/renderlib/AppearanceDataObject.hpp @@ -1,19 +1,28 @@ #pragma once -#include "core/prty/prtyInt8.hpp" +#include "core/prty/prtyIntegerTemplate.hpp" +#include "core/prty/prtyEnum.hpp" #include "core/prty/prtyFloat.hpp" #include "core/prty/prtyBoolean.hpp" #include "core/prty/prtyVector3d.hpp" #include "core/prty/prtyColor.hpp" #include "glm.h" -class AppearanceDataOject +class AppearanceDataObject { public: - AppearanceDataObject(); + AppearanceDataObject() + { + RendererType.SetEnumTag(0, "Ray march blending"); + RendererType.SetEnumTag(1, "Path traced"); - prtyInt8 RendererType{ "RendererType", 0 }; - prtyInt8 ShadingType{ "ShadingType", 0 }; + ShadingType.SetEnumTag(0, "BRDF Only"); + ShadingType.SetEnumTag(1, "Phase Function Only"); + ShadingType.SetEnumTag(2, "Mixed"); + } + + prtyEnum RendererType{ "RendererType", 0 }; + prtyEnum ShadingType{ "ShadingType", 0 }; prtyFloat DensityScale{ "DensityScale", 1.0f }; prtyFloat GradientFactor{ "GradientFactor", 0.5f }; prtyFloat StepSizePrimaryRay{ "StepSizePrimaryRay", 1.0f }; diff --git a/renderlib/AppearanceObject.cpp b/renderlib/AppearanceObject.cpp index ed1fc23e..102dcd30 100644 --- a/renderlib/AppearanceObject.cpp +++ b/renderlib/AppearanceObject.cpp @@ -1,5 +1,8 @@ -#include "AppearanceUiDescription.hpp" +#include "AppearanceObject.hpp" +#include "Logging.h" +#include "serialize/docReader.h" +#include "serialize/docWriter.h" // ComboBoxUiInfo AppearanceUiDescription::m_rendererType("Renderer Type", // "Select volume rendering type", // "Select volume rendering type", @@ -55,22 +58,101 @@ AppearanceObject::AppearanceObject() : prtyObject() { - m_RenderSettings = std::make_shared(); - m_rendererType = new ComboBoxUiInfo(&m_appearanceDataObject.RendererType, "Appearance", "Renderer Type"); - m_shadingType = new ComboBoxUiInfo(&m_appearanceDataObject.ShadingType, "Appearance", "Shading Type"); - m_densityScale = new FloatSliderSpinnerUiInfo(&m_appearanceDataObject.DensityScale, "Appearance", "Density Scale"); - m_gradientFactor = - new FloatSliderSpinnerUiInfo(&m_appearanceDataObject.GradientFactor, "Appearance", "Gradient Factor"); + std::string category("Rendering"); + m_renderSettings = std::make_shared(); + m_rendererType = new ComboBoxUiInfo(&m_appearanceDataObject.RendererType, category, "Renderer Type"); + m_rendererType->SetToolTip("Select volume rendering type"); + m_rendererType->SetStatusTip("Select volume rendering type"); + AddProperty(m_rendererType); + m_shadingType = new ComboBoxUiInfo(&m_appearanceDataObject.ShadingType, category, "Shading Type"); + m_shadingType->SetToolTip("Select volume shading style"); + m_shadingType->SetStatusTip("Select volume shading style"); + AddProperty(m_shadingType); + m_densityScale = new FloatSliderSpinnerUiInfo(&m_appearanceDataObject.DensityScale, category, "Density Scale"); + m_densityScale->SetToolTip("Set scattering density for volume"); + m_densityScale->SetStatusTip("Set scattering density for volume"); + m_densityScale->min = 0.001f; + m_densityScale->max = 100.0f; + m_densityScale->decimals = 3; // decimals + m_densityScale->singleStep = 0.01f; // singleStep + m_densityScale->numTickMarks = 10; // numTickMarks + m_densityScale->suffix = ""; // suffix + AddProperty(m_densityScale); + m_gradientFactor = new FloatSliderSpinnerUiInfo(&m_appearanceDataObject.GradientFactor, category, "Gradient Factor"); + m_gradientFactor->SetToolTip("Mix between BRDF and Phase shading"); + m_gradientFactor->SetStatusTip("Mix between BRDF and Phase shading"); + m_gradientFactor->min = 0.0f; + m_gradientFactor->max = 1.0f; + m_gradientFactor->decimals = 3; // decimals + m_gradientFactor->singleStep = 0.01f; // singleStep + m_gradientFactor->numTickMarks = 10; // numTickMarks + m_gradientFactor->suffix = ""; // suffix + AddProperty(m_gradientFactor); m_stepSizePrimaryRay = - new FloatSliderSpinnerUiInfo(&m_appearanceDataObject.StepSizePrimaryRay, "Appearance", "Step Size Primary Ray"); + new FloatSliderSpinnerUiInfo(&m_appearanceDataObject.StepSizePrimaryRay, category, "Step Size Primary Ray"); + m_stepSizePrimaryRay->SetToolTip("Set volume ray march step size for camera rays"); + m_stepSizePrimaryRay->SetStatusTip("Set volume ray march step size for camera rays"); + m_stepSizePrimaryRay->min = 1.0f; + m_stepSizePrimaryRay->max = 100.0f; + m_stepSizePrimaryRay->decimals = 3; // decimals + m_stepSizePrimaryRay->singleStep = 0.01f; // singleStep + m_stepSizePrimaryRay->numTickMarks = 10; // numTickMarks + m_stepSizePrimaryRay->suffix = ""; // suffix + AddProperty(m_stepSizePrimaryRay); m_stepSizeSecondaryRay = - new FloatSliderSpinnerUiInfo(&m_appearanceDataObject.StepSizeSecondaryRay, "Appearance", "Step Size Secondary Ray"); - m_interpolate = new CheckBoxUiInfo(&m_appearanceDataObject.Interpolate, "Appearance", "Interpolate"); - m_backgroundColor = new ColorPickerUiInfo(&m_appearanceDataObject.BackgroundColor, "Appearance", "Background Color"); - m_showBoundingBox = new CheckBoxUiInfo(&m_appearanceDataObject.ShowBoundingBox, "Appearance", "Show Bounding Box"); - m_boundingBoxColor = - new ColorPickerUiInfo(&m_appearanceDataObject.BoundingBoxColor, "Appearance", "Bounding Box Color"); - m_showScaleBar = new CheckBoxUiInfo(&m_appearanceDataObject.ShowScaleBar, "Appearance", "Show Scale Bar"); + new FloatSliderSpinnerUiInfo(&m_appearanceDataObject.StepSizeSecondaryRay, category, "Step Size Secondary Ray"); + m_stepSizeSecondaryRay->SetToolTip("Set volume ray march step size for scattered rays"); + m_stepSizeSecondaryRay->SetStatusTip("Set volume ray march step size for scattered rays"); + m_stepSizeSecondaryRay->min = 1.0f; + m_stepSizeSecondaryRay->max = 100.0f; + m_stepSizeSecondaryRay->decimals = 3; // decimals + m_stepSizeSecondaryRay->singleStep = 0.01f; // singleStep + m_stepSizeSecondaryRay->numTickMarks = 10; // numTickMarks + m_stepSizeSecondaryRay->suffix = ""; // suffix + AddProperty(m_stepSizeSecondaryRay); + m_interpolate = new CheckBoxUiInfo(&m_appearanceDataObject.Interpolate, category, "Interpolate"); + m_interpolate->SetToolTip("Interpolated volume sampling"); + m_interpolate->SetStatusTip("Interpolated volume sampling"); + AddProperty(m_interpolate); + m_backgroundColor = new ColorPickerUiInfo(&m_appearanceDataObject.BackgroundColor, category, "Background Color"); + m_backgroundColor->SetToolTip("Set background color"); + m_backgroundColor->SetStatusTip("Set background color"); + AddProperty(m_backgroundColor); + m_showBoundingBox = new CheckBoxUiInfo(&m_appearanceDataObject.ShowBoundingBox, category, "Show Bounding Box"); + m_showBoundingBox->SetToolTip("Show/hide bounding box"); + m_showBoundingBox->SetStatusTip("Show/hide bounding box"); + AddProperty(m_showBoundingBox); + m_boundingBoxColor = new ColorPickerUiInfo(&m_appearanceDataObject.BoundingBoxColor, category, "Bounding Box Color"); + m_boundingBoxColor->SetToolTip("Set bounding box color"); + m_boundingBoxColor->SetStatusTip("Set bounding box color"); + AddProperty(m_boundingBoxColor); + m_showScaleBar = new CheckBoxUiInfo(&m_appearanceDataObject.ShowScaleBar, category, "Show Scale Bar"); + m_showScaleBar->SetToolTip("Show/hide scale bar"); + m_showScaleBar->SetStatusTip("Show/hide scale bar"); + AddProperty(m_showScaleBar); + + m_appearanceDataObject.RendererType.AddCallback( + new prtyCallbackWrapper(this, &AppearanceObject::RendererTypeChanged)); + m_appearanceDataObject.ShadingType.AddCallback( + new prtyCallbackWrapper(this, &AppearanceObject::ShadingTypeChanged)); + m_appearanceDataObject.DensityScale.AddCallback( + new prtyCallbackWrapper(this, &AppearanceObject::DensityScaleChanged)); + m_appearanceDataObject.GradientFactor.AddCallback( + new prtyCallbackWrapper(this, &AppearanceObject::GradientFactorChanged)); + m_appearanceDataObject.StepSizePrimaryRay.AddCallback( + new prtyCallbackWrapper(this, &AppearanceObject::StepSizePrimaryRayChanged)); + m_appearanceDataObject.StepSizeSecondaryRay.AddCallback( + new prtyCallbackWrapper(this, &AppearanceObject::StepSizeSecondaryRayChanged)); + m_appearanceDataObject.Interpolate.AddCallback( + new prtyCallbackWrapper(this, &AppearanceObject::InterpolateChanged)); + m_appearanceDataObject.BackgroundColor.AddCallback( + new prtyCallbackWrapper(this, &AppearanceObject::BackgroundColorChanged)); + m_appearanceDataObject.ShowBoundingBox.AddCallback( + new prtyCallbackWrapper(this, &AppearanceObject::ShowBoundingBoxChanged)); + m_appearanceDataObject.BoundingBoxColor.AddCallback( + new prtyCallbackWrapper(this, &AppearanceObject::BoundingBoxColorChanged)); + m_appearanceDataObject.ShowScaleBar.AddCallback( + new prtyCallbackWrapper(this, &AppearanceObject::ShowScaleBarChanged)); } void @@ -85,17 +167,17 @@ AppearanceObject::updatePropsFromObject() m_appearanceDataObject.StepSizeSecondaryRay.SetValue(m_renderSettings->m_RenderSettings.m_StepSizeFactorShadow); m_appearanceDataObject.Interpolate.SetValue(m_renderSettings->m_RenderSettings.m_InterpolatedVolumeSampling); } - if (m_scene) { - BackgroundColor.SetValue(glm::vec4(m_scene->m_material.m_backgroundColor[0], - m_scene->m_material.m_backgroundColor[1], - m_scene->m_material.m_backgroundColor[2], - 1.0f)); - ShowBoundingBox.SetValue(m_scene->m_material.m_showBoundingBox); - BoundingBoxColor.SetValue(glm::vec4(m_scene->m_material.m_boundingBoxColor[0], - m_scene->m_material.m_boundingBoxColor[1], - m_scene->m_material.m_boundingBoxColor[2], - 1.0f)); - ShowScaleBar.SetValue(m_scene->m_showScaleBar); + if (auto scene = m_scene.lock()) { + m_appearanceDataObject.BackgroundColor.SetValue(glm::vec4(scene->m_material.m_backgroundColor[0], + scene->m_material.m_backgroundColor[1], + scene->m_material.m_backgroundColor[2], + 1.0f)); + m_appearanceDataObject.ShowBoundingBox.SetValue(scene->m_material.m_showBoundingBox); + m_appearanceDataObject.BoundingBoxColor.SetValue(glm::vec4(scene->m_material.m_boundingBoxColor[0], + scene->m_material.m_boundingBoxColor[1], + scene->m_material.m_boundingBoxColor[2], + 1.0f)); + m_appearanceDataObject.ShowScaleBar.SetValue(scene->m_showScaleBar); } } @@ -111,17 +193,137 @@ AppearanceObject::updateObjectFromProps() m_renderSettings->m_RenderSettings.m_StepSizeFactor = m_appearanceDataObject.StepSizePrimaryRay.GetValue(); m_renderSettings->m_RenderSettings.m_StepSizeFactorShadow = m_appearanceDataObject.StepSizeSecondaryRay.GetValue(); m_renderSettings->m_RenderSettings.m_InterpolatedVolumeSampling = m_appearanceDataObject.Interpolate.GetValue(); - m_scene->m_material.m_backgroundColor[0] = m_appearanceDataObject.BackgroundColor.GetValue().x; - m_scene->m_material.m_backgroundColor[1] = m_appearanceDataObject.BackgroundColor.GetValue().y; - m_scene->m_material.m_backgroundColor[2] = m_appearanceDataObject.BackgroundColor.GetValue().z; - m_scene->m_material.m_showBoundingBox = m_appearanceDataObject.ShowBoundingBox.GetValue(); - m_scene->m_material.m_boundingBoxColor[0] = m_appearanceDataObject.BoundingBoxColor.GetValue().x; - m_scene->m_material.m_boundingBoxColor[1] = m_appearanceDataObject.BoundingBoxColor.GetValue().y; - m_scene->m_material.m_boundingBoxColor[2] = m_appearanceDataObject.BoundingBoxColor.GetValue().z; - m_scene->m_showScaleBar = m_appearanceDataObject.ShowScaleBar.GetValue(); - + if (auto scene = m_scene.lock()) { + scene->m_material.m_backgroundColor[0] = m_appearanceDataObject.BackgroundColor.GetValue().x; + scene->m_material.m_backgroundColor[1] = m_appearanceDataObject.BackgroundColor.GetValue().y; + scene->m_material.m_backgroundColor[2] = m_appearanceDataObject.BackgroundColor.GetValue().z; + scene->m_material.m_showBoundingBox = m_appearanceDataObject.ShowBoundingBox.GetValue(); + scene->m_material.m_boundingBoxColor[0] = m_appearanceDataObject.BoundingBoxColor.GetValue().x; + scene->m_material.m_boundingBoxColor[1] = m_appearanceDataObject.BoundingBoxColor.GetValue().y; + scene->m_material.m_boundingBoxColor[2] = m_appearanceDataObject.BoundingBoxColor.GetValue().z; + scene->m_showScaleBar = m_appearanceDataObject.ShowScaleBar.GetValue(); + } m_renderSettings->m_DirtyFlags.SetFlag(RenderParamsDirty); m_renderSettings->m_DirtyFlags.SetFlag(TransferFunctionDirty); m_renderSettings->m_DirtyFlags.SetFlag(LightsDirty); } -} \ No newline at end of file +} + +void +AppearanceObject::RendererTypeChanged(prtyProperty* i_Property, bool i_bDirty) +{ + LOG_ERROR << "Renderer type changed, but not implemented yet!"; +} +void +AppearanceObject::ShadingTypeChanged(prtyProperty* i_Property, bool i_bDirty) +{ + m_renderSettings->m_RenderSettings.m_ShadingType = m_appearanceDataObject.ShadingType.GetValue(); + m_renderSettings->m_DirtyFlags.SetFlag(RenderParamsDirty); +} +void +AppearanceObject::DensityScaleChanged(prtyProperty* i_Property, bool i_bDirty) +{ + m_renderSettings->m_RenderSettings.m_DensityScale = m_appearanceDataObject.DensityScale.GetValue(); + m_renderSettings->m_DirtyFlags.SetFlag(RenderParamsDirty); +} +void +AppearanceObject::GradientFactorChanged(prtyProperty* i_Property, bool i_bDirty) +{ + m_renderSettings->m_RenderSettings.m_GradientFactor = m_appearanceDataObject.GradientFactor.GetValue(); + m_renderSettings->m_DirtyFlags.SetFlag(RenderParamsDirty); +} +void +AppearanceObject::StepSizePrimaryRayChanged(prtyProperty* i_Property, bool i_bDirty) +{ + m_renderSettings->m_RenderSettings.m_StepSizeFactor = m_appearanceDataObject.StepSizePrimaryRay.GetValue(); + m_renderSettings->m_DirtyFlags.SetFlag(RenderParamsDirty); +} +void +AppearanceObject::StepSizeSecondaryRayChanged(prtyProperty* i_Property, bool i_bDirty) +{ + m_renderSettings->m_RenderSettings.m_StepSizeFactorShadow = m_appearanceDataObject.StepSizeSecondaryRay.GetValue(); + m_renderSettings->m_DirtyFlags.SetFlag(RenderParamsDirty); +} +void +AppearanceObject::InterpolateChanged(prtyProperty* i_Property, bool i_bDirty) +{ + m_renderSettings->m_RenderSettings.m_InterpolatedVolumeSampling = m_appearanceDataObject.Interpolate.GetValue(); + m_renderSettings->m_DirtyFlags.SetFlag(RenderParamsDirty); +} +void +AppearanceObject::BackgroundColorChanged(prtyProperty* i_Property, bool i_bDirty) +{ + if (auto scene = m_scene.lock()) { + scene->m_material.m_backgroundColor[0] = m_appearanceDataObject.BackgroundColor.GetValue().x; + scene->m_material.m_backgroundColor[1] = m_appearanceDataObject.BackgroundColor.GetValue().y; + scene->m_material.m_backgroundColor[2] = m_appearanceDataObject.BackgroundColor.GetValue().z; + m_renderSettings->m_DirtyFlags.SetFlag(EnvironmentDirty); + } +} +void +AppearanceObject::ShowBoundingBoxChanged(prtyProperty* i_Property, bool i_bDirty) +{ + if (auto scene = m_scene.lock()) { + scene->m_material.m_showBoundingBox = m_appearanceDataObject.ShowBoundingBox.GetValue(); + m_renderSettings->m_DirtyFlags.SetFlag(EnvironmentDirty); + } +} +void +AppearanceObject::BoundingBoxColorChanged(prtyProperty* i_Property, bool i_bDirty) +{ + if (auto scene = m_scene.lock()) { + scene->m_material.m_boundingBoxColor[0] = m_appearanceDataObject.BoundingBoxColor.GetValue().x; + scene->m_material.m_boundingBoxColor[1] = m_appearanceDataObject.BoundingBoxColor.GetValue().y; + scene->m_material.m_boundingBoxColor[2] = m_appearanceDataObject.BoundingBoxColor.GetValue().z; + m_renderSettings->m_DirtyFlags.SetFlag(EnvironmentDirty); + } +} +void +AppearanceObject::ShowScaleBarChanged(prtyProperty* i_Property, bool i_bDirty) +{ + if (auto scene = m_scene.lock()) { + scene->m_showScaleBar = m_appearanceDataObject.ShowScaleBar.GetValue(); + m_renderSettings->m_DirtyFlags.SetFlag(EnvironmentDirty); + } +} + +void +AppearanceObject::fromDocument(docReader* reader) +{ + // Peek at metadata + uint32_t version = reader->peekVersion(); + std::string type = reader->peekObjectType(); + std::string name = reader->peekObjectName(); + + reader->readPrty(&m_appearanceDataObject.RendererType); + reader->readPrty(&m_appearanceDataObject.ShadingType); + reader->readPrty(&m_appearanceDataObject.DensityScale); + reader->readPrty(&m_appearanceDataObject.GradientFactor); + reader->readPrty(&m_appearanceDataObject.StepSizePrimaryRay); + reader->readPrty(&m_appearanceDataObject.StepSizeSecondaryRay); + reader->readPrty(&m_appearanceDataObject.Interpolate); + reader->readPrty(&m_appearanceDataObject.BackgroundColor); + reader->readPrty(&m_appearanceDataObject.ShowBoundingBox); + reader->readPrty(&m_appearanceDataObject.BoundingBoxColor); + reader->readPrty(&m_appearanceDataObject.ShowScaleBar); +} + +void +AppearanceObject::toDocument(docWriter* writer) +{ + writer->beginObject("appearance0", "AppearanceObject", AppearanceObject::CURRENT_VERSION); + + m_appearanceDataObject.RendererType.Write(*writer); + m_appearanceDataObject.ShadingType.Write(*writer); + m_appearanceDataObject.DensityScale.Write(*writer); + m_appearanceDataObject.GradientFactor.Write(*writer); + m_appearanceDataObject.StepSizePrimaryRay.Write(*writer); + m_appearanceDataObject.StepSizeSecondaryRay.Write(*writer); + m_appearanceDataObject.Interpolate.Write(*writer); + m_appearanceDataObject.BackgroundColor.Write(*writer); + m_appearanceDataObject.ShowBoundingBox.Write(*writer); + m_appearanceDataObject.BoundingBoxColor.Write(*writer); + m_appearanceDataObject.ShowScaleBar.Write(*writer); + + writer->endObject(); +} diff --git a/renderlib/AppearanceObject.hpp b/renderlib/AppearanceObject.hpp index 49e1321c..5c34f207 100644 --- a/renderlib/AppearanceObject.hpp +++ b/renderlib/AppearanceObject.hpp @@ -3,8 +3,12 @@ #include "AppearanceDataObject.hpp" #include "core/prty/prtyObject.hpp" #include "RenderSettings.h" +#include "AppScene.h" #include "uiInfo.hpp" +class docReader; +class docWriter; + struct AppearanceUiDescription { static ComboBoxUiInfo m_rendererType; @@ -29,7 +33,7 @@ class AppearanceObject : public prtyObject void updateObjectFromProps(); // Getter for appearance data object - // AppearanceDataObject& getAppearanceDataObject() { return m_appearanceDataObject; } + AppearanceDataObject& appearanceDataObject() { return m_appearanceDataObject; } const AppearanceDataObject& getAppearanceDataObject() const { return m_appearanceDataObject; } // Getters for UI info objects @@ -48,12 +52,18 @@ class AppearanceObject : public prtyObject // Getter for the rendersettings std::shared_ptr getRenderSettings() const { return m_renderSettings; } + // document reading and writing; TODO consider an abstract base class to enforce commonality + static constexpr uint32_t CURRENT_VERSION = 1; + void fromDocument(docReader* reader); + void toDocument(docWriter* writer); + private: // the properties AppearanceDataObject m_appearanceDataObject; - // the actual camera + // the actual settings std::shared_ptr m_renderSettings; + std::weak_ptr m_scene; // the ui info ComboBoxUiInfo* m_rendererType; @@ -67,4 +77,16 @@ class AppearanceObject : public prtyObject CheckBoxUiInfo* m_showBoundingBox; ColorPickerUiInfo* m_boundingBoxColor; CheckBoxUiInfo* m_showScaleBar; + + void RendererTypeChanged(prtyProperty* i_Property, bool i_bDirty); + void ShadingTypeChanged(prtyProperty* i_Property, bool i_bDirty); + void DensityScaleChanged(prtyProperty* i_Property, bool i_bDirty); + void GradientFactorChanged(prtyProperty* i_Property, bool i_bDirty); + void StepSizePrimaryRayChanged(prtyProperty* i_Property, bool i_bDirty); + void StepSizeSecondaryRayChanged(prtyProperty* i_Property, bool i_bDirty); + void InterpolateChanged(prtyProperty* i_Property, bool i_bDirty); + void BackgroundColorChanged(prtyProperty* i_Property, bool i_bDirty); + void ShowBoundingBoxChanged(prtyProperty* i_Property, bool i_bDirty); + void BoundingBoxColorChanged(prtyProperty* i_Property, bool i_bDirty); + void ShowScaleBarChanged(prtyProperty* i_Property, bool i_bDirty); }; diff --git a/renderlib/AreaLightObject.cpp b/renderlib/AreaLightObject.cpp index 434037b5..03a53f23 100644 --- a/renderlib/AreaLightObject.cpp +++ b/renderlib/AreaLightObject.cpp @@ -4,6 +4,9 @@ #include "MathUtil.h" #include "Logging.h" +#include "serialize/docReader.h" +#include "serialize/docWriter.h" + AreaLightObject::AreaLightObject() : prtyObject() { @@ -156,3 +159,34 @@ AreaLightObject::ColorChanged(prtyProperty* i_Property, bool i_bDirty) { updateSceneLightFromProps(); } + +void +AreaLightObject::fromDocument(docReader* reader) +{ + // Peek at metadata + uint32_t version = reader->peekVersion(); + std::string type = reader->peekObjectType(); + std::string name = reader->peekObjectName(); + + reader->readPrty(&m_arealightDataObject.Theta); + reader->readPrty(&m_arealightDataObject.Phi); + reader->readPrty(&m_arealightDataObject.Size); + reader->readPrty(&m_arealightDataObject.Distance); + reader->readPrty(&m_arealightDataObject.Intensity); + reader->readPrty(&m_arealightDataObject.Color); +} + +void +AreaLightObject::toDocument(docWriter* writer) +{ + writer->beginObject("areaLight0", "AreaLightObject", AreaLightObject::CURRENT_VERSION); + + m_arealightDataObject.Theta.Write(*writer); + m_arealightDataObject.Phi.Write(*writer); + m_arealightDataObject.Size.Write(*writer); + m_arealightDataObject.Distance.Write(*writer); + m_arealightDataObject.Intensity.Write(*writer); + m_arealightDataObject.Color.Write(*writer); + + writer->endObject(); +} diff --git a/renderlib/AreaLightObject.hpp b/renderlib/AreaLightObject.hpp index 036f1025..ec216421 100644 --- a/renderlib/AreaLightObject.hpp +++ b/renderlib/AreaLightObject.hpp @@ -17,7 +17,7 @@ class ArealightDataObject ArealightDataObject() = default; prtyFloat Theta{ "Theta", 0.0f }; - prtyFloat Phi{ "Phi", 1.5708f }; // PI/2 + prtyFloat Phi{ "Phi", 90.0f }; // PI/2 prtyFloat Size{ "Size", 1.0f }; prtyFloat Distance{ "Distance", 10.0f }; prtyFloat Intensity{ "Intensity", 100.0f }; @@ -37,6 +37,10 @@ class AreaLightObject : public prtyObject // Update scene light instance from properties (called automatically via callbacks) void updateSceneLightFromProps(); + // Getter for data object + ArealightDataObject& getDataObject() { return m_arealightDataObject; } + const ArealightDataObject& getDataObject() const { return m_arealightDataObject; } + // Property change callbacks void ThetaChanged(prtyProperty* i_Property, bool i_bDirty); void PhiChanged(prtyProperty* i_Property, bool i_bDirty); @@ -47,6 +51,11 @@ class AreaLightObject : public prtyObject void setDirtyCallback(std::function callback) { m_dirtyCallback = callback; } + // document reading and writing; TODO consider an abstract base class to enforce commonality + static constexpr uint32_t CURRENT_VERSION = 1; + void fromDocument(docReader* reader); + void toDocument(docWriter* writer); + private: ArealightDataObject m_arealightDataObject; diff --git a/renderlib/CMakeLists.txt b/renderlib/CMakeLists.txt index 668adafb..b1ac2a23 100644 --- a/renderlib/CMakeLists.txt +++ b/renderlib/CMakeLists.txt @@ -36,6 +36,10 @@ target_sources(renderlib PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/CameraDataObject.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/CameraObject.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/CameraObject.hpp" + "${CMAKE_CURRENT_SOURCE_DIR}/ChannelSettingsDataObject.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/ChannelSettingsDataObject.hpp" + "${CMAKE_CURRENT_SOURCE_DIR}/ClipPlaneObject.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/ClipPlaneObject.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/ClipPlaneTool.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/ClipPlaneTool.h" "${CMAKE_CURRENT_SOURCE_DIR}/Colormap.cpp" @@ -106,6 +110,7 @@ add_subdirectory(graphics) add_subdirectory(io) add_subdirectory(pugixml) add_subdirectory(core) +add_subdirectory(serialize) target_link_libraries(renderlib Qt::Core Qt::OpenGL diff --git a/renderlib/CameraDataObject.cpp b/renderlib/CameraDataObject.cpp index 461b8a67..d81aa493 100644 --- a/renderlib/CameraDataObject.cpp +++ b/renderlib/CameraDataObject.cpp @@ -1,3 +1 @@ #include "CameraDataObject.hpp" - -#include "Logging.h" diff --git a/renderlib/CameraDataObject.hpp b/renderlib/CameraDataObject.hpp index a26c862c..1e4eb095 100644 --- a/renderlib/CameraDataObject.hpp +++ b/renderlib/CameraDataObject.hpp @@ -1,18 +1,37 @@ #pragma once #include "core/prty/prtyFloat.hpp" -#include "core/prty/prtyInt8.hpp" +#include "core/prty/prtyEnum.hpp" #include "core/prty/prtyBoolean.hpp" +#include "core/prty/prtyVector3d.hpp" class CameraDataObject { public: - CameraDataObject() {} + CameraDataObject() + { + ExposureIterations.SetEnumTag(0, "1"); + ExposureIterations.SetEnumTag(1, "2"); + ExposureIterations.SetEnumTag(2, "4"); + ExposureIterations.SetEnumTag(3, "8"); + + ProjectionMode.SetEnumTag(0, "Perspective"); + ProjectionMode.SetEnumTag(1, "Orthographic"); + } prtyFloat Exposure{ "Exposure", 0.75f }; - prtyInt8 ExposureIterations{ "ExposureIterations", 1 }; + prtyEnum ExposureIterations{ "ExposureIterations", 0 }; prtyBoolean NoiseReduction{ "NoiseReduction", false }; prtyFloat ApertureSize{ "ApertureSize", 0.0f }; - prtyFloat FieldOfView{ "FieldOfView", 30.0f }; + prtyFloat FieldOfView{ "FieldOfView", 30.0f }; // degrees prtyFloat FocalDistance{ "FocalDistance", 0.0f }; + + prtyVector3d Position{ "Position", glm::vec3(0.0f, 0.0f, 0.0f) }; + prtyVector3d Target{ "Target", glm::vec3(0.0f, 0.0f, -1.0f) }; + prtyFloat NearPlane{ "NearPlane", 0.1f }; + prtyFloat FarPlane{ "FarPlane", 1000.0f }; + prtyFloat Roll{ "Roll", 0.0f }; // tilt angle in degrees + + prtyFloat OrthoScale{ "OrthoScale", 1.0f }; // orthographic scale for orthographic projection + prtyEnum ProjectionMode{ "ProjectionMode", 0 }; // 0 = perspective, 1 = orthographic }; diff --git a/renderlib/CameraObject.cpp b/renderlib/CameraObject.cpp index d45b5def..1705ca2d 100644 --- a/renderlib/CameraObject.cpp +++ b/renderlib/CameraObject.cpp @@ -1,68 +1,111 @@ -#include "CameraUiDescription.hpp" - -// FloatSliderSpinnerUiInfo CameraUiDescription::m_exposure("Exposure", -// "Set Exposure", -// "Set camera exposure", -// 0.0f, -// 1.0f, -// 2, // decimals -// 0.01, // singleStep -// 0 // numTickMarks -// ); -// ComboBoxUiInfo CameraUiDescription::m_exposureIterations("Exposure Time", -// "Set Exposure Time", -// "Set number of samples to accumulate per viewport update", -// { "1", "2", "4", "8" }); -// CheckBoxUiInfo CameraUiDescription::m_noiseReduction("Noise Reduction", -// "Enable denoising pass", -// "Enable denoising pass"); -// FloatSliderSpinnerUiInfo CameraUiDescription::m_apertureSize("Aperture Size", -// "Set camera aperture size", -// "Set camera aperture size", -// 0.0f, -// 0.1f, -// 2, // decimals -// 0.01, // singleStep -// 0, // numTickMarks -// " mm"); -// FloatSliderSpinnerUiInfo CameraUiDescription::m_fieldOfView("Field of view", -// "Set camera field of view angle", -// "Set camera field of view angle", -// 10.0f, -// 150.0f, -// 2, // decimals -// 0.01, // singleStep -// 0, // numTickMarks -// " deg"); -// FloatSliderSpinnerUiInfo CameraUiDescription::m_focalDistance("Focal distance", -// "Set focal distance", -// "Set focal distance", -// 0.0f, -// 15.0f, -// 2, // decimals -// 0.01, // singleStep -// 0, // numTickMarks -// " m"); +#include "CameraObject.hpp" + +#include "Logging.h" +#include "serialize/docReader.h" +#include "serialize/docWriter.h" +#include "glm.h" CameraObject::CameraObject() : prtyObject() { m_camera = std::make_shared(); m_ExposureUIInfo = new FloatSliderSpinnerUiInfo(&m_cameraDataObject.Exposure, "Camera", "Exposure"); + m_ExposureUIInfo->SetToolTip("Set Exposure"); + m_ExposureUIInfo->SetStatusTip("Set camera exposure"); + m_ExposureUIInfo->min = 0.0f; + m_ExposureUIInfo->max = 1.0f; + m_ExposureUIInfo->decimals = 2; // decimals + m_ExposureUIInfo->singleStep = 0.01f; // singleStep + m_ExposureUIInfo->numTickMarks = 0; // numTickMarks + AddProperty(m_ExposureUIInfo); m_ExposureIterationsUIInfo = new ComboBoxUiInfo(&m_cameraDataObject.ExposureIterations, "Camera", "Exposure Iterations"); + m_ExposureIterationsUIInfo->SetToolTip("Set Exposure Iterations"); + m_ExposureIterationsUIInfo->SetStatusTip("Set number of samples to accumulate per viewport update"); + AddProperty(m_ExposureIterationsUIInfo); m_NoiseReductionUIInfo = new CheckBoxUiInfo(&m_cameraDataObject.NoiseReduction, "Camera", "Noise Reduction"); + m_NoiseReductionUIInfo->SetToolTip("Enable Noise Reduction"); + m_NoiseReductionUIInfo->SetStatusTip("Enable denoising pass"); + AddProperty(m_NoiseReductionUIInfo); m_ApertureSizeUIInfo = new FloatSliderSpinnerUiInfo(&m_cameraDataObject.ApertureSize, "Camera", "Aperture Size"); + m_ApertureSizeUIInfo->SetToolTip("Set Aperture Size"); + m_ApertureSizeUIInfo->SetStatusTip("Set camera aperture size"); + m_ApertureSizeUIInfo->min = 0.0f; + m_ApertureSizeUIInfo->max = 0.1f; + m_ApertureSizeUIInfo->decimals = 2; // decimals + m_ApertureSizeUIInfo->singleStep = 0.01f; // + m_ApertureSizeUIInfo->numTickMarks = 0; // numTickMarks + m_ApertureSizeUIInfo->suffix = " mm"; // suffix + AddProperty(m_ApertureSizeUIInfo); m_FieldOfViewUIInfo = new FloatSliderSpinnerUiInfo(&m_cameraDataObject.FieldOfView, "Camera", "Field of View"); + m_FieldOfViewUIInfo->SetToolTip("Set Field of View"); + m_FieldOfViewUIInfo->SetStatusTip("Set camera field of view angle"); + m_FieldOfViewUIInfo->min = 10.0f; + m_FieldOfViewUIInfo->max = 150.0f; + m_FieldOfViewUIInfo->decimals = 2; // decimals + m_FieldOfViewUIInfo->singleStep = 0.01f; // single + m_FieldOfViewUIInfo->numTickMarks = 0; // numTickMarks + m_FieldOfViewUIInfo->suffix = " deg"; // suffix + AddProperty(m_FieldOfViewUIInfo); m_FocalDistanceUIInfo = new FloatSliderSpinnerUiInfo(&m_cameraDataObject.FocalDistance, "Camera", "Focal Distance"); + m_FocalDistanceUIInfo->SetToolTip("Set Focal Distance"); + m_FocalDistanceUIInfo->SetStatusTip("Set focal distance"); + m_FocalDistanceUIInfo->min = 0.0f; + m_FocalDistanceUIInfo->max = 15.0f; + m_FocalDistanceUIInfo->decimals = 2; // decimals + m_FocalDistanceUIInfo->singleStep = 0.01f; // single + m_FocalDistanceUIInfo->numTickMarks = 0; // numTickMarks + m_FocalDistanceUIInfo->suffix = " m"; // suffix + AddProperty(m_FocalDistanceUIInfo); + + m_cameraDataObject.Exposure.AddCallback(new prtyCallbackWrapper(this, &CameraObject::ExposureChanged)); + m_cameraDataObject.ExposureIterations.AddCallback( + new prtyCallbackWrapper(this, &CameraObject::ExposureIterationsChanged)); + m_cameraDataObject.NoiseReduction.AddCallback( + new prtyCallbackWrapper(this, &CameraObject::NoiseReductionChanged)); + m_cameraDataObject.ApertureSize.AddCallback( + new prtyCallbackWrapper(this, &CameraObject::ApertureSizeChanged)); + m_cameraDataObject.FieldOfView.AddCallback( + new prtyCallbackWrapper(this, &CameraObject::FieldOfViewChanged)); + m_cameraDataObject.FocalDistance.AddCallback( + new prtyCallbackWrapper(this, &CameraObject::FocalDistanceChanged)); + + m_cameraDataObject.Position.AddCallback( + new prtyCallbackWrapper(this, &CameraObject::TransformationChanged)); + m_cameraDataObject.Target.AddCallback( + new prtyCallbackWrapper(this, &CameraObject::TransformationChanged)); + m_cameraDataObject.Roll.AddCallback( + new prtyCallbackWrapper(this, &CameraObject::TransformationChanged)); } void CameraObject::updatePropsFromObject() { + // TODO FIXME if we set everything through props, then this is not needed. if (m_camera) { m_cameraDataObject.Exposure.SetValue(1.0f - m_camera->m_Film.m_Exposure); - m_cameraDataObject.ExposureIterations.SetValue(m_camera->m_Film.m_ExposureIterations); + + uint8_t exposureIterationsValue = m_camera->m_Film.m_ExposureIterations; + // convert m_camera->m_Film.m_ExposureIterations to string + // and then find the corresponding index in the enum + switch (m_camera->m_Film.m_ExposureIterations) { + case 1: + exposureIterationsValue = 0; + break; + case 2: + exposureIterationsValue = 1; + break; + case 4: + exposureIterationsValue = 2; + break; + case 8: + exposureIterationsValue = 3; + break; + default: + LOG_ERROR << "Invalid Exposure Iterations: " << m_camera->m_Film.m_ExposureIterations; + exposureIterationsValue = 0; // default to 1 + } + m_cameraDataObject.ExposureIterations.SetValue(exposureIterationsValue); // TODO this is not hooked up to the camera properly // m_cameraDataObject.NoiseReduction.SetValue(m_camera->m_Film.m_NoiseReduction); m_cameraDataObject.ApertureSize.SetValue(m_camera->m_Aperture.m_Size); @@ -70,13 +113,33 @@ CameraObject::updatePropsFromObject() m_cameraDataObject.FocalDistance.SetValue(m_camera->m_Focus.m_FocalDistance); } } +uint8_t +CameraObject::GetExposureIterationsValue(int i_ComboBoxIndex) +{ + // Convert the combo box index to the corresponding exposure iterations value + switch (i_ComboBoxIndex) { + case 0: + return 1; // 1 iteration + case 1: + return 2; // 2 iterations + case 2: + return 4; // 4 iterations + case 3: + return 8; // 8 iterations + default: + LOG_ERROR << "Invalid Exposure Iterations index: " << i_ComboBoxIndex; + return 1; // default to 1 iteration + } +} + void CameraObject::updateObjectFromProps() { // update low-level camera object from properties if (m_camera) { m_camera->m_Film.m_Exposure = 1.0f - m_cameraDataObject.Exposure.GetValue(); - m_camera->m_Film.m_ExposureIterations = m_cameraDataObject.ExposureIterations.GetValue(); + uint8_t exposureIterationsValue = GetExposureIterationsValue(m_cameraDataObject.ExposureIterations.GetValue()); + m_camera->m_Film.m_ExposureIterations = exposureIterationsValue; // Aperture m_camera->m_Aperture.m_Size = m_cameraDataObject.ApertureSize.GetValue(); @@ -106,7 +169,7 @@ CameraObject::ExposureChanged(prtyProperty* i_Property, bool i_bDirty) void CameraObject::ExposureIterationsChanged(prtyProperty* i_Property, bool i_bDirty) { - m_camera->m_Film.m_ExposureIterations = m_cameraDataObject.ExposureIterations.GetValue(); + m_camera->m_Film.m_ExposureIterations = GetExposureIterationsValue(m_cameraDataObject.ExposureIterations.GetValue()); m_camera->m_Dirty = true; } void @@ -132,3 +195,70 @@ CameraObject::FocalDistanceChanged(prtyProperty* i_Property, bool i_bDirty) m_camera->m_Focus.m_FocalDistance = m_cameraDataObject.FocalDistance.GetValue(); m_camera->m_Dirty = true; } + +//-------------------------------------------------------------------- +// common code when a property related to position of light is changed +//-------------------------------------------------------------------- +void +CameraObject::TransformationChanged(prtyProperty* i_Property, bool i_bDirty) +{ + // Rotate up vector through tilt angle + glm::vec3 pos, target; + // assumes world space. + pos = m_cameraDataObject.Position.GetValue(); + target = m_cameraDataObject.Target.GetValue(); + glm::vec3 up = glm::vec3(0, 1, 0); // default up vector + // Rotate the up vector around the vector from position to target + // using the roll angle (tilt angle) + up = glm::rotate(up, DEG_TO_RAD * m_cameraDataObject.Roll.GetValue(), target - pos); + + m_camera->m_From = pos; + m_camera->m_Target = target; + m_camera->m_Up = up; + m_camera->m_Dirty = true; +} + +void +CameraObject::fromDocument(docReader* reader) +{ + // Peek at metadata + uint32_t version = reader->peekVersion(); + std::string type = reader->peekObjectType(); + std::string name = reader->peekObjectName(); + + reader->readPrty(&m_cameraDataObject.Exposure); + reader->readPrty(&m_cameraDataObject.ExposureIterations); + reader->readPrty(&m_cameraDataObject.NoiseReduction); + reader->readPrty(&m_cameraDataObject.ApertureSize); + reader->readPrty(&m_cameraDataObject.FieldOfView); + reader->readPrty(&m_cameraDataObject.FocalDistance); + reader->readPrty(&m_cameraDataObject.Position); + reader->readPrty(&m_cameraDataObject.Target); + reader->readPrty(&m_cameraDataObject.NearPlane); + reader->readPrty(&m_cameraDataObject.FarPlane); + reader->readPrty(&m_cameraDataObject.Roll); + reader->readPrty(&m_cameraDataObject.OrthoScale); + reader->readPrty(&m_cameraDataObject.ProjectionMode); +} +void +CameraObject::toDocument(docWriter* writer) +{ + writer->beginObject("camera0", "CameraObject", CameraObject::CURRENT_VERSION); + + m_cameraDataObject.Exposure.Write(*writer); + m_cameraDataObject.ExposureIterations.Write(*writer); + m_cameraDataObject.NoiseReduction.Write(*writer); + m_cameraDataObject.ApertureSize.Write(*writer); + m_cameraDataObject.FieldOfView.Write(*writer); + m_cameraDataObject.FocalDistance.Write(*writer); + + m_cameraDataObject.Position.Write(*writer); + m_cameraDataObject.Target.Write(*writer); + m_cameraDataObject.NearPlane.Write(*writer); + m_cameraDataObject.FarPlane.Write(*writer); + m_cameraDataObject.Roll.Write(*writer); + m_cameraDataObject.OrthoScale.Write(*writer); + m_cameraDataObject.ProjectionMode.Write(*writer); + + writer->endObject(); +} diff --git a/renderlib/CameraObject.hpp b/renderlib/CameraObject.hpp index b51a7e03..91cec842 100644 --- a/renderlib/CameraObject.hpp +++ b/renderlib/CameraObject.hpp @@ -5,15 +5,8 @@ #include "core/prty/prtyObject.hpp" #include "CCamera.h" -struct CameraUiDescription -{ - static FloatSliderSpinnerUiInfo m_exposure; - static ComboBoxUiInfo m_exposureIterations; - static CheckBoxUiInfo m_noiseReduction; - static FloatSliderSpinnerUiInfo m_apertureSize; - static FloatSliderSpinnerUiInfo m_fieldOfView; - static FloatSliderSpinnerUiInfo m_focalDistance; -}; +class docReader; +class docWriter; class CameraObject : public prtyObject { @@ -24,7 +17,7 @@ class CameraObject : public prtyObject void updateObjectFromProps(); // Getter for camera data object - // CameraDataObject& getCameraDataObject() { return m_cameraDataObject; } + CameraDataObject& getCameraDataObject() { return m_cameraDataObject; } const CameraDataObject& getCameraDataObject() const { return m_cameraDataObject; } // Getters for UI info objects @@ -38,6 +31,14 @@ class CameraObject : public prtyObject // Getter for the camera std::shared_ptr getCamera() const { return m_camera; } + // Convert UI specific combo box index to a known enum type + static uint8_t GetExposureIterationsValue(int i_ComboBoxIndex); + + // document reading and writing; TODO consider an abstract base class to enforce commonality + static constexpr uint32_t CURRENT_VERSION = 1; + void fromDocument(docReader* reader); + void toDocument(docWriter* writer); + private: // the properties CameraDataObject m_cameraDataObject; @@ -52,4 +53,12 @@ class CameraObject : public prtyObject FloatSliderSpinnerUiInfo* m_ApertureSizeUIInfo; FloatSliderSpinnerUiInfo* m_FieldOfViewUIInfo; FloatSliderSpinnerUiInfo* m_FocalDistanceUIInfo; + + void ExposureChanged(prtyProperty* i_Property, bool i_bDirty); + void ExposureIterationsChanged(prtyProperty* i_Property, bool i_bDirty); + void NoiseReductionChanged(prtyProperty* i_Property, bool i_bDirty); + void ApertureSizeChanged(prtyProperty* i_Property, bool i_bDirty); + void FieldOfViewChanged(prtyProperty* i_Property, bool i_bDirty); + void FocalDistanceChanged(prtyProperty* i_Property, bool i_bDirty); + void TransformationChanged(prtyProperty* i_Property, bool i_bDirty); }; diff --git a/renderlib/ChannelSettingsDataObject.cpp b/renderlib/ChannelSettingsDataObject.cpp new file mode 100644 index 00000000..0fde0daf --- /dev/null +++ b/renderlib/ChannelSettingsDataObject.cpp @@ -0,0 +1 @@ +#include "ChannelSettingsDataObject.hpp" diff --git a/renderlib/ChannelSettingsDataObject.hpp b/renderlib/ChannelSettingsDataObject.hpp new file mode 100644 index 00000000..48403608 --- /dev/null +++ b/renderlib/ChannelSettingsDataObject.hpp @@ -0,0 +1,109 @@ +#pragma once + +#include "Colormap.h" +#include "GradientData.h" +#include "core/prty/prtyColor.hpp" +#include "core/prty/prtyFloat.hpp" +#include "core/prty/prtyEnum.hpp" +#include "core/prty/prtyBoolean.hpp" +#include "core/prty/prtyVector3d.hpp" +#include "core/prty/prtyText.hpp" + +class prtyControlPointVector + : public prtyPropertyTemplate, const std::vector&> +{ +public: + prtyControlPointVector(const std::string& i_Name) + : prtyPropertyTemplate, const std::vector&>( + i_Name, + std::vector{ { 0.0f, 0.0f }, { 1.0f, 1.0f } }) + { + } + prtyControlPointVector(const std::string& i_Name, const std::vector& i_InitialValue) + : prtyPropertyTemplate, const std::vector&>(i_Name, i_InitialValue) + { + } + + virtual const char* GetType() override { return "ControlPointVector"; } + virtual void Read(docReader& io_Reader) override + { + // Implement reading from a reader if needed + } + virtual void Write(docWriter& io_Writer) const override + { + // Implement writing to a writer if needed + } +}; + +class prtyColorControlPointVector + : public prtyPropertyTemplate, const std::vector&> +{ +public: + prtyColorControlPointVector(const std::string& i_Name) + : prtyPropertyTemplate, const std::vector&>( + i_Name, + std::vector{ { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }, { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f } }) + { + } + prtyColorControlPointVector(const std::string& i_Name, const std::vector& i_InitialValue) + : prtyPropertyTemplate, const std::vector&>(i_Name, + i_InitialValue) + { + } + + virtual const char* GetType() override { return "ColorControlPointVector"; } + virtual void Read(docReader& io_Reader) override + { + // Implement reading from a reader if needed + } + virtual void Write(docWriter& io_Writer) const override + { + // Implement writing to a writer if needed + } +}; + +class GradientDataObject +{ +public: + GradientDataObject() = default; + + // Gradient related properties can be added here + prtyEnum ActiveMode; + + prtyFloat Window{ "Window", 0.25f }; + prtyFloat Level{ "Level", 0.5f }; + prtyFloat Isovalue{ "Isovalue", 0.5f }; + prtyFloat Isorange{ "Isorange", 0.1f }; + prtyFloat PctLow{ "PctLow", 0.5f }; + prtyFloat PctHigh{ "PctHigh", 0.98f }; + prtyUint16 MaxU16{ "MaxU16", 65535 }; + prtyUint16 MinU16{ "MinU16", 0 }; + prtyControlPointVector CustomControlPoints{ "CustomControlPoints", + std::vector{ { 0.0f, 0.0f }, { 1.0f, 1.0f } } }; +}; + +class ChannelSettingsDataObject +{ +public: + ChannelSettingsDataObject() {} + + prtyColor DiffuseColor{ "DiffuseColor", glm::vec4(1.0f, 1.0f, 1.0f, 1.0f) }; + prtyColor SpecularColor{ "SpecularColor", glm::vec4(1.0f, 1.0f, 1.0f, 1.0f) }; + prtyColor EmissiveColor{ "EmissiveColor", glm::vec4(0.0f, 0.0f, 0.0f, 1.0f) }; + prtyFloat Roughness{ "Roughness", 0.5f }; + prtyFloat Opacity{ "Opacity", 1.0f }; + prtyBoolean Enabled{ "Enabled", true }; + prtyFloat Labels{ "Labels", 0.0f }; + + // a colormap has a name and a set of color control points + // std::string name = "none"; + // std::vector stops; + // where a ControlPointSettings_V1 is std::pair> + + prtyText ColormapName{ "ColormapName", ColorRamp::NO_COLORMAP_NAME }; + prtyColorControlPointVector Colormap{ "Colormap", + std::vector{ { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }, + { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f } } }; + + GradientDataObject GradientData; +}; diff --git a/renderlib/ClipPlaneObject.cpp b/renderlib/ClipPlaneObject.cpp new file mode 100644 index 00000000..e1b80c6d --- /dev/null +++ b/renderlib/ClipPlaneObject.cpp @@ -0,0 +1,123 @@ +#include "ClipPlaneObject.hpp" + +#include "ScenePlane.h" +#include "MathUtil.h" +#include "Logging.h" + +#include "serialize/docReader.h" +#include "serialize/docWriter.h" + +ClipPlaneObject::ClipPlaneObject() + : prtyObject() +{ + m_scenePlane = std::make_shared(glm::vec3(0.0f, 0.0f, 0.0f)); + + m_enabledUIInfo = new CheckBoxUiInfo(&m_clipPlaneDataObject.Enabled, "General", "Enabled"); + m_enabledUIInfo->SetToolTip("Enable or disable the clip plane"); + m_enabledUIInfo->SetStatusTip("Enable or disable the clip plane in the scene"); + AddProperty(m_enabledUIInfo); + m_showHelperUIInfo = new CheckBoxUiInfo(&m_clipPlaneDataObject.ShowHelper, "General", "Show Helper"); + m_showHelperUIInfo->SetToolTip("Show or hide the clip plane helper"); + m_showHelperUIInfo->SetStatusTip("Show or hide the clip plane helper in the scene"); + AddProperty(m_showHelperUIInfo); + + // Add callbacks for property changes + m_clipPlaneDataObject.Enabled.AddCallback( + new prtyCallbackWrapper(this, &ClipPlaneObject::EnabledChanged)); + m_clipPlaneDataObject.ShowHelper.AddCallback( + new prtyCallbackWrapper(this, &ClipPlaneObject::ShowHelperChanged)); + m_clipPlaneDataObject.Position.AddCallback( + new prtyCallbackWrapper(this, &ClipPlaneObject::PositionChanged)); + m_clipPlaneDataObject.Rotation.AddCallback( + new prtyCallbackWrapper(this, &ClipPlaneObject::RotationChanged)); +} + +void +ClipPlaneObject::updatePropsFromObject() +{ + if (!m_scenePlane) + return; + + const Plane& plane = m_scenePlane->m_plane; + + m_clipPlaneDataObject.Enabled.SetValue(m_scenePlane->m_enabled); + m_clipPlaneDataObject.ShowHelper.SetValue(m_scenePlane->getVisible()); + m_clipPlaneDataObject.Position.SetValue(m_scenePlane->m_transform.m_center); + m_clipPlaneDataObject.Rotation.SetValue(m_scenePlane->m_transform.m_rotation); +} + +void +ClipPlaneObject::updateObjectFromProps() +{ + if (!m_scenePlane) + return; + + Plane& plane = m_scenePlane->m_plane; + + // update plane + m_scenePlane->m_enabled = m_clipPlaneDataObject.Enabled.GetValue(); + m_scenePlane->setVisible(m_clipPlaneDataObject.ShowHelper.GetValue()); + m_scenePlane->m_transform.m_center = m_clipPlaneDataObject.Position.GetValue(); + m_scenePlane->m_transform.m_rotation = m_clipPlaneDataObject.Rotation.GetValue(); + m_scenePlane->updateTransform(); + + // Notify observers + for (auto& observer : m_scenePlane->m_observers) { + observer(plane); + } + + // Call dirty callback if set + if (m_dirtyCallback) { + m_dirtyCallback(); + } +} + +void +ClipPlaneObject::EnabledChanged(prtyProperty* i_Property, bool i_bDirty) +{ + updateObjectFromProps(); +} + +void +ClipPlaneObject::ShowHelperChanged(prtyProperty* i_Property, bool i_bDirty) +{ + updateObjectFromProps(); +} + +void +ClipPlaneObject::PositionChanged(prtyProperty* i_Property, bool i_bDirty) +{ + updateObjectFromProps(); +} + +void +ClipPlaneObject::RotationChanged(prtyProperty* i_Property, bool i_bDirty) +{ + updateObjectFromProps(); +} + +void +ClipPlaneObject::fromDocument(docReader* reader) +{ + // Peek at metadata + uint32_t version = reader->peekVersion(); + std::string type = reader->peekObjectType(); + std::string name = reader->peekObjectName(); + + reader->readPrty(&m_clipPlaneDataObject.Enabled); + reader->readPrty(&m_clipPlaneDataObject.ShowHelper); + reader->readPrty(&m_clipPlaneDataObject.Position); + reader->readPrty(&m_clipPlaneDataObject.Rotation); +} + +void +ClipPlaneObject::toDocument(docWriter* writer) +{ + writer->beginObject("clipPlane0", "ClipPlaneObject", ClipPlaneObject::CURRENT_VERSION); + + m_clipPlaneDataObject.Enabled.Write(*writer); + m_clipPlaneDataObject.ShowHelper.Write(*writer); + m_clipPlaneDataObject.Position.Write(*writer); + m_clipPlaneDataObject.Rotation.Write(*writer); + writer->endObject(); +} diff --git a/renderlib/ClipPlaneObject.hpp b/renderlib/ClipPlaneObject.hpp new file mode 100644 index 00000000..4b13cc14 --- /dev/null +++ b/renderlib/ClipPlaneObject.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include "core/prty/prtyObject.hpp" +#include "core/prty/prtyBoolean.hpp" +#include "core/prty/prtyRotation.hpp" +#include "core/prty/prtyVector3d.hpp" +#include "uiInfo.hpp" + +#include +#include + +class ScenePlane; + +// Data object for arealight properties +class ClipPlaneDataObject +{ +public: + ClipPlaneDataObject() = default; + + prtyBoolean Enabled{ "Enabled", true }; + prtyBoolean ShowHelper{ "ShowHelper", true }; + prtyVector3d Position{ "Position", glm::vec3(0.0f, 0.0f, 0.0f) }; + prtyRotation Rotation{ "Rotation", glm::vec3(0.0f, 0.0f, 0.0f) }; +}; + +class ClipPlaneObject : public prtyObject +{ +public: + ClipPlaneObject(); + + std::shared_ptr getScenePlane() const { return m_scenePlane; } + + // Update properties from scene plane instance + void updatePropsFromObject(); + + // Update scene plane instance from properties (called automatically via callbacks) + void updateObjectFromProps(); + + // Getter for data object + ClipPlaneDataObject& getDataObject() { return m_clipPlaneDataObject; } + const ClipPlaneDataObject& getDataObject() const { return m_clipPlaneDataObject; } + + // Property change callbacks + void EnabledChanged(prtyProperty* i_Property, bool i_bDirty); + void ShowHelperChanged(prtyProperty* i_Property, bool i_bDirty); + void PositionChanged(prtyProperty* i_Property, bool i_bDirty); + void RotationChanged(prtyProperty* i_Property, bool i_bDirty); + + void setDirtyCallback(std::function callback) { m_dirtyCallback = callback; } + + // document reading and writing; TODO consider an abstract base class to enforce commonality + static constexpr uint32_t CURRENT_VERSION = 1; + void fromDocument(docReader* reader); + void toDocument(docWriter* writer); + +private: + ClipPlaneDataObject m_clipPlaneDataObject; + + // the actual object + std::shared_ptr m_scenePlane; + + std::function m_dirtyCallback; + + // UI Info objects + CheckBoxUiInfo* m_enabledUIInfo; + CheckBoxUiInfo* m_showHelperUIInfo; +}; diff --git a/renderlib/Colormap.h b/renderlib/Colormap.h index 82836541..31419d05 100644 --- a/renderlib/Colormap.h +++ b/renderlib/Colormap.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -58,8 +59,27 @@ struct ColorControlPoint g = (rgb >> 8) & 0xff; b = (rgb >> 0) & 0xff; } + + bool operator==(const ColorControlPoint& other) const + { + return first == other.first && r == other.r && g == other.g && b == other.b && a == other.a; + } + + bool operator!=(const ColorControlPoint& other) const { return !(*this == other); } }; +// Serialization operator for ColorControlPoint vectors +inline std::ostream& +operator<<(std::ostream& os, const std::vector& f) +{ + os << "[ "; + for (const auto& point : f) { + os << "(" << point.first << ", [" << point.r << ", " << point.g << ", " << point.b << ", " << point.a << "]) "; + } + os << " ]"; + return os; +} + class ColorRamp { public: diff --git a/renderlib/GradientData.h b/renderlib/GradientData.h index f249a8e3..77ea288b 100644 --- a/renderlib/GradientData.h +++ b/renderlib/GradientData.h @@ -1,12 +1,25 @@ #pragma once #include +#include #include struct Histogram; using LutControlPoint = std::pair; +// Serialization operator for LutControlPoint vectors +inline std::ostream& +operator<<(std::ostream& os, const std::vector& f) +{ + os << "[ "; + for (const auto& point : f) { + os << "(" << point.first << ", " << point.second << ") "; + } + os << " ]"; + return os; +} + enum class GradientEditMode { WINDOW_LEVEL, diff --git a/renderlib/ScenePlane.h b/renderlib/ScenePlane.h index 7fabbf6c..edf83076 100644 --- a/renderlib/ScenePlane.h +++ b/renderlib/ScenePlane.h @@ -29,4 +29,5 @@ class ScenePlane : public SceneObject // should the tool be showing? void setVisible(bool v); + bool getVisible() const { return m_tool->m_visible; } }; diff --git a/renderlib/SkyLightObject.cpp b/renderlib/SkyLightObject.cpp index 9f169a1e..919f8455 100644 --- a/renderlib/SkyLightObject.cpp +++ b/renderlib/SkyLightObject.cpp @@ -3,6 +3,9 @@ #include "SceneLight.h" #include "Logging.h" +#include "serialize/docReader.h" +#include "serialize/docWriter.h" + SkyLightObject::SkyLightObject() : prtyObject() { @@ -153,3 +156,34 @@ SkyLightObject::BottomColorChanged(prtyProperty* i_Property, bool i_bDirty) { updateSceneLightFromProps(); } + +void +SkyLightObject::fromDocument(docReader* reader) +{ + // Peek at metadata + uint32_t version = reader->peekVersion(); + std::string type = reader->peekObjectType(); + std::string name = reader->peekObjectName(); + + reader->readPrty(&m_skylightDataObject.TopIntensity); + reader->readPrty(&m_skylightDataObject.TopColor); + reader->readPrty(&m_skylightDataObject.MiddleIntensity); + reader->readPrty(&m_skylightDataObject.MiddleColor); + reader->readPrty(&m_skylightDataObject.BottomIntensity); + reader->readPrty(&m_skylightDataObject.BottomColor); +} + +void +SkyLightObject::toDocument(docWriter* writer) +{ + writer->beginObject("skyLight0", "SkyLightObject", SkyLightObject::CURRENT_VERSION); + + m_skylightDataObject.TopIntensity.Write(*writer); + m_skylightDataObject.TopColor.Write(*writer); + m_skylightDataObject.MiddleIntensity.Write(*writer); + m_skylightDataObject.MiddleColor.Write(*writer); + m_skylightDataObject.BottomIntensity.Write(*writer); + m_skylightDataObject.BottomColor.Write(*writer); + + writer->endObject(); +} diff --git a/renderlib/SkyLightObject.hpp b/renderlib/SkyLightObject.hpp index bce75a84..0737e6b5 100644 --- a/renderlib/SkyLightObject.hpp +++ b/renderlib/SkyLightObject.hpp @@ -37,6 +37,10 @@ class SkyLightObject : public prtyObject // Update scene light instance from properties (called automatically via callbacks) void updateSceneLightFromProps(); + // Getter for data object + SkylightDataObject& getDataObject() { return m_skylightDataObject; } + const SkylightDataObject& getDataObject() const { return m_skylightDataObject; } + // Property change callbacks void TopIntensityChanged(prtyProperty* i_Property, bool i_bDirty); void TopColorChanged(prtyProperty* i_Property, bool i_bDirty); @@ -47,6 +51,11 @@ class SkyLightObject : public prtyObject void setDirtyCallback(std::function callback) { m_dirtyCallback = callback; } + // document reading and writing; TODO consider an abstract base class to enforce commonality + static constexpr uint32_t CURRENT_VERSION = 1; + void fromDocument(docReader* reader); + void toDocument(docWriter* writer); + private: SkylightDataObject m_skylightDataObject; diff --git a/renderlib/ViewerWindow.cpp b/renderlib/ViewerWindow.cpp index 4968cd23..ee7dc20c 100644 --- a/renderlib/ViewerWindow.cpp +++ b/renderlib/ViewerWindow.cpp @@ -137,10 +137,6 @@ ViewerWindow::updateCamera() // let renderer know camera is dirty m_renderSettings->m_DirtyFlags.SetFlag(CameraDirty); } - if (m_CCamera.m_Dirty) { - m_renderSettings->m_DirtyFlags.SetFlag(CameraDirty); - m_CCamera.m_Dirty = false; - } sceneView.camera = renderCamera; sceneView.camera.Update(); diff --git a/renderlib/core/CMakeLists.txt b/renderlib/core/CMakeLists.txt index c5301ab1..31f9428c 100644 --- a/renderlib/core/CMakeLists.txt +++ b/renderlib/core/CMakeLists.txt @@ -1,2 +1,6 @@ +target_include_directories(renderlib PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" +) + add_subdirectory(prty) add_subdirectory(undo) diff --git a/renderlib/core/prty/CMakeLists.txt b/renderlib/core/prty/CMakeLists.txt index 03edfb9b..694649ec 100644 --- a/renderlib/core/prty/CMakeLists.txt +++ b/renderlib/core/prty/CMakeLists.txt @@ -1,3 +1,7 @@ +target_include_directories(renderlib PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" +) + target_sources(renderlib PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/prtyBoolean.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/prtyBoolean.hpp" @@ -9,10 +13,7 @@ target_sources(renderlib PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/prtyEnum.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/prtyFloat.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/prtyFloat.hpp" -"${CMAKE_CURRENT_SOURCE_DIR}/prtyInt32.cpp" -"${CMAKE_CURRENT_SOURCE_DIR}/prtyInt32.hpp" -"${CMAKE_CURRENT_SOURCE_DIR}/prtyInt8.cpp" -"${CMAKE_CURRENT_SOURCE_DIR}/prtyInt8.hpp" +"${CMAKE_CURRENT_SOURCE_DIR}/prtyIntegerTemplate.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/prtyInterest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/prtyInterest.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/prtyInterestUtil.cpp" diff --git a/renderlib/core/prty/prtyBoolean.cpp b/renderlib/core/prty/prtyBoolean.cpp index aaacb84a..9464484b 100644 --- a/renderlib/core/prty/prtyBoolean.cpp +++ b/renderlib/core/prty/prtyBoolean.cpp @@ -1,7 +1,8 @@ #include "core/prty/prtyBoolean.hpp" // #include "core/ch/chReader.hpp" -// #include "core/ch/chWriter.hpp" +#include "serialize/docReader.h" +#include "serialize/docWriter.h" //---------------------------------------------------------------------------- //---------------------------------------------------------------------------- @@ -80,18 +81,17 @@ prtyBoolean::operator!=(const bool i_Value) const //-------------------------------------------------------------------- // virtual void -prtyBoolean::Read(chReader& io_Reader) +prtyBoolean::Read(docReader& io_Reader) { - // bool temp; - // io_Reader.Read(temp); - // SetValue(temp); + bool temp = io_Reader.readBool(GetPropertyName()); + SetValue(temp); } //-------------------------------------------------------------------- //-------------------------------------------------------------------- // virtual void -prtyBoolean::Write(chWriter& io_Writer) const +prtyBoolean::Write(docWriter& io_Writer) const { - // io_Writer.Write(GetValue()); + io_Writer.writeBool(GetPropertyName(), GetValue()); } diff --git a/renderlib/core/prty/prtyBoolean.hpp b/renderlib/core/prty/prtyBoolean.hpp index 8ba2ecdc..8abeb495 100644 --- a/renderlib/core/prty/prtyBoolean.hpp +++ b/renderlib/core/prty/prtyBoolean.hpp @@ -40,9 +40,9 @@ class prtyBoolean : public prtyPropertyTemplate //-------------------------------------------------------------------- //-------------------------------------------------------------------- - virtual void Read(chReader& io_Reader); + virtual void Read(docReader& io_Reader); //-------------------------------------------------------------------- //-------------------------------------------------------------------- - virtual void Write(chWriter& io_Writer) const; + virtual void Write(docWriter& io_Writer) const; }; diff --git a/renderlib/core/prty/prtyColor.cpp b/renderlib/core/prty/prtyColor.cpp index 6e302065..d5797919 100644 --- a/renderlib/core/prty/prtyColor.cpp +++ b/renderlib/core/prty/prtyColor.cpp @@ -2,6 +2,8 @@ // #include "core/ch/chChunkParserUtil.hpp" // #include "core/ch/chReader.hpp" +#include "serialize/docReader.h" +#include "serialize/docWriter.h" //---------------------------------------------------------------------------- //---------------------------------------------------------------------------- @@ -80,18 +82,18 @@ prtyColor::operator!=(const glm::vec4& i_Value) const //-------------------------------------------------------------------- // virtual void -prtyColor::Read(chReader& io_Reader) +prtyColor::Read(docReader& io_Reader) { - // glm::vec4 temp; - // chChunkParserUtil::Read(io_Reader, temp); - // SetValue(temp); + std::vector temp; + temp = io_Reader.readFloat32Array(GetPropertyName()); + SetValue(glm::vec4(temp[0], temp[1], temp[2], temp[3])); } //-------------------------------------------------------------------- //-------------------------------------------------------------------- // virtual void -prtyColor::Write(chWriter& io_Writer) const +prtyColor::Write(docWriter& io_Writer) const { - // chChunkParserUtil::Write(io_Writer, GetValue()); + io_Writer.writeFloat32Array(GetPropertyName(), 4, glm::value_ptr(GetValue())); } diff --git a/renderlib/core/prty/prtyColor.hpp b/renderlib/core/prty/prtyColor.hpp index 822e4781..29aaf9a8 100644 --- a/renderlib/core/prty/prtyColor.hpp +++ b/renderlib/core/prty/prtyColor.hpp @@ -42,9 +42,9 @@ class prtyColor : public prtyPropertyTemplate //-------------------------------------------------------------------- //-------------------------------------------------------------------- - virtual void Read(chReader& io_Reader); + virtual void Read(docReader& io_Reader); //-------------------------------------------------------------------- //-------------------------------------------------------------------- - virtual void Write(chWriter& io_Writer) const; + virtual void Write(docWriter& io_Writer) const; }; diff --git a/renderlib/core/prty/prtyEnum.hpp b/renderlib/core/prty/prtyEnum.hpp index 48a821e5..efae2418 100644 --- a/renderlib/core/prty/prtyEnum.hpp +++ b/renderlib/core/prty/prtyEnum.hpp @@ -1,6 +1,6 @@ #pragma once -#include "core/prty/prtyInt8.hpp" +#include "core/prty/prtyIntegerTemplate.hpp" #include diff --git a/renderlib/core/prty/prtyFloat.cpp b/renderlib/core/prty/prtyFloat.cpp index 53d52b5b..cc3cf944 100644 --- a/renderlib/core/prty/prtyFloat.cpp +++ b/renderlib/core/prty/prtyFloat.cpp @@ -2,7 +2,8 @@ #include "core/prty/prtyUnits.hpp" // #include "core/ch/chReader.hpp" -// #include "core/ch/chWriter.hpp" +#include "serialize/docReader.h" +#include "serialize/docWriter.h" //---------------------------------------------------------------------------- //---------------------------------------------------------------------------- @@ -144,19 +145,18 @@ prtyFloat::operator<=(const float i_Value) const //-------------------------------------------------------------------- // virtual void -prtyFloat::Read(chReader& io_Reader) +prtyFloat::Read(docReader& io_Reader) { - // float temp; - // io_Reader.Read(temp); - // SetValue(temp); + float temp; + temp = io_Reader.readFloat32(GetPropertyName()); + SetValue(temp); } //-------------------------------------------------------------------- //-------------------------------------------------------------------- // virtual void -prtyFloat::Write(chWriter& io_Writer) const +prtyFloat::Write(docWriter& io_Writer) const { - // float temp = GetValue(); - // io_Writer.Write(temp); + io_Writer.writeFloat32(GetPropertyName(), GetValue()); } diff --git a/renderlib/core/prty/prtyFloat.hpp b/renderlib/core/prty/prtyFloat.hpp index 350e3494..b249caf7 100644 --- a/renderlib/core/prty/prtyFloat.hpp +++ b/renderlib/core/prty/prtyFloat.hpp @@ -63,11 +63,11 @@ class prtyFloat : public prtyPropertyTemplate bool operator<=(const float i_Value) const; //-------------------------------------------------------------------- //-------------------------------------------------------------------- - virtual void Read(chReader& io_Reader); + virtual void Read(docReader& io_Reader); //-------------------------------------------------------------------- //-------------------------------------------------------------------- - virtual void Write(chWriter& io_Writer) const; + virtual void Write(docWriter& io_Writer) const; private: bool m_bUseUnits; diff --git a/renderlib/core/prty/prtyInt32.cpp b/renderlib/core/prty/prtyInt32.cpp deleted file mode 100644 index c14b0c06..00000000 --- a/renderlib/core/prty/prtyInt32.cpp +++ /dev/null @@ -1,142 +0,0 @@ -#include "core/prty/prtyInt32.hpp" - -// #include "core/ch/chChunkParserUtil.hpp" -// #include "core/ch/chReader.hpp" - -//---------------------------------------------------------------------------- -//---------------------------------------------------------------------------- -prtyInt32::prtyInt32() - : prtyPropertyTemplate("Int32", 0) -{ -} - -//---------------------------------------------------------------------------- -//---------------------------------------------------------------------------- -prtyInt32::prtyInt32(const std::string& i_Name) - : prtyPropertyTemplate(i_Name, 0) -{ -} - -//---------------------------------------------------------------------------- -//---------------------------------------------------------------------------- -prtyInt32::prtyInt32(const std::string& i_Name, const int& i_InitialValue) - : prtyPropertyTemplate(i_Name, i_InitialValue) -{ -} - -//-------------------------------------------------------------------- -// The type of property it is -//-------------------------------------------------------------------- -const char* -prtyInt32::GetType() -{ - return "Int32"; -} - -//-------------------------------------------------------------------- -// operators -//-------------------------------------------------------------------- -prtyInt32& -prtyInt32::operator=(const prtyInt32& i_Property) -{ - // copy base data - prtyProperty::operator=(i_Property); - - SetValue(i_Property.GetValue()); - return *this; -} -prtyInt32& -prtyInt32::operator=(const int i_Value) -{ - SetValue(i_Value); - return *this; -} - -//-------------------------------------------------------------------- -// comparison operators -//-------------------------------------------------------------------- -bool -prtyInt32::operator==(const prtyInt32& i_Property) const -{ - return (m_Value == i_Property.GetValue()); -} -bool -prtyInt32::operator!=(const prtyInt32& i_Property) const -{ - return (m_Value != i_Property.GetValue()); -} -bool -prtyInt32::operator==(const int i_Value) const -{ - return (m_Value == i_Value); -} -bool -prtyInt32::operator!=(const int i_Value) const -{ - return (m_Value != i_Value); -} - -//-------------------------------------------------------------------- -// comparison operators -//-------------------------------------------------------------------- -bool -prtyInt32::operator>(const int i_Value) const -{ - return (m_Value > i_Value); -} -bool -prtyInt32::operator>=(const int i_Value) const -{ - return (m_Value >= i_Value); -} -bool -prtyInt32::operator<(const int i_Value) const -{ - return (m_Value < i_Value); -} -bool -prtyInt32::operator<=(const int i_Value) const -{ - return (m_Value <= i_Value); -} -bool -prtyInt32::operator>(const prtyInt32& i_Value) const -{ - return (m_Value > i_Value.GetValue()); -} -bool -prtyInt32::operator>=(const prtyInt32& i_Value) const -{ - return (m_Value >= i_Value.GetValue()); -} -bool -prtyInt32::operator<(const prtyInt32& i_Value) const -{ - return (m_Value < i_Value.GetValue()); -} -bool -prtyInt32::operator<=(const prtyInt32& i_Value) const -{ - return (m_Value <= i_Value.GetValue()); -} - -//-------------------------------------------------------------------- -//-------------------------------------------------------------------- -// virtual -void -prtyInt32::Read(chReader& io_Reader) -{ - // int32_t temp; - // io_Reader.Read(temp); - // SetValue(temp); -} - -//-------------------------------------------------------------------- -//-------------------------------------------------------------------- -// virtual -void -prtyInt32::Write(chWriter& io_Writer) const -{ - // int32_t temp = GetValue(); - // io_Writer.Write(temp); -} diff --git a/renderlib/core/prty/prtyInt32.hpp b/renderlib/core/prty/prtyInt32.hpp deleted file mode 100644 index a7abfcde..00000000 --- a/renderlib/core/prty/prtyInt32.hpp +++ /dev/null @@ -1,60 +0,0 @@ -#pragma once - -#include "core/prty/prtyPropertyTemplate.hpp" - -//============================================================================ -//============================================================================ -class prtyInt32 : public prtyPropertyTemplate -{ -public: - //-------------------------------------------------------------------- - //-------------------------------------------------------------------- - prtyInt32(); - - //-------------------------------------------------------------------- - //-------------------------------------------------------------------- - prtyInt32(const std::string& i_Name); - - //-------------------------------------------------------------------- - //-------------------------------------------------------------------- - prtyInt32(const std::string& i_Name, const int& i_InitialValue); - - //-------------------------------------------------------------------- - // The type of property it is - //-------------------------------------------------------------------- - virtual const char* GetType(); - - //-------------------------------------------------------------------- - // operators - //-------------------------------------------------------------------- - prtyInt32& operator=(const prtyInt32& i_Property); - prtyInt32& operator=(const int i_Value); - - //-------------------------------------------------------------------- - // comparison operators - //-------------------------------------------------------------------- - bool operator==(const prtyInt32& i_Property) const; - bool operator!=(const prtyInt32& i_Property) const; - bool operator==(const int i_Value) const; - bool operator!=(const int i_Value) const; - - //-------------------------------------------------------------------- - // comparison operators - //-------------------------------------------------------------------- - bool operator>(const int i_Value) const; - bool operator>=(const int i_Value) const; - bool operator<(const int i_Value) const; - bool operator<=(const int i_Value) const; - bool operator>(const prtyInt32& i_Value) const; - bool operator>=(const prtyInt32& i_Value) const; - bool operator<(const prtyInt32& i_Value) const; - bool operator<=(const prtyInt32& i_Value) const; - - //-------------------------------------------------------------------- - //-------------------------------------------------------------------- - virtual void Read(chReader& io_Reader); - - //-------------------------------------------------------------------- - //-------------------------------------------------------------------- - virtual void Write(chWriter& io_Writer) const; -}; diff --git a/renderlib/core/prty/prtyInt8.cpp b/renderlib/core/prty/prtyInt8.cpp deleted file mode 100644 index bbe85620..00000000 --- a/renderlib/core/prty/prtyInt8.cpp +++ /dev/null @@ -1,142 +0,0 @@ -#include "core/prty/prtyInt8.hpp" - -// #include "core/ch/chReader.hpp" -// #include "core/ch/chWriter.hpp" - -//---------------------------------------------------------------------------- -//---------------------------------------------------------------------------- -prtyInt8::prtyInt8() - : prtyPropertyTemplate("Int8", 0) -{ -} - -//---------------------------------------------------------------------------- -//---------------------------------------------------------------------------- -prtyInt8::prtyInt8(const std::string& i_Name) - : prtyPropertyTemplate(i_Name, 0) -{ -} - -//---------------------------------------------------------------------------- -//---------------------------------------------------------------------------- -prtyInt8::prtyInt8(const std::string& i_Name, int8_t i_InitialValue) - : prtyPropertyTemplate(i_Name, i_InitialValue) -{ -} - -//-------------------------------------------------------------------- -// The type of property it is -//-------------------------------------------------------------------- -const char* -prtyInt8::GetType() -{ - return "Int8"; -} - -//-------------------------------------------------------------------- -// operators -//-------------------------------------------------------------------- -prtyInt8& -prtyInt8::operator=(const prtyInt8& i_Property) -{ - // copy base data - prtyProperty::operator=(i_Property); - - SetValue(i_Property.GetValue()); - return *this; -} -prtyInt8& -prtyInt8::operator=(const int8_t i_Value) -{ - SetValue(i_Value); - return *this; -} - -//-------------------------------------------------------------------- -// comparison operators -//-------------------------------------------------------------------- -bool -prtyInt8::operator==(const prtyInt8& i_Property) const -{ - return (m_Value == i_Property.GetValue()); -} -bool -prtyInt8::operator!=(const prtyInt8& i_Property) const -{ - return (m_Value != i_Property.GetValue()); -} -bool -prtyInt8::operator==(const int8_t i_Value) const -{ - return (m_Value == i_Value); -} -bool -prtyInt8::operator!=(const int8_t i_Value) const -{ - return (m_Value != i_Value); -} - -//-------------------------------------------------------------------- -// comparison operators -//-------------------------------------------------------------------- -bool -prtyInt8::operator>(const int8_t i_Value) const -{ - return (m_Value > i_Value); -} -bool -prtyInt8::operator>=(const int8_t i_Value) const -{ - return (m_Value >= i_Value); -} -bool -prtyInt8::operator<(const int8_t i_Value) const -{ - return (m_Value < i_Value); -} -bool -prtyInt8::operator<=(const int8_t i_Value) const -{ - return (m_Value <= i_Value); -} -bool -prtyInt8::operator>(const prtyInt8& i_Value) const -{ - return (m_Value > i_Value.GetValue()); -} -bool -prtyInt8::operator>=(const prtyInt8& i_Value) const -{ - return (m_Value >= i_Value.GetValue()); -} -bool -prtyInt8::operator<(const prtyInt8& i_Value) const -{ - return (m_Value < i_Value.GetValue()); -} -bool -prtyInt8::operator<=(const prtyInt8& i_Value) const -{ - return (m_Value <= i_Value.GetValue()); -} - -//-------------------------------------------------------------------- -//-------------------------------------------------------------------- -// virtual -void -prtyInt8::Read(chReader& io_Reader) -{ - // int8_t temp; - // io_Reader.Read(temp); - // SetValue(temp); -} - -//-------------------------------------------------------------------- -//-------------------------------------------------------------------- -// virtual -void -prtyInt8::Write(chWriter& io_Writer) const -{ - // int8_t temp = GetValue(); - // io_Writer.Write(temp); -} diff --git a/renderlib/core/prty/prtyInt8.hpp b/renderlib/core/prty/prtyInt8.hpp deleted file mode 100644 index 182ce164..00000000 --- a/renderlib/core/prty/prtyInt8.hpp +++ /dev/null @@ -1,60 +0,0 @@ -#pragma once - -#include "core/prty/prtyPropertyTemplate.hpp" - -//============================================================================ -//============================================================================ -class prtyInt8 : public prtyPropertyTemplate -{ -public: - //-------------------------------------------------------------------- - //-------------------------------------------------------------------- - prtyInt8(); - - //-------------------------------------------------------------------- - //-------------------------------------------------------------------- - prtyInt8(const std::string& i_Name); - - //-------------------------------------------------------------------- - //-------------------------------------------------------------------- - prtyInt8(const std::string& i_Name, int8_t i_InitialValue); - - //-------------------------------------------------------------------- - // The type of property it is - //-------------------------------------------------------------------- - virtual const char* GetType(); - - //-------------------------------------------------------------------- - // operators - //-------------------------------------------------------------------- - prtyInt8& operator=(const prtyInt8& i_Property); - prtyInt8& operator=(const int8_t i_Value); - - //-------------------------------------------------------------------- - // comparison operators - //-------------------------------------------------------------------- - bool operator==(const prtyInt8& i_Property) const; - bool operator!=(const prtyInt8& i_Property) const; - bool operator==(const int8_t i_Value) const; - bool operator!=(const int8_t i_Value) const; - - //-------------------------------------------------------------------- - // comparison operators - //-------------------------------------------------------------------- - bool operator>(const int8_t i_Value) const; - bool operator>=(const int8_t i_Value) const; - bool operator<(const int8_t i_Value) const; - bool operator<=(const int8_t i_Value) const; - bool operator>(const prtyInt8& i_Value) const; - bool operator>=(const prtyInt8& i_Value) const; - bool operator<(const prtyInt8& i_Value) const; - bool operator<=(const prtyInt8& i_Value) const; - - //-------------------------------------------------------------------- - //-------------------------------------------------------------------- - virtual void Read(chReader& io_Reader); - - //-------------------------------------------------------------------- - //-------------------------------------------------------------------- - virtual void Write(chWriter& io_Writer) const; -}; diff --git a/renderlib/core/prty/prtyIntegerTemplate.hpp b/renderlib/core/prty/prtyIntegerTemplate.hpp new file mode 100644 index 00000000..df69356d --- /dev/null +++ b/renderlib/core/prty/prtyIntegerTemplate.hpp @@ -0,0 +1,148 @@ +#pragma once + +#include "core/prty/prtyPropertyTemplate.hpp" +#include "serialize/docReader.h" +#include "serialize/docWriter.h" +#include + +//============================================================================ +// Template class for integer properties (signed and unsigned) +// This eliminates the need for separate prtyInt8, prtyInt16, prtyInt32, etc. +//============================================================================ +template +class prtyIntegerTemplate : public prtyPropertyTemplate +{ + static_assert(std::is_integral_v, "prtyIntegerTemplate only works with integral types"); + +public: + //-------------------------------------------------------------------- + //-------------------------------------------------------------------- + prtyIntegerTemplate() + : prtyPropertyTemplate(GetTypeName(), T{}) + { + } + + //-------------------------------------------------------------------- + //-------------------------------------------------------------------- + prtyIntegerTemplate(const std::string& i_Name) + : prtyPropertyTemplate(i_Name, T{}) + { + } + + //-------------------------------------------------------------------- + //-------------------------------------------------------------------- + prtyIntegerTemplate(const std::string& i_Name, T i_InitialValue) + : prtyPropertyTemplate(i_Name, i_InitialValue) + { + } + + //-------------------------------------------------------------------- + // The type of property it is + //-------------------------------------------------------------------- + virtual const char* GetType() override { return GetTypeName(); } + + //-------------------------------------------------------------------- + // operators + //-------------------------------------------------------------------- + prtyIntegerTemplate& operator=(const prtyIntegerTemplate& i_Property) + { + // copy base data + prtyProperty::operator=(i_Property); + this->SetValue(i_Property.GetValue()); + return *this; + } + + prtyIntegerTemplate& operator=(T i_Value) + { + this->SetValue(i_Value); + return *this; + } + + //-------------------------------------------------------------------- + // comparison operators + //-------------------------------------------------------------------- + bool operator==(const prtyIntegerTemplate& i_Property) const { return (this->m_Value == i_Property.GetValue()); } + + bool operator!=(const prtyIntegerTemplate& i_Property) const { return (this->m_Value != i_Property.GetValue()); } + + bool operator==(T i_Value) const { return (this->m_Value == i_Value); } + + bool operator!=(T i_Value) const { return (this->m_Value != i_Value); } + + //-------------------------------------------------------------------- + // comparison operators + //-------------------------------------------------------------------- + bool operator>(T i_Value) const { return (this->m_Value > i_Value); } + + bool operator>=(T i_Value) const { return (this->m_Value >= i_Value); } + + bool operator<(T i_Value) const { return (this->m_Value < i_Value); } + + bool operator<=(T i_Value) const { return (this->m_Value <= i_Value); } + + bool operator>(const prtyIntegerTemplate& i_Value) const { return (this->m_Value > i_Value.GetValue()); } + + bool operator>=(const prtyIntegerTemplate& i_Value) const { return (this->m_Value >= i_Value.GetValue()); } + + bool operator<(const prtyIntegerTemplate& i_Value) const { return (this->m_Value < i_Value.GetValue()); } + + bool operator<=(const prtyIntegerTemplate& i_Value) const { return (this->m_Value <= i_Value.GetValue()); } + + //-------------------------------------------------------------------- + //-------------------------------------------------------------------- + virtual void Read(docReader& io_Reader) override + { + T temp; + if constexpr (std::is_signed_v) { + temp = io_Reader.readInt(this->GetPropertyName()); + } else { + temp = io_Reader.readUint(this->GetPropertyName()); + } + this->SetValue(temp); + } + + //-------------------------------------------------------------------- + //-------------------------------------------------------------------- + virtual void Write(docWriter& io_Writer) const override + { + if constexpr (std::is_signed_v) { + io_Writer.writeInt(this->GetPropertyName(), this->GetValue()); + } else { + io_Writer.writeUint(this->GetPropertyName(), this->GetValue()); + } + } + +private: + static constexpr const char* GetTypeName() + { + if constexpr (std::is_same_v) { + return "Int8"; + } else if constexpr (std::is_same_v) { + return "Int16"; + } else if constexpr (std::is_same_v || std::is_same_v) { + return "Int32"; + } else if constexpr (std::is_same_v) { + return "Int64"; + } else if constexpr (std::is_same_v) { + return "Uint8"; + } else if constexpr (std::is_same_v) { + return "Uint16"; + } else if constexpr (std::is_same_v) { + return "Uint32"; + } else if constexpr (std::is_same_v) { + return "Uint64"; + } else { + return "Integer"; + } + } +}; + +// Convenient type aliases for common integer types +using prtyInt8 = prtyIntegerTemplate; +using prtyInt16 = prtyIntegerTemplate; +using prtyInt32 = prtyIntegerTemplate; +using prtyInt64 = prtyIntegerTemplate; +using prtyUint8 = prtyIntegerTemplate; +using prtyUint16 = prtyIntegerTemplate; +using prtyUint32 = prtyIntegerTemplate; +using prtyUint64 = prtyIntegerTemplate; diff --git a/renderlib/core/prty/prtyProperty.hpp b/renderlib/core/prty/prtyProperty.hpp index 0d6c29b8..6f81b9e1 100644 --- a/renderlib/core/prty/prtyProperty.hpp +++ b/renderlib/core/prty/prtyProperty.hpp @@ -13,8 +13,8 @@ //============================================================================ class prtyProperty; class prtyPropertyReference; -class chReader; -class chWriter; +class docReader; +class docWriter; //============================================================================ // typedefs @@ -187,11 +187,11 @@ class prtyProperty //-------------------------------------------------------------------- //-------------------------------------------------------------------- - virtual void Read(chReader& io_Reader) = 0; + virtual void Read(docReader& io_Reader) = 0; //-------------------------------------------------------------------- //-------------------------------------------------------------------- - virtual void Write(chWriter& io_Writer) const = 0; + virtual void Write(docWriter& io_Writer) const = 0; //-------------------------------------------------------------------- // In some cases we want the form builder to check whether or not diff --git a/renderlib/core/prty/prtyRotation.cpp b/renderlib/core/prty/prtyRotation.cpp index f833516c..4d0965ef 100644 --- a/renderlib/core/prty/prtyRotation.cpp +++ b/renderlib/core/prty/prtyRotation.cpp @@ -5,6 +5,8 @@ // #include "core/ch/chChunkParserUtil.hpp" // #include "core/ch/chReader.hpp" // #include "core/ma/maConstants.hpp" +#include "serialize/docReader.h" +#include "serialize/docWriter.h" #include "core/undo/undoUndoMgr.hpp" namespace { @@ -191,19 +193,19 @@ prtyRotation::CreateUndoOperation(std::shared_ptr i_pProp //-------------------------------------------------------------------- // virtual void -prtyRotation::Read(chReader& io_Reader) +prtyRotation::Read(docReader& io_Reader) { // // We need to write euler angles, how to handle versions? - // glm::quat temp; - // chChunkParserUtil::Read(io_Reader, temp); - // SetQuaternion(temp); + std::vector temp; + temp = io_Reader.readFloat32Array(GetPropertyName()); + SetQuaternion(glm::quat(temp[3], temp[0], temp[1], temp[2])); } //-------------------------------------------------------------------- //-------------------------------------------------------------------- // virtual void -prtyRotation::Write(chWriter& io_Writer) const +prtyRotation::Write(docWriter& io_Writer) const { - // chChunkParserUtil::Write(io_Writer, m_Quaternion); + io_Writer.writeFloat32Array(GetPropertyName(), 4, glm::value_ptr(GetQuaternion())); } diff --git a/renderlib/core/prty/prtyRotation.hpp b/renderlib/core/prty/prtyRotation.hpp index 7aad3327..3b8ea437 100644 --- a/renderlib/core/prty/prtyRotation.hpp +++ b/renderlib/core/prty/prtyRotation.hpp @@ -81,11 +81,11 @@ class prtyRotation : public prtyProperty //-------------------------------------------------------------------- //-------------------------------------------------------------------- - virtual void Read(chReader& io_Reader); + virtual void Read(docReader& io_Reader); //-------------------------------------------------------------------- //-------------------------------------------------------------------- - virtual void Write(chWriter& io_Writer) const; + virtual void Write(docWriter& io_Writer) const; private: glm::quat m_Quaternion; diff --git a/renderlib/core/prty/prtyText.cpp b/renderlib/core/prty/prtyText.cpp index 74b87231..c937368a 100644 --- a/renderlib/core/prty/prtyText.cpp +++ b/renderlib/core/prty/prtyText.cpp @@ -2,6 +2,8 @@ // #include "core/ch/chChunkParserUtil.hpp" // #include "core/ch/chReader.hpp" +#include "serialize/docReader.h" +#include "serialize/docWriter.h" //-------------------------------------------------------------------- //-------------------------------------------------------------------- @@ -86,18 +88,19 @@ prtyText::operator!=(const std::string& i_Value) const //-------------------------------------------------------------------- // virtual void -prtyText::Read(chReader& io_Reader) +prtyText::Read(docReader& io_Reader) { - // std::string temp; - // chChunkParserUtil::Read(io_Reader, temp); - // SetValue(temp); + std::string temp; + temp = io_Reader.readString(GetPropertyName()); + SetValue(temp); } //-------------------------------------------------------------------- //-------------------------------------------------------------------- // virtual void -prtyText::Write(chWriter& io_Writer) const +prtyText::Write(docWriter& io_Writer) const { + io_Writer.writeString(GetPropertyName(), GetValue()); // chChunkParserUtil::Write(io_Writer, GetValue()); } diff --git a/renderlib/core/prty/prtyText.hpp b/renderlib/core/prty/prtyText.hpp index 1e2fa90e..e56ed5f2 100644 --- a/renderlib/core/prty/prtyText.hpp +++ b/renderlib/core/prty/prtyText.hpp @@ -41,9 +41,9 @@ class prtyText : public prtyPropertyTemplate //-------------------------------------------------------------------- //-------------------------------------------------------------------- - virtual void Read(chReader& io_Reader); + virtual void Read(docReader& io_Reader); //-------------------------------------------------------------------- //-------------------------------------------------------------------- - virtual void Write(chWriter& io_Writer) const; + virtual void Write(docWriter& io_Writer) const; }; diff --git a/renderlib/core/prty/prtyVector3d.cpp b/renderlib/core/prty/prtyVector3d.cpp index ca0bfa8c..5ea6f8d7 100644 --- a/renderlib/core/prty/prtyVector3d.cpp +++ b/renderlib/core/prty/prtyVector3d.cpp @@ -3,6 +3,8 @@ // #include "core/ch/chChunkParserUtil.hpp" // #include "core/ch/chReader.hpp" +#include "serialize/docReader.h" +#include "serialize/docWriter.h" //---------------------------------------------------------------------------- //---------------------------------------------------------------------------- @@ -132,18 +134,18 @@ prtyVector3d::SetScaledValue(const glm::vec3& i_Value, bool i_bDirty) // UndoFla //-------------------------------------------------------------------- // virtual void -prtyVector3d::Read(chReader& io_Reader) +prtyVector3d::Read(docReader& io_Reader) { - // glm::vec3 temp; - // chChunkParserUtil::Read(io_Reader, temp); - // SetValue(temp); + std::vector temp; + temp = io_Reader.readFloat32Array(GetPropertyName()); + SetValue(glm::vec3(temp[0], temp[1], temp[2])); } //-------------------------------------------------------------------- //-------------------------------------------------------------------- // virtual void -prtyVector3d::Write(chWriter& io_Writer) const +prtyVector3d::Write(docWriter& io_Writer) const { - // chChunkParserUtil::Write(io_Writer, GetValue()); + io_Writer.writeFloat32Array(GetPropertyName(), 3, glm::value_ptr(GetValue())); } diff --git a/renderlib/core/prty/prtyVector3d.hpp b/renderlib/core/prty/prtyVector3d.hpp index 80a34873..89fa65a3 100644 --- a/renderlib/core/prty/prtyVector3d.hpp +++ b/renderlib/core/prty/prtyVector3d.hpp @@ -65,11 +65,11 @@ class prtyVector3d : public prtyPropertyTemplate //-------------------------------------------------------------------- //-------------------------------------------------------------------- - virtual void Read(chReader& io_Reader); + virtual void Read(docReader& io_Reader); //-------------------------------------------------------------------- //-------------------------------------------------------------------- - virtual void Write(chWriter& io_Writer) const; + virtual void Write(docWriter& io_Writer) const; private: bool m_bUseUnits; diff --git a/renderlib/serialize/CMakeLists.txt b/renderlib/serialize/CMakeLists.txt new file mode 100644 index 00000000..fa083d8a --- /dev/null +++ b/renderlib/serialize/CMakeLists.txt @@ -0,0 +1,21 @@ +target_include_directories(renderlib PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" +) + +target_sources(renderlib PRIVATE +"${CMAKE_CURRENT_SOURCE_DIR}/SerializationConstants.h" +"${CMAKE_CURRENT_SOURCE_DIR}/docWriter.h" +"${CMAKE_CURRENT_SOURCE_DIR}/docWriter.cpp" +"${CMAKE_CURRENT_SOURCE_DIR}/docWriterJson.h" +"${CMAKE_CURRENT_SOURCE_DIR}/docWriterJson.cpp" +"${CMAKE_CURRENT_SOURCE_DIR}/docWriterYaml.h" +"${CMAKE_CURRENT_SOURCE_DIR}/docWriterYaml.cpp" +"${CMAKE_CURRENT_SOURCE_DIR}/docReader.h" +"${CMAKE_CURRENT_SOURCE_DIR}/docReader.cpp" +"${CMAKE_CURRENT_SOURCE_DIR}/docReaderJson.h" +"${CMAKE_CURRENT_SOURCE_DIR}/docReaderJson.cpp" +"${CMAKE_CURRENT_SOURCE_DIR}/docReaderYaml.h" +"${CMAKE_CURRENT_SOURCE_DIR}/docReaderYaml.cpp" +) + + diff --git a/renderlib/serialize/SerializationConstants.h b/renderlib/serialize/SerializationConstants.h new file mode 100644 index 00000000..5622e714 --- /dev/null +++ b/renderlib/serialize/SerializationConstants.h @@ -0,0 +1,10 @@ +#pragma once + +namespace SerializationConstants { + +// Reserved keys for serialization metadata +constexpr const char* TYPE_KEY = "_type"; +constexpr const char* VERSION_KEY = "_version"; +constexpr const char* NAME_KEY = "_name"; + +} // namespace SerializationConstants diff --git a/renderlib/serialize/docReader.cpp b/renderlib/serialize/docReader.cpp new file mode 100644 index 00000000..e017761f --- /dev/null +++ b/renderlib/serialize/docReader.cpp @@ -0,0 +1,6 @@ +#include "docReader.h" + +#include "core/prty/prtyProperty.hpp" +#include "core/prty/prtyObject.hpp" + +#include "Logging.h" diff --git a/renderlib/serialize/docReader.h b/renderlib/serialize/docReader.h new file mode 100644 index 00000000..68645349 --- /dev/null +++ b/renderlib/serialize/docReader.h @@ -0,0 +1,125 @@ +#pragma once + +#include +#include +#include + +class prtyProperty; +class prtyObject; + +class docReader +{ +public: + docReader() {} + virtual ~docReader() {} + + // this must be called to start and end the document before any other reading can happen. + virtual bool beginDocument(std::string filePath) = 0; + virtual void endDocument() = 0; + + // objects can contain other objects, lists, and properties. + virtual bool beginObject(const std::string& i_name) = 0; + virtual void endObject() = 0; + + // lists can contain objects or properties. + virtual bool beginList(const std::string& i_name) = 0; + virtual void endList() = 0; + + // Check if a key exists at the current level + virtual bool hasKey(const std::string& key) = 0; + + // Peek at object type and version without consuming + virtual std::string peekObjectType() = 0; + virtual uint32_t peekVersion() = 0; + virtual std::string peekObjectName() = 0; + + // properties will read their name and associated value using the primitive read methods. + virtual bool readPrty(prtyProperty* p) = 0; + + // Templated property reader that maps value types to primitive read methods + template + T readProperty(const std::string& name) + { + if constexpr (std::is_same_v) { + return readBool(name); + } else if constexpr (std::is_same_v) { + return readInt8(name); + } else if constexpr (std::is_same_v) { + return readInt32(name); + } else if constexpr (std::is_same_v) { + return readUint32(name); + } else if constexpr (std::is_same_v) { + return readFloat32(name); + } else if constexpr (std::is_same_v) { + return readString(name); + } else if constexpr (std::is_same_v>) { + return readFloat32Array(name); + } else if constexpr (std::is_same_v>) { + return readInt32Array(name); + } else if constexpr (std::is_same_v>) { + return readUint32Array(name); + } else { + static_assert(sizeof(T) == 0, "Unsupported type for readProperty"); + return T{}; + } + } + + // Overload that takes a pointer and assigns to it + template + void readProperty(const std::string& name, T* value) + { + *value = readProperty(name); + } + + // Templated integer read method + template + T readInt(const std::string& name) + { + if constexpr (std::is_same_v) { + return readInt8(name); + } else if constexpr (std::is_same_v) { + return readInt16(name); + } else if constexpr (std::is_same_v || std::is_same_v) { + return readInt32(name); + } else if constexpr (std::is_same_v) { + return readInt64(name); + } else { + static_assert(sizeof(T) == 0, "Unsupported signed integer type for readInt"); + return T{}; + } + } + + // Templated unsigned integer read method + template + T readUint(const std::string& name) + { + if constexpr (std::is_same_v) { + return readUint8(name); + } else if constexpr (std::is_same_v) { + return readUint16(name); + } else if constexpr (std::is_same_v) { + return readUint32(name); + } else if constexpr (std::is_same_v) { + return readUint64(name); + } else { + static_assert(sizeof(T) == 0, "Unsupported unsigned integer type for readUint"); + return T{}; + } + } + + // All primitive read methods now require a name parameter + virtual bool readBool(const std::string& name) = 0; + virtual int8_t readInt8(const std::string& name) = 0; + virtual int16_t readInt16(const std::string& name) = 0; + virtual int32_t readInt32(const std::string& name) = 0; + virtual int64_t readInt64(const std::string& name) = 0; + virtual uint8_t readUint8(const std::string& name) = 0; + virtual uint16_t readUint16(const std::string& name) = 0; + virtual uint32_t readUint32(const std::string& name) = 0; + virtual uint64_t readUint64(const std::string& name) = 0; + virtual float readFloat32(const std::string& name) = 0; + virtual std::vector readFloat32Array(const std::string& name) = 0; + virtual std::vector readInt32Array(const std::string& name) = 0; + virtual std::vector readUint32Array(const std::string& name) = 0; + virtual std::string readString(const std::string& name) = 0; +}; diff --git a/renderlib/serialize/docReaderJson.cpp b/renderlib/serialize/docReaderJson.cpp new file mode 100644 index 00000000..c926ba57 --- /dev/null +++ b/renderlib/serialize/docReaderJson.cpp @@ -0,0 +1,482 @@ +#include "docReaderJson.h" + +#include "SerializationConstants.h" +#include "core/prty/prtyProperty.hpp" +#include "Logging.h" + +#include "json/json.hpp" + +#include + +docReaderJson::docReaderJson() + : m_root(nullptr) + , m_current(nullptr) + , m_nextKey("") +{ +} + +docReaderJson::~docReaderJson() +{ + if (m_root) { + delete m_root; + m_root = nullptr; + } +} + +bool +docReaderJson::beginDocument(std::string filePath) +{ + m_filePath = filePath; + if (m_root) { + delete m_root; + } + + // Read the JSON file + std::ifstream inFile(m_filePath); + if (!inFile.is_open()) { + LOG_ERROR << "Failed to open file for reading: " << m_filePath; + return false; + } + + try { + m_root = new nlohmann::json(); + inFile >> *m_root; + m_current = m_root; + } catch (const nlohmann::json::exception& e) { + LOG_ERROR << "JSON parse error: " << e.what(); + delete m_root; + m_root = nullptr; + return false; + } + + inFile.close(); + + // Clear the context stack + while (!m_contextStack.empty()) { + m_contextStack.pop(); + } + + return true; +} + +void +docReaderJson::endDocument() +{ + if (!m_contextStack.empty()) { + LOG_ERROR << "endDocument() called with " << m_contextStack.size() + << " unclosed context(s). Document may be incomplete."; + } +} + +bool +docReaderJson::beginObject(const std::string& i_name) +{ + if (!m_current) { + LOG_ERROR << "beginObject() called with null current object"; + return false; + } + + nlohmann::json* targetObj = nullptr; + + if (m_contextStack.empty()) { + // Root level object + if (m_current->contains(i_name) && (*m_current)[i_name].is_object()) { + targetObj = &(*m_current)[i_name]; + } else { + LOG_ERROR << "beginObject() - key not found or not an object: " << i_name; + return false; + } + } else { + const Context& ctx = m_contextStack.top(); + if (ctx.isArray()) { + // Reading object from an array + if (ctx.arrayIndex < ctx.jsonObj->size() && (*ctx.jsonObj)[ctx.arrayIndex].is_object()) { + targetObj = &(*ctx.jsonObj)[ctx.arrayIndex]; + } else { + LOG_ERROR << "beginObject() - array index out of bounds or not an object"; + return false; + } + } else { + // Reading object from an object + if (ctx.jsonObj->contains(i_name) && (*ctx.jsonObj)[i_name].is_object()) { + targetObj = &(*ctx.jsonObj)[i_name]; + } else { + LOG_ERROR << "beginObject() - key not found or not an object: " << i_name; + return false; + } + } + } + + pushContext(targetObj, i_name, ContextType::Object); + return true; +} + +void +docReaderJson::endObject() +{ + if (m_contextStack.empty()) { + LOG_ERROR << "endObject() called with no matching beginObject()"; + return; + } + + if (!popContext(ContextType::Object)) { + LOG_ERROR << "endObject() called but current context is not an object"; + } +} + +bool +docReaderJson::beginList(const std::string& i_name) +{ + if (!m_current) { + LOG_ERROR << "beginList() called with null current object"; + return false; + } + + nlohmann::json* targetArray = nullptr; + + if (m_contextStack.empty()) { + // Root level array + if (m_current->contains(i_name) && (*m_current)[i_name].is_array()) { + targetArray = &(*m_current)[i_name]; + } else { + LOG_ERROR << "beginList() - key not found or not an array: " << i_name; + return false; + } + } else { + const Context& ctx = m_contextStack.top(); + if (ctx.isArray()) { + // Reading array from an array + if (ctx.arrayIndex < ctx.jsonObj->size() && (*ctx.jsonObj)[ctx.arrayIndex].is_array()) { + targetArray = &(*ctx.jsonObj)[ctx.arrayIndex]; + } else { + LOG_ERROR << "beginList() - array index out of bounds or not an array"; + return false; + } + } else { + // Reading array from an object + if (ctx.jsonObj->contains(i_name) && (*ctx.jsonObj)[i_name].is_array()) { + targetArray = &(*ctx.jsonObj)[i_name]; + } else { + LOG_ERROR << "beginList() - key not found or not an array: " << i_name; + return false; + } + } + } + + pushContext(targetArray, i_name, ContextType::Array); + return true; +} + +void +docReaderJson::endList() +{ + if (m_contextStack.empty()) { + LOG_ERROR << "endList() called with no matching beginList()"; + return; + } + + if (!popContext(ContextType::Array)) { + LOG_ERROR << "endList() called but current context is not an array"; + } +} + +bool +docReaderJson::hasKey(const std::string& key) +{ + nlohmann::json* current = getCurrentObject(); + if (!current || !current->is_object()) { + return false; + } + return current->contains(key); +} + +std::string +docReaderJson::peekObjectType() +{ + nlohmann::json* current = getCurrentObject(); + if (!current || !current->is_object()) { + return ""; + } + + // Look for a "_type" key in the current object + if (current->contains(SerializationConstants::TYPE_KEY) && (*current)[SerializationConstants::TYPE_KEY].is_string()) { + return (*current)[SerializationConstants::TYPE_KEY].get(); + } + + return ""; +} + +uint32_t +docReaderJson::peekVersion() +{ + nlohmann::json* current = getCurrentObject(); + if (!current || !current->is_object()) { + return 0; + } + + // Look for a "_version" key in the current object + if (current->contains(SerializationConstants::VERSION_KEY) && + (*current)[SerializationConstants::VERSION_KEY].is_number_integer()) { + return (*current)[SerializationConstants::VERSION_KEY].get(); + } + + return 0; +} + +std::string +docReaderJson::peekObjectName() +{ + nlohmann::json* current = getCurrentObject(); + if (!current || !current->is_object()) { + return ""; + } + + // Look for a "_name" key in the current object + if (current->contains(SerializationConstants::NAME_KEY) && (*current)[SerializationConstants::NAME_KEY].is_string()) { + return (*current)[SerializationConstants::NAME_KEY].get(); + } + + return ""; +} + +bool +docReaderJson::readPrty(prtyProperty* p) +{ + if (!p) { + return false; + } + + // Store the property name for the next read operation + m_nextKey = p->GetPropertyName(); + + // Check if the key exists + nlohmann::json* current = getCurrentObject(); + if (!current || !current->contains(m_nextKey)) { + LOG_ERROR << "readPrty() - property key not found: " << m_nextKey; + return false; + } + + // Let the property read itself + p->Read(*this); + return true; +} + +bool +docReaderJson::readBool(const std::string& name) +{ + nlohmann::json* current = getCurrentObject(); + if (!current) { + return false; + } + + if (m_contextStack.empty() || !m_contextStack.top().isArray()) { + // Reading from object by key + if (current->contains(name) && (*current)[name].is_boolean()) { + return (*current)[name].get(); + } + } else { + // Reading from array by index + Context& ctx = m_contextStack.top(); + if (ctx.arrayIndex < ctx.jsonObj->size() && (*ctx.jsonObj)[ctx.arrayIndex].is_boolean()) { + bool value = (*ctx.jsonObj)[ctx.arrayIndex].get(); + ctx.arrayIndex++; + return value; + } + } + + return false; +} + +int8_t +docReaderJson::readInt8(const std::string& name) +{ + return readSignedIntegerValue(name); +} + +int16_t +docReaderJson::readInt16(const std::string& name) +{ + return readSignedIntegerValue(name); +} + +int32_t +docReaderJson::readInt32(const std::string& name) +{ + return readSignedIntegerValue(name); +} + +int64_t +docReaderJson::readInt64(const std::string& name) +{ + return readSignedIntegerValue(name); +} + +uint8_t +docReaderJson::readUint8(const std::string& name) +{ + return readUnsignedIntegerValue(name); +} + +uint16_t +docReaderJson::readUint16(const std::string& name) +{ + return readUnsignedIntegerValue(name); +} + +uint32_t +docReaderJson::readUint32(const std::string& name) +{ + return readUnsignedIntegerValue(name); +} + +uint64_t +docReaderJson::readUint64(const std::string& name) +{ + return readUnsignedIntegerValue(name); +} + +float +docReaderJson::readFloat32(const std::string& name) +{ + nlohmann::json* current = getCurrentObject(); + if (!current) { + return 0.0f; + } + + if (m_contextStack.empty() || !m_contextStack.top().isArray()) { + // Reading from object by key + if (current->contains(name) && (*current)[name].is_number()) { + return (*current)[name].get(); + } + } else { + // Reading from array by index + Context& ctx = m_contextStack.top(); + if (ctx.arrayIndex < ctx.jsonObj->size() && (*ctx.jsonObj)[ctx.arrayIndex].is_number()) { + float value = (*ctx.jsonObj)[ctx.arrayIndex].get(); + ctx.arrayIndex++; + return value; + } + } + + return 0.0f; +} + +std::vector +docReaderJson::readFloat32Array(const std::string& name) +{ + std::vector result; + nlohmann::json* current = getCurrentObject(); + if (!current) { + return result; + } + + if (current->contains(name) && (*current)[name].is_array()) { + const nlohmann::json& arr = (*current)[name]; + for (const auto& elem : arr) { + if (elem.is_number()) { + result.push_back(elem.get()); + } + } + } + + return result; +} + +std::vector +docReaderJson::readInt32Array(const std::string& name) +{ + std::vector result; + nlohmann::json* current = getCurrentObject(); + if (!current) { + return result; + } + + if (current->contains(name) && (*current)[name].is_array()) { + const nlohmann::json& arr = (*current)[name]; + for (const auto& elem : arr) { + if (elem.is_number_integer()) { + result.push_back(elem.get()); + } + } + } + + return result; +} + +std::vector +docReaderJson::readUint32Array(const std::string& name) +{ + std::vector result; + nlohmann::json* current = getCurrentObject(); + if (!current) { + return result; + } + + if (current->contains(name) && (*current)[name].is_array()) { + const nlohmann::json& arr = (*current)[name]; + for (const auto& elem : arr) { + if (elem.is_number_unsigned()) { + result.push_back(elem.get()); + } + } + } + + return result; +} + +std::string +docReaderJson::readString(const std::string& name) +{ + nlohmann::json* current = getCurrentObject(); + if (!current) { + return ""; + } + + if (m_contextStack.empty() || !m_contextStack.top().isArray()) { + // Reading from object by key + if (current->contains(name) && (*current)[name].is_string()) { + return (*current)[name].get(); + } + } else { + // Reading from array by index + Context& ctx = m_contextStack.top(); + if (ctx.arrayIndex < ctx.jsonObj->size() && (*ctx.jsonObj)[ctx.arrayIndex].is_string()) { + std::string value = (*ctx.jsonObj)[ctx.arrayIndex].get(); + ctx.arrayIndex++; + return value; + } + } + + return ""; +} + +void +docReaderJson::pushContext(nlohmann::json* obj, const std::string& name, ContextType type) +{ + m_contextStack.push(Context(obj, name, type)); +} + +bool +docReaderJson::popContext(ContextType expectedType) +{ + if (m_contextStack.empty()) { + return false; + } + + const Context& ctx = m_contextStack.top(); + if (ctx.type != expectedType) { + return false; + } + + m_contextStack.pop(); + return true; +} + +nlohmann::json* +docReaderJson::getCurrentObject() +{ + if (m_contextStack.empty()) { + return m_current; + } + return m_contextStack.top().jsonObj; +} diff --git a/renderlib/serialize/docReaderJson.h b/renderlib/serialize/docReaderJson.h new file mode 100644 index 00000000..8356c36b --- /dev/null +++ b/renderlib/serialize/docReaderJson.h @@ -0,0 +1,144 @@ +#pragma once + +#include "docReader.h" + +#include "json/json.hpp" + +#include +#include +#include + +class docReaderJson : public docReader +{ +public: + docReaderJson(); + virtual ~docReaderJson(); + + // Document lifecycle + virtual bool beginDocument(std::string filePath) override; + virtual void endDocument() override; + + // Object support + virtual bool beginObject(const std::string& i_name) override; + virtual void endObject() override; + + // List/array support + virtual bool beginList(const std::string& i_name) override; + virtual void endList() override; + + // Key checking + virtual bool hasKey(const std::string& key) override; + + // Peek operations + virtual std::string peekObjectType() override; + virtual uint32_t peekVersion() override; + virtual std::string peekObjectName() override; + + // Property reading + virtual bool readPrty(prtyProperty* p) override; + + // Primitive type reading - all require a name parameter + virtual bool readBool(const std::string& name) override; + virtual int8_t readInt8(const std::string& name) override; + virtual int16_t readInt16(const std::string& name) override; + virtual int32_t readInt32(const std::string& name) override; + virtual int64_t readInt64(const std::string& name) override; + virtual uint8_t readUint8(const std::string& name) override; + virtual uint16_t readUint16(const std::string& name) override; + virtual uint32_t readUint32(const std::string& name) override; + virtual uint64_t readUint64(const std::string& name) override; + virtual float readFloat32(const std::string& name) override; + virtual std::vector readFloat32Array(const std::string& name) override; + virtual std::vector readInt32Array(const std::string& name) override; + virtual std::vector readUint32Array(const std::string& name) override; + virtual std::string readString(const std::string& name) override; + +private: + enum class ContextType + { + Object, + Array + }; + + struct Context + { + nlohmann::json* jsonObj; + std::string name; + ContextType type; + size_t arrayIndex; // For tracking position in arrays + + Context(nlohmann::json* obj, const std::string& n, ContextType t) + : jsonObj(obj) + , name(n) + , type(t) + , arrayIndex(0) + { + } + + bool isArray() const { return type == ContextType::Array; } + bool isObject() const { return type == ContextType::Object; } + }; + + void pushContext(nlohmann::json* obj, const std::string& name, ContextType type); + bool popContext(ContextType expectedType); + nlohmann::json* getCurrentObject(); + + // Template helpers to reduce duplication in integer reading + template + T readSignedIntegerValue(const std::string& name) + { + nlohmann::json* current = getCurrentObject(); + if (!current) { + return 0; + } + + if (m_contextStack.empty() || !m_contextStack.top().isArray()) { + // Reading from object by key + if (current->contains(name) && (*current)[name].is_number_integer()) { + return (*current)[name].get(); + } + } else { + // Reading from array by index + Context& ctx = m_contextStack.top(); + if (ctx.arrayIndex < ctx.jsonObj->size() && (*ctx.jsonObj)[ctx.arrayIndex].is_number_integer()) { + T value = (*ctx.jsonObj)[ctx.arrayIndex].get(); + ctx.arrayIndex++; + return value; + } + } + + return 0; + } + + template + T readUnsignedIntegerValue(const std::string& name) + { + nlohmann::json* current = getCurrentObject(); + if (!current) { + return 0; + } + + if (m_contextStack.empty() || !m_contextStack.top().isArray()) { + // Reading from object by key + if (current->contains(name) && (*current)[name].is_number_unsigned()) { + return (*current)[name].get(); + } + } else { + // Reading from array by index + Context& ctx = m_contextStack.top(); + if (ctx.arrayIndex < ctx.jsonObj->size() && (*ctx.jsonObj)[ctx.arrayIndex].is_number_unsigned()) { + T value = (*ctx.jsonObj)[ctx.arrayIndex].get(); + ctx.arrayIndex++; + return value; + } + } + + return 0; + } + + nlohmann::json* m_root; + nlohmann::json* m_current; + std::stack m_contextStack; + std::string m_nextKey; + std::string m_filePath; +}; diff --git a/renderlib/serialize/docReaderYaml.cpp b/renderlib/serialize/docReaderYaml.cpp new file mode 100644 index 00000000..89923d42 --- /dev/null +++ b/renderlib/serialize/docReaderYaml.cpp @@ -0,0 +1,749 @@ +#include "docReaderYaml.h" + +#include "SerializationConstants.h" +#include "core/prty/prtyProperty.hpp" +#include "Logging.h" + +#include +#include +#include + +docReaderYaml::docReaderYaml() + : m_current(nullptr) + , m_nextKey("") +{ + m_root.data = YamlObject(); +} + +docReaderYaml::~docReaderYaml() {} + +bool +docReaderYaml::beginDocument(std::string filePath) +{ + m_filePath = filePath; + m_root.data = YamlObject(); + + if (!parseYaml(filePath)) { + LOG_ERROR << "Failed to parse YAML file: " << filePath; + return false; + } + + m_current = &m_root; + + // Clear the context stack + while (!m_contextStack.empty()) { + m_contextStack.pop(); + } + + return true; +} + +void +docReaderYaml::endDocument() +{ + if (!m_contextStack.empty()) { + LOG_ERROR << "endDocument() called with " << m_contextStack.size() + << " unclosed context(s). Document may be incomplete."; + } +} + +bool +docReaderYaml::beginObject(const std::string& i_name) +{ + if (!m_current) { + LOG_ERROR << "beginObject() called with null current value"; + return false; + } + + YamlValue* targetObj = nullptr; + + if (m_contextStack.empty()) { + // Root level object + if (m_current->isObject()) { + YamlObject& obj = m_current->asObject(); + if (obj.find(i_name) != obj.end() && obj[i_name].isObject()) { + targetObj = &obj[i_name]; + } + } + } else { + const Context& ctx = m_contextStack.top(); + if (ctx.isArray() && ctx.value->isArray()) { + // Reading object from an array + YamlArray& arr = ctx.value->asArray(); + if (ctx.arrayIndex < arr.size() && arr[ctx.arrayIndex].isObject()) { + targetObj = &arr[ctx.arrayIndex]; + } + } else if (ctx.isObject() && ctx.value->isObject()) { + // Reading object from an object + YamlObject& obj = ctx.value->asObject(); + if (obj.find(i_name) != obj.end() && obj[i_name].isObject()) { + targetObj = &obj[i_name]; + } + } + } + + if (!targetObj) { + LOG_ERROR << "beginObject() - object not found: " << i_name; + return false; + } + + pushContext(targetObj, i_name, ContextType::Object); + return true; +} + +void +docReaderYaml::endObject() +{ + if (m_contextStack.empty()) { + LOG_ERROR << "endObject() called with no matching beginObject()"; + return; + } + + if (!popContext(ContextType::Object)) { + LOG_ERROR << "endObject() called but current context is not an object"; + } +} + +bool +docReaderYaml::beginList(const std::string& i_name) +{ + if (!m_current) { + LOG_ERROR << "beginList() called with null current value"; + return false; + } + + YamlValue* targetArray = nullptr; + + if (m_contextStack.empty()) { + // Root level array + if (m_current->isObject()) { + YamlObject& obj = m_current->asObject(); + if (obj.find(i_name) != obj.end() && obj[i_name].isArray()) { + targetArray = &obj[i_name]; + } + } + } else { + const Context& ctx = m_contextStack.top(); + if (ctx.isArray() && ctx.value->isArray()) { + // Reading array from an array + YamlArray& arr = ctx.value->asArray(); + if (ctx.arrayIndex < arr.size() && arr[ctx.arrayIndex].isArray()) { + targetArray = &arr[ctx.arrayIndex]; + } + } else if (ctx.isObject() && ctx.value->isObject()) { + // Reading array from an object + YamlObject& obj = ctx.value->asObject(); + if (obj.find(i_name) != obj.end() && obj[i_name].isArray()) { + targetArray = &obj[i_name]; + } + } + } + + if (!targetArray) { + LOG_ERROR << "beginList() - array not found: " << i_name; + return false; + } + + pushContext(targetArray, i_name, ContextType::Array); + return true; +} + +void +docReaderYaml::endList() +{ + if (m_contextStack.empty()) { + LOG_ERROR << "endList() called with no matching beginList()"; + return; + } + + if (!popContext(ContextType::Array)) { + LOG_ERROR << "endList() called but current context is not an array"; + } +} + +bool +docReaderYaml::hasKey(const std::string& key) +{ + YamlValue* current = getCurrentValue(); + if (!current || !current->isObject()) { + return false; + } + YamlObject& obj = current->asObject(); + return obj.find(key) != obj.end(); +} + +std::string +docReaderYaml::peekObjectType() +{ + YamlValue* current = getCurrentValue(); + if (!current || !current->isObject()) { + return ""; + } + + YamlObject& obj = current->asObject(); + if (obj.find(SerializationConstants::TYPE_KEY) != obj.end() && obj[SerializationConstants::TYPE_KEY].isString()) { + return obj[SerializationConstants::TYPE_KEY].asString(); + } + + return ""; +} + +uint32_t +docReaderYaml::peekVersion() +{ + YamlValue* current = getCurrentValue(); + if (!current || !current->isObject()) { + return 0; + } + + YamlObject& obj = current->asObject(); + if (obj.find(SerializationConstants::VERSION_KEY) != obj.end() && + obj[SerializationConstants::VERSION_KEY].isString()) { + return stringToInt(obj[SerializationConstants::VERSION_KEY].asString()); + } + + return 0; +} + +std::string +docReaderYaml::peekObjectName() +{ + YamlValue* current = getCurrentValue(); + if (!current || !current->isObject()) { + return ""; + } + + YamlObject& obj = current->asObject(); + if (obj.find(SerializationConstants::NAME_KEY) != obj.end() && obj[SerializationConstants::NAME_KEY].isString()) { + return obj[SerializationConstants::NAME_KEY].asString(); + } + + return ""; +} + +bool +docReaderYaml::readPrty(prtyProperty* p) +{ + if (!p) { + return false; + } + + // Store the property name for the next read operation + m_nextKey = p->GetPropertyName(); + + // Check if the key exists + if (!hasKey(m_nextKey.c_str())) { + LOG_ERROR << "readPrty() - property key not found: " << m_nextKey; + return false; + } + + // Let the property read itself + p->Read(*this); + return true; +} + +bool +docReaderYaml::readBool(const std::string& name) +{ + YamlValue* current = getCurrentValue(); + if (!current) { + return false; + } + + if (m_contextStack.empty() || !m_contextStack.top().isArray()) { + // Reading from object by key + if (current->isObject()) { + YamlObject& obj = current->asObject(); + if (obj.find(name) != obj.end() && obj[name].isString()) { + return stringToBool(obj[name].asString()); + } + } + } else { + // Reading from array by index + Context& ctx = m_contextStack.top(); + if (ctx.value->isArray()) { + YamlArray& arr = ctx.value->asArray(); + if (ctx.arrayIndex < arr.size() && arr[ctx.arrayIndex].isString()) { + bool value = stringToBool(arr[ctx.arrayIndex].asString()); + ctx.arrayIndex++; + return value; + } + } + } + + return false; +} + +int8_t +docReaderYaml::readInt8(const std::string& name) +{ + return readIntegerValue(name, [this](const std::string& s) { return stringToInt(s); }); +} + +int16_t +docReaderYaml::readInt16(const std::string& name) +{ + return readIntegerValue(name, [this](const std::string& s) { return stringToInt(s); }); +} + +int32_t +docReaderYaml::readInt32(const std::string& name) +{ + return readIntegerValue(name, [this](const std::string& s) { return stringToInt(s); }); +} + +int64_t +docReaderYaml::readInt64(const std::string& name) +{ + return readIntegerValue(name, [this](const std::string& s) { return stringToInt64(s); }); +} + +uint8_t +docReaderYaml::readUint8(const std::string& name) +{ + return readIntegerValue(name, [this](const std::string& s) { return stringToUint(s); }); +} + +uint16_t +docReaderYaml::readUint16(const std::string& name) +{ + return readIntegerValue(name, [this](const std::string& s) { return stringToUint(s); }); +} + +uint32_t +docReaderYaml::readUint32(const std::string& name) +{ + return readIntegerValue(name, [this](const std::string& s) { return stringToUint(s); }); +} + +uint64_t +docReaderYaml::readUint64(const std::string& name) +{ + return readIntegerValue(name, [this](const std::string& s) { return stringToUint(s); }); +} + +float +docReaderYaml::readFloat32(const std::string& name) +{ + YamlValue* current = getCurrentValue(); + if (!current) { + return 0.0f; + } + + if (m_contextStack.empty() || !m_contextStack.top().isArray()) { + // Reading from object by key + if (current->isObject()) { + YamlObject& obj = current->asObject(); + if (obj.find(name) != obj.end() && obj[name].isString()) { + return stringToFloat(obj[name].asString()); + } + } + } else { + // Reading from array by index + Context& ctx = m_contextStack.top(); + if (ctx.value->isArray()) { + YamlArray& arr = ctx.value->asArray(); + if (ctx.arrayIndex < arr.size() && arr[ctx.arrayIndex].isString()) { + float value = stringToFloat(arr[ctx.arrayIndex].asString()); + ctx.arrayIndex++; + return value; + } + } + } + + return 0.0f; +} + +std::vector +docReaderYaml::readFloat32Array(const std::string& name) +{ + std::vector result; + YamlValue* current = getCurrentValue(); + if (!current || !current->isObject()) { + return result; + } + + YamlObject& obj = current->asObject(); + if (obj.find(name) != obj.end() && obj[name].isArray()) { + YamlArray& arr = obj[name].asArray(); + for (const auto& elem : arr) { + if (elem.isString()) { + result.push_back(stringToFloat(elem.asString())); + } + } + } + + return result; +} + +std::vector +docReaderYaml::readInt32Array(const std::string& name) +{ + std::vector result; + YamlValue* current = getCurrentValue(); + if (!current || !current->isObject()) { + return result; + } + + YamlObject& obj = current->asObject(); + if (obj.find(name) != obj.end() && obj[name].isArray()) { + YamlArray& arr = obj[name].asArray(); + for (const auto& elem : arr) { + if (elem.isString()) { + result.push_back(stringToInt(elem.asString())); + } + } + } + + return result; +} + +std::vector +docReaderYaml::readUint32Array(const std::string& name) +{ + std::vector result; + YamlValue* current = getCurrentValue(); + if (!current || !current->isObject()) { + return result; + } + + YamlObject& obj = current->asObject(); + if (obj.find(name) != obj.end() && obj[name].isArray()) { + YamlArray& arr = obj[name].asArray(); + for (const auto& elem : arr) { + if (elem.isString()) { + result.push_back(static_cast(stringToInt(elem.asString()))); + } + } + } + + return result; +} + +std::string +docReaderYaml::readString(const std::string& name) +{ + YamlValue* current = getCurrentValue(); + if (!current) { + return ""; + } + + if (m_contextStack.empty() || !m_contextStack.top().isArray()) { + // Reading from object by key + if (current->isObject()) { + YamlObject& obj = current->asObject(); + if (obj.find(name) != obj.end() && obj[name].isString()) { + return obj[name].asString(); + } + } + } else { + // Reading from array by index + Context& ctx = m_contextStack.top(); + if (ctx.value->isArray()) { + YamlArray& arr = ctx.value->asArray(); + if (ctx.arrayIndex < arr.size() && arr[ctx.arrayIndex].isString()) { + std::string value = arr[ctx.arrayIndex].asString(); + ctx.arrayIndex++; + return value; + } + } + } + + return ""; +} + +bool +docReaderYaml::parseYaml(const std::string& filePath) +{ + std::ifstream file(filePath); + if (!file.is_open()) { + return false; + } + + // Skip YAML header if present + std::string line; + if (std::getline(file, line) && line != "---") { + file.seekg(0); // Reset if not a header + } + + int currentIndent = 0; + m_root.data = parseObject(file, currentIndent, 0); + + file.close(); + return true; +} + +docReaderYaml::YamlObject +docReaderYaml::parseObject(std::ifstream& file, int& currentIndent, int baseIndent) +{ + YamlObject obj; + std::string line; + std::streampos lastPos = file.tellg(); + + while (std::getline(file, line)) { + if (line.empty() || line.find_first_not_of(" \t") == std::string::npos) { + continue; // Skip empty lines + } + + std::string key; + int indent = getIndent(line); + std::string value = parseLine(line, key, indent); + + if (indent < baseIndent) { + // We've dedented, go back and return + file.seekg(lastPos); + currentIndent = indent; + break; + } + + if (indent == baseIndent && !key.empty()) { + if (value.empty()) { + // Check next line for nested content + lastPos = file.tellg(); + std::string nextLine; + if (std::getline(file, nextLine)) { + int nextIndent = getIndent(nextLine); + file.seekg(lastPos); + + if (nextIndent > indent) { + // Parse nested object + int nestedIndent = 0; + YamlValue nestedValue; + + if (nextLine.find('-') != std::string::npos && nextLine.find_first_not_of(" \t") == nextLine.find('-')) { + // It's an array + nestedValue.data = parseArray(file, nestedIndent, indent + 2); + } else { + // It's an object + nestedValue.data = parseObject(file, nestedIndent, indent + 2); + } + obj[key] = nestedValue; + } else { + // Empty value + YamlValue emptyValue; + emptyValue.data = std::string(""); + obj[key] = emptyValue; + } + } + } else { + // Simple key-value pair - check if it's an inline array + YamlValue yamlValue; + + // Check if value is an inline array (starts with [ and ends with ]) + if (!value.empty() && value.front() == '[' && value.back() == ']') { + // Parse inline array + YamlArray arr; + std::string content = value.substr(1, value.length() - 2); // Remove [ and ] + + if (!content.empty()) { + size_t start = 0; + size_t end = 0; + + while ((end = content.find(',', start)) != std::string::npos) { + std::string element = trimString(content.substr(start, end - start)); + if (!element.empty()) { + YamlValue elemValue; + elemValue.data = element; + arr.push_back(elemValue); + } + start = end + 1; + } + + // Add the last element + std::string element = trimString(content.substr(start)); + if (!element.empty()) { + YamlValue elemValue; + elemValue.data = element; + arr.push_back(elemValue); + } + } + + yamlValue.data = arr; + } else { + yamlValue.data = value; + } + + obj[key] = yamlValue; + } + } + + lastPos = file.tellg(); + } + + return obj; +} + +docReaderYaml::YamlArray +docReaderYaml::parseArray(std::ifstream& file, int& currentIndent, int baseIndent) +{ + YamlArray arr; + std::string line; + std::streampos lastPos = file.tellg(); + + while (std::getline(file, line)) { + if (line.empty() || line.find_first_not_of(" \t") == std::string::npos) { + continue; + } + + int indent = getIndent(line); + + if (indent < baseIndent) { + file.seekg(lastPos); + currentIndent = indent; + break; + } + + if (indent == baseIndent) { + size_t dashPos = line.find('-'); + if (dashPos != std::string::npos && dashPos == line.find_first_not_of(" \t")) { + std::string value = trimString(line.substr(dashPos + 1)); + + if (value.empty()) { + // Check for nested content + lastPos = file.tellg(); + std::string nextLine; + if (std::getline(file, nextLine)) { + int nextIndent = getIndent(nextLine); + file.seekg(lastPos); + + if (nextIndent > indent) { + YamlValue nestedValue; + int nestedIndent = 0; + nestedValue.data = parseObject(file, nestedIndent, indent + 2); + arr.push_back(nestedValue); + } + } + } else { + YamlValue yamlValue; + yamlValue.data = value; + arr.push_back(yamlValue); + } + } + } + + lastPos = file.tellg(); + } + + return arr; +} + +std::string +docReaderYaml::parseLine(const std::string& line, std::string& key, int& indent) +{ + indent = getIndent(line); + size_t colonPos = line.find(':'); + + if (colonPos != std::string::npos) { + key = trimString(line.substr(0, colonPos)); + std::string value = trimString(line.substr(colonPos + 1)); + return value; + } + + return ""; +} + +std::string +docReaderYaml::trimString(const std::string& str) +{ + size_t start = str.find_first_not_of(" \t\r\n"); + if (start == std::string::npos) { + return ""; + } + size_t end = str.find_last_not_of(" \t\r\n"); + return str.substr(start, end - start + 1); +} + +int +docReaderYaml::getIndent(const std::string& line) +{ + int indent = 0; + for (char c : line) { + if (c == ' ') { + indent++; + } else if (c == '\t') { + indent += 2; // Treat tab as 2 spaces + } else { + break; + } + } + return indent; +} + +bool +docReaderYaml::stringToBool(const std::string& str) +{ + std::string lower = str; + std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); + return (lower == "true" || lower == "yes" || lower == "1"); +} + +int +docReaderYaml::stringToInt(const std::string& str) +{ + try { + return std::stoi(str); + } catch (...) { + return 0; + } +} + +int64_t +docReaderYaml::stringToInt64(const std::string& str) +{ + try { + return std::stoll(str); + } catch (...) { + return 0; + } +} + +uint32_t +docReaderYaml::stringToUint(const std::string& str) +{ + try { + return static_cast(std::stoull(str)); + } catch (...) { + return 0; + } +} + +float +docReaderYaml::stringToFloat(const std::string& str) +{ + try { + return std::stof(str); + } catch (...) { + return 0.0f; + } +} + +void +docReaderYaml::pushContext(YamlValue* val, const std::string& name, ContextType type) +{ + m_contextStack.push(Context(val, name, type)); +} + +bool +docReaderYaml::popContext(ContextType expectedType) +{ + if (m_contextStack.empty()) { + return false; + } + + const Context& ctx = m_contextStack.top(); + if (ctx.type != expectedType) { + return false; + } + + m_contextStack.pop(); + return true; +} + +docReaderYaml::YamlValue* +docReaderYaml::getCurrentValue() +{ + if (m_contextStack.empty()) { + return m_current; + } + return m_contextStack.top().value; +} diff --git a/renderlib/serialize/docReaderYaml.h b/renderlib/serialize/docReaderYaml.h new file mode 100644 index 00000000..6ab2dd86 --- /dev/null +++ b/renderlib/serialize/docReaderYaml.h @@ -0,0 +1,163 @@ +#pragma once + +#include "docReader.h" + +#include +#include +#include +#include +#include +#include + +class docReaderYaml : public docReader +{ +public: + docReaderYaml(); + virtual ~docReaderYaml(); + + // Document lifecycle + virtual bool beginDocument(std::string filePath) override; + virtual void endDocument() override; + + // Object support + virtual bool beginObject(const std::string& i_name) override; + virtual void endObject() override; + + // List/array support + virtual bool beginList(const std::string& i_name) override; + virtual void endList() override; + + // Key checking + virtual bool hasKey(const std::string& key) override; + + // Peek operations + virtual std::string peekObjectType() override; + virtual uint32_t peekVersion() override; + virtual std::string peekObjectName() override; + + // Property reading + virtual bool readPrty(prtyProperty* p) override; + + // Primitive type reading - all require a name parameter + virtual bool readBool(const std::string& name) override; + virtual int8_t readInt8(const std::string& name) override; + virtual int16_t readInt16(const std::string& name) override; + virtual int32_t readInt32(const std::string& name) override; + virtual int64_t readInt64(const std::string& name) override; + virtual uint8_t readUint8(const std::string& name) override; + virtual uint16_t readUint16(const std::string& name) override; + virtual uint32_t readUint32(const std::string& name) override; + virtual uint64_t readUint64(const std::string& name) override; + virtual float readFloat32(const std::string& name) override; + virtual std::vector readFloat32Array(const std::string& name) override; + virtual std::vector readInt32Array(const std::string& name) override; + virtual std::vector readUint32Array(const std::string& name) override; + virtual std::string readString(const std::string& name) override; + +private: + // Simple YAML value types + struct YamlValue; + using YamlObject = std::map; + using YamlArray = std::vector; + + struct YamlValue + { + std::variant data; + + bool isString() const { return std::holds_alternative(data); } + bool isObject() const { return std::holds_alternative(data); } + bool isArray() const { return std::holds_alternative(data); } + + std::string& asString() { return std::get(data); } + YamlObject& asObject() { return std::get(data); } + YamlArray& asArray() { return std::get(data); } + + const std::string& asString() const { return std::get(data); } + const YamlObject& asObject() const { return std::get(data); } + const YamlArray& asArray() const { return std::get(data); } + }; + + enum class ContextType + { + Object, + Array + }; + + struct Context + { + YamlValue* value; + std::string name; + ContextType type; + size_t arrayIndex; // For tracking position in arrays + + Context(YamlValue* val, const std::string& n, ContextType t) + : value(val) + , name(n) + , type(t) + , arrayIndex(0) + { + } + + bool isArray() const { return type == ContextType::Array; } + bool isObject() const { return type == ContextType::Object; } + }; + + // Parsing helpers + bool parseYaml(const std::string& filePath); + YamlValue parseValue(std::ifstream& file, int& currentIndent, int expectedIndent); + YamlObject parseObject(std::ifstream& file, int& currentIndent, int baseIndent); + YamlArray parseArray(std::ifstream& file, int& currentIndent, int baseIndent); + std::string parseLine(const std::string& line, std::string& key, int& indent); + std::string trimString(const std::string& str); + int getIndent(const std::string& line); + + // Type conversion helpers + bool stringToBool(const std::string& str); + int stringToInt(const std::string& str); + int64_t stringToInt64(const std::string& str); + uint32_t stringToUint(const std::string& str); + float stringToFloat(const std::string& str); + + void pushContext(YamlValue* val, const std::string& name, ContextType type); + bool popContext(ContextType expectedType); + YamlValue* getCurrentValue(); + + // Template helper to reduce duplication in integer reading + template + T readIntegerValue(const std::string& name, ConvFunc convFunc) + { + YamlValue* current = getCurrentValue(); + if (!current) { + return 0; + } + + if (m_contextStack.empty() || !m_contextStack.top().isArray()) { + // Reading from object by key + if (current->isObject()) { + YamlObject& obj = current->asObject(); + if (obj.find(name) != obj.end() && obj[name].isString()) { + return static_cast(convFunc(obj[name].asString())); + } + } + } else { + // Reading from array by index + Context& ctx = m_contextStack.top(); + if (ctx.value->isArray()) { + YamlArray& arr = ctx.value->asArray(); + if (ctx.arrayIndex < arr.size() && arr[ctx.arrayIndex].isString()) { + T value = static_cast(convFunc(arr[ctx.arrayIndex].asString())); + ctx.arrayIndex++; + return value; + } + } + } + + return 0; + } + + YamlValue m_root; + YamlValue* m_current; + std::stack m_contextStack; + std::string m_nextKey; + std::string m_filePath; +}; diff --git a/renderlib/serialize/docWriter.cpp b/renderlib/serialize/docWriter.cpp new file mode 100644 index 00000000..99189117 --- /dev/null +++ b/renderlib/serialize/docWriter.cpp @@ -0,0 +1,4 @@ +#include "docWriter.h" + +#include "core/prty/prtyProperty.hpp" +#include "core/prty/prtyObject.hpp" diff --git a/renderlib/serialize/docWriter.h b/renderlib/serialize/docWriter.h new file mode 100644 index 00000000..404dfcf6 --- /dev/null +++ b/renderlib/serialize/docWriter.h @@ -0,0 +1,111 @@ +#pragma once + +#include +#include +#include + +class prtyProperty; +class prtyObject; + +class docWriter +{ +public: + docWriter() {} + virtual ~docWriter() {} + + // this must be called to start and end the document before any other writing can happen. + virtual void beginDocument(std::string filePath) = 0; + virtual void endDocument() = 0; + + // objects can contain other objects, lists, and properties. + virtual void beginObject(const std::string& i_name, const std::string& i_objectType, uint32_t version) = 0; + virtual void endObject() = 0; + + // lists can contain objects or properties. + virtual void beginList(const std::string& i_name) = 0; + virtual void endList() = 0; + + // properties will write their name and associated value using the primitive write methods. + virtual void writePrty(const prtyProperty* p) = 0; + + // Templated property writer that maps value types to primitive write methods + template + size_t writeProperty(const std::string& name, const T& value) + { + if constexpr (std::is_same_v) { + return writeBool(name, value); + } else if constexpr (std::is_same_v) { + return writeInt8(name, value); + } else if constexpr (std::is_same_v) { + return writeInt32(name, value); + } else if constexpr (std::is_same_v) { + return writeUint32(name, value); + } else if constexpr (std::is_same_v) { + return writeFloat32(name, value); + } else if constexpr (std::is_same_v) { + return writeString(name, value); + } else if constexpr (std::is_same_v>) { + return writeFloat32Array(name, value); + } else if constexpr (std::is_same_v>) { + return writeInt32Array(name, value); + } else if constexpr (std::is_same_v>) { + return writeUint32Array(name, value); + } else { + static_assert(sizeof(T) == 0, "Unsupported type for writeProperty"); + return 0; + } + } + + // Templated integer write method + template + size_t writeInt(const std::string& name, T value) + { + if constexpr (std::is_same_v) { + return writeInt8(name, value); + } else if constexpr (std::is_same_v) { + return writeInt16(name, value); + } else if constexpr (std::is_same_v || std::is_same_v) { + return writeInt32(name, value); + } else if constexpr (std::is_same_v) { + return writeInt64(name, value); + } else { + static_assert(sizeof(T) == 0, "Unsupported signed integer type for writeInt"); + return 0; + } + } + + // Templated unsigned integer write method + template + size_t writeUint(const std::string& name, T value) + { + if constexpr (std::is_same_v) { + return writeUint8(name, value); + } else if constexpr (std::is_same_v) { + return writeUint16(name, value); + } else if constexpr (std::is_same_v) { + return writeUint32(name, value); + } else if constexpr (std::is_same_v) { + return writeUint64(name, value); + } else { + static_assert(sizeof(T) == 0, "Unsupported unsigned integer type for writeUint"); + return 0; + } + } + + // All primitive write methods now require a name parameter + virtual size_t writeBool(const std::string& name, bool value) = 0; + virtual size_t writeInt8(const std::string& name, int8_t value) = 0; + virtual size_t writeInt16(const std::string& name, int16_t value) = 0; + virtual size_t writeInt32(const std::string& name, int32_t value) = 0; + virtual size_t writeInt64(const std::string& name, int64_t value) = 0; + virtual size_t writeUint8(const std::string& name, uint8_t value) = 0; + virtual size_t writeUint16(const std::string& name, uint16_t value) = 0; + virtual size_t writeUint32(const std::string& name, uint32_t value) = 0; + virtual size_t writeUint64(const std::string& name, uint64_t value) = 0; + virtual size_t writeFloat32(const std::string& name, float value) = 0; + virtual size_t writeFloat32Array(const std::string& name, const std::vector& value) = 0; + virtual size_t writeFloat32Array(const std::string& name, size_t count, const float* values) = 0; + virtual size_t writeInt32Array(const std::string& name, const std::vector& value) = 0; + virtual size_t writeUint32Array(const std::string& name, const std::vector& value) = 0; + virtual size_t writeString(const std::string& name, const std::string& value) = 0; +}; diff --git a/renderlib/serialize/docWriterJson.cpp b/renderlib/serialize/docWriterJson.cpp new file mode 100644 index 00000000..8a7658ef --- /dev/null +++ b/renderlib/serialize/docWriterJson.cpp @@ -0,0 +1,364 @@ +#include "docWriterJson.h" + +#include "SerializationConstants.h" + +#include "core/prty/prtyProperty.hpp" +#include "Logging.h" + +#include "json/json.hpp" + +#include + +docWriterJson::docWriterJson() + : m_root(nullptr) + , m_current(nullptr) +{ +} + +docWriterJson::~docWriterJson() +{ + if (m_root) { + delete m_root; + m_root = nullptr; + } +} + +void +docWriterJson::beginDocument(std::string filePath) +{ + m_filePath = filePath; + if (m_root) { + delete m_root; + } + m_root = new nlohmann::json(nlohmann::json::object()); + m_current = m_root; + + // Clear the context stack + while (!m_contextStack.empty()) { + m_contextStack.pop(); + } +} + +void +docWriterJson::endDocument() +{ + if (!m_root) { + return; + } + + // Validate that all contexts are closed + if (!m_contextStack.empty()) { + logError("endDocument() called with " + std::to_string(m_contextStack.size()) + + " unclosed context(s). Document may be incomplete."); + } + + // Write the JSON to file + std::ofstream outFile(m_filePath); + if (outFile.is_open()) { + outFile << m_root->dump(2); // Pretty print with 2-space indentation + outFile.close(); + } +} + +void +docWriterJson::beginObject(const std::string& i_name, const std::string& i_objectType, uint32_t version) +{ + nlohmann::json* newObj = new nlohmann::json(nlohmann::json::object()); + + // Add _name, _type and _version metadata + (*newObj)[SerializationConstants::TYPE_KEY] = i_objectType; + (*newObj)[SerializationConstants::VERSION_KEY] = version; + (*newObj)[SerializationConstants::NAME_KEY] = i_name; + + if (m_contextStack.empty()) { + // Root level object + (*m_current)[i_name] = *newObj; + pushContext(&(*m_current)[i_name], i_name, ContextType::Object); + } else { + const Context& ctx = m_contextStack.top(); + if (ctx.isArray()) { + // Adding object to an array + ctx.jsonObj->push_back(*newObj); + pushContext(&ctx.jsonObj->back(), i_name, ContextType::Object); + } else { + // Adding object to an object + (*ctx.jsonObj)[i_name] = *newObj; + pushContext(&(*ctx.jsonObj)[i_name], i_name, ContextType::Object); + } + } + + delete newObj; +} + +void +docWriterJson::endObject() +{ + if (m_contextStack.empty()) { + logError("endObject() called with no matching beginObject()"); + return; + } + + if (!popContext(ContextType::Object)) { + logError("endObject() called but current context is not an object"); + } +} + +void +docWriterJson::beginList(const std::string& i_name) +{ + nlohmann::json* newArray = new nlohmann::json(nlohmann::json::array()); + + if (m_contextStack.empty()) { + // Root level array + (*m_current)[i_name] = *newArray; + pushContext(&(*m_current)[i_name], i_name, ContextType::Array); + } else { + const Context& ctx = m_contextStack.top(); + if (ctx.isArray()) { + // Adding array to an array + ctx.jsonObj->push_back(*newArray); + pushContext(&ctx.jsonObj->back(), i_name, ContextType::Array); + } else { + // Adding array to an object + (*ctx.jsonObj)[i_name] = *newArray; + pushContext(&(*ctx.jsonObj)[i_name], i_name, ContextType::Array); + } + } + + delete newArray; +} + +void +docWriterJson::endList() +{ + if (m_contextStack.empty()) { + logError("endList() called with no matching beginList()"); + return; + } + + if (!popContext(ContextType::Array)) { + logError("endList() called but current context is not an array"); + } +} + +void +docWriterJson::writePrty(const prtyProperty* p) +{ + if (!p) { + return; + } + + p->Write(*this); +} + +size_t +docWriterJson::writeBool(const std::string& name, bool value) +{ + nlohmann::json* current = getCurrentObject(); + if (!current) { + return 0; + } + + if (m_contextStack.empty() || !m_contextStack.top().isArray()) { + (*current)[name] = value; + } else { + current->push_back(value); + } + + return sizeof(bool); +} + +size_t +docWriterJson::writeInt8(const std::string& name, int8_t value) +{ + return writeIntegerValue(name, value); +} + +size_t +docWriterJson::writeInt16(const std::string& name, int16_t value) +{ + return writeIntegerValue(name, value); +} + +size_t +docWriterJson::writeInt32(const std::string& name, int32_t value) +{ + return writeIntegerValue(name, value); +} + +size_t +docWriterJson::writeInt64(const std::string& name, int64_t value) +{ + return writeIntegerValue(name, value); +} + +size_t +docWriterJson::writeUint8(const std::string& name, uint8_t value) +{ + return writeIntegerValue(name, value); +} + +size_t +docWriterJson::writeUint16(const std::string& name, uint16_t value) +{ + return writeIntegerValue(name, value); +} + +size_t +docWriterJson::writeUint32(const std::string& name, uint32_t value) +{ + return writeIntegerValue(name, value); +} + +size_t +docWriterJson::writeUint64(const std::string& name, uint64_t value) +{ + return writeIntegerValue(name, value); +} + +size_t +docWriterJson::writeFloat32(const std::string& name, float value) +{ + nlohmann::json* current = getCurrentObject(); + if (!current) { + return 0; + } + + if (m_contextStack.empty() || !m_contextStack.top().isArray()) { + (*current)[name] = value; + } else { + current->push_back(value); + } + + return sizeof(float); +} + +size_t +docWriterJson::writeFloat32Array(const std::string& name, const std::vector& value) +{ + return writeFloat32Array(name, value.size(), value.data()); +} + +size_t +docWriterJson::writeFloat32Array(const std::string& name, size_t count, const float* values) +{ + nlohmann::json* current = getCurrentObject(); + if (!current) { + return 0; + } + + nlohmann::json arr = nlohmann::json::array(); + for (size_t i = 0; i < count; ++i) { + arr.push_back(values[i]); + } + + if (m_contextStack.empty() || !m_contextStack.top().isArray()) { + (*current)[name] = arr; + } else { + current->push_back(arr); + } + + return count * sizeof(float); +} + +size_t +docWriterJson::writeInt32Array(const std::string& name, const std::vector& value) +{ + nlohmann::json* current = getCurrentObject(); + if (!current) { + return 0; + } + + nlohmann::json arr = nlohmann::json::array(); + for (int32_t v : value) { + arr.push_back(v); + } + + if (m_contextStack.empty() || !m_contextStack.top().isArray()) { + (*current)[name] = arr; + } else { + current->push_back(arr); + } + + return value.size() * sizeof(int32_t); +} + +size_t +docWriterJson::writeUint32Array(const std::string& name, const std::vector& value) +{ + nlohmann::json* current = getCurrentObject(); + if (!current) { + return 0; + } + + nlohmann::json arr = nlohmann::json::array(); + for (uint32_t v : value) { + arr.push_back(v); + } + + if (m_contextStack.empty() || !m_contextStack.top().isArray()) { + (*current)[name] = arr; + } else { + current->push_back(arr); + } + + return value.size() * sizeof(uint32_t); +} + +size_t +docWriterJson::writeString(const std::string& name, const std::string& value) +{ + nlohmann::json* current = getCurrentObject(); + if (!current) { + return 0; + } + + if (m_contextStack.empty() || !m_contextStack.top().isArray()) { + (*current)[name] = value; + } else { + current->push_back(value); + } + + return value.size(); +} + +void +docWriterJson::pushContext(nlohmann::json* obj, const std::string& name, ContextType type) +{ + m_contextStack.push(Context(obj, name, type)); +} + +bool +docWriterJson::popContext(ContextType expectedType) +{ + if (m_contextStack.empty()) { + return false; + } + + const Context& ctx = m_contextStack.top(); + bool isCorrectType = (ctx.type == expectedType); + + if (!isCorrectType) { + logError("Mismatched begin/end calls: expected " + + std::string(expectedType == ContextType::Object ? "Object" : "Array") + " but found " + + std::string(ctx.type == ContextType::Object ? "Object" : "Array") + " for context '" + ctx.name + "'"); + } + + m_contextStack.pop(); + return isCorrectType; +} + +void +docWriterJson::logError(const std::string& message) +{ + LOG_ERROR << "docWriterJson: " << message; +} + +nlohmann::json* +docWriterJson::getCurrentObject() +{ + if (m_contextStack.empty()) { + return m_current; + } + return m_contextStack.top().jsonObj; +} diff --git a/renderlib/serialize/docWriterJson.h b/renderlib/serialize/docWriterJson.h new file mode 100644 index 00000000..a1ba98dd --- /dev/null +++ b/renderlib/serialize/docWriterJson.h @@ -0,0 +1,100 @@ +#pragma once + +#include "docWriter.h" + +#include "json/json.hpp" + +#include +#include +#include + +class docWriterJson : public docWriter +{ +public: + docWriterJson(); + virtual ~docWriterJson(); + + // Document lifecycle + virtual void beginDocument(std::string filePath) override; + virtual void endDocument() override; + + // Object support + virtual void beginObject(const std::string& i_name, const std::string& i_objectType, uint32_t version) override; + virtual void endObject() override; + + // List/array support + virtual void beginList(const std::string& i_name) override; + virtual void endList() override; + + // Property writing + virtual void writePrty(const prtyProperty* p) override; + + // Primitive type writing - all require a name parameter + virtual size_t writeBool(const std::string& name, bool value) override; + virtual size_t writeInt8(const std::string& name, int8_t value) override; + virtual size_t writeInt16(const std::string& name, int16_t value) override; + virtual size_t writeInt32(const std::string& name, int32_t value) override; + virtual size_t writeInt64(const std::string& name, int64_t value) override; + virtual size_t writeUint8(const std::string& name, uint8_t value) override; + virtual size_t writeUint16(const std::string& name, uint16_t value) override; + virtual size_t writeUint32(const std::string& name, uint32_t value) override; + virtual size_t writeUint64(const std::string& name, uint64_t value) override; + virtual size_t writeFloat32(const std::string& name, float value) override; + virtual size_t writeFloat32Array(const std::string& name, const std::vector& value) override; + virtual size_t writeFloat32Array(const std::string& name, size_t count, const float* values) override; + virtual size_t writeInt32Array(const std::string& name, const std::vector& value) override; + virtual size_t writeUint32Array(const std::string& name, const std::vector& value) override; + virtual size_t writeString(const std::string& name, const std::string& value) override; + +private: + enum class ContextType + { + Object, + Array + }; + + struct Context + { + nlohmann::json* jsonObj; + std::string name; + ContextType type; + + Context(nlohmann::json* obj, const std::string& n, ContextType t) + : jsonObj(obj) + , name(n) + , type(t) + { + } + + bool isArray() const { return type == ContextType::Array; } + bool isObject() const { return type == ContextType::Object; } + }; + + std::string m_filePath; + nlohmann::json* m_root; + std::stack m_contextStack; + nlohmann::json* m_current; + + void pushContext(nlohmann::json* obj, const std::string& name, ContextType type); + bool popContext(ContextType expectedType); + nlohmann::json* getCurrentObject(); + void logError(const std::string& message); + + // Template helper to reduce duplication in integer writing + template + size_t writeIntegerValue(const std::string& name, T value) + { + nlohmann::json* current = getCurrentObject(); + if (!current) { + return 0; + } + + if (m_contextStack.empty() || !m_contextStack.top().isArray()) { + (*current)[name] = value; + } else { + current->push_back(value); + } + + return sizeof(T); + } +}; diff --git a/renderlib/serialize/docWriterYaml.cpp b/renderlib/serialize/docWriterYaml.cpp new file mode 100644 index 00000000..a171be3e --- /dev/null +++ b/renderlib/serialize/docWriterYaml.cpp @@ -0,0 +1,490 @@ +#include "docWriterYaml.h" + +#include "SerializationConstants.h" + +#include "core/prty/prtyProperty.hpp" +#include "Logging.h" + +#include +#include + +docWriterYaml::docWriterYaml() + : m_indentLevel(0) +{ +} + +docWriterYaml::~docWriterYaml() {} + +void +docWriterYaml::beginDocument(std::string filePath) +{ + m_filePath = filePath; + m_output.str(""); + m_output.clear(); + m_indentLevel = 0; + + // Clear the context stack + while (!m_contextStack.empty()) { + m_contextStack.pop(); + } + + // Write YAML header + m_output << "---\n"; +} + +void +docWriterYaml::endDocument() +{ + // Validate that all contexts are closed + if (!m_contextStack.empty()) { + logError("endDocument() called with " + std::to_string(m_contextStack.size()) + + " unclosed context(s). Document may be incomplete."); + } + + // Write the YAML to file + std::ofstream outFile(m_filePath); + if (outFile.is_open()) { + outFile << m_output.str(); + outFile.close(); + } +} + +void +docWriterYaml::beginObject(const std::string& i_name, const std::string& i_objectType, uint32_t version) +{ + if (m_contextStack.empty()) { + // Root level object + writeKey(i_name); + m_output << "\n"; + m_indentLevel++; + } else { + Context& ctx = m_contextStack.top(); + if (ctx.isArray()) { + // Adding object to an array + writeIndent(); + m_output << "- "; + if (!i_name.empty()) { + m_output << i_name << ":\n"; + } else { + m_output << "\n"; + } + m_indentLevel++; + ctx.firstItem = false; + } else { + // Adding object to an object + writeIndent(); + writeKey(i_name); + m_output << "\n"; + m_indentLevel++; + ctx.firstItem = false; + } + } + + // Write _type and _version metadata + writeIndent(); + writeKey(SerializationConstants::TYPE_KEY); + m_output << escapeString(i_objectType) << "\n"; + + writeIndent(); + writeKey(SerializationConstants::VERSION_KEY); + m_output << version << "\n"; + + writeIndent(); + writeKey(SerializationConstants::NAME_KEY); + m_output << escapeString(i_name) << "\n"; + + pushContext(i_name, ContextType::Object); +} + +void +docWriterYaml::endObject() +{ + if (m_contextStack.empty()) { + logError("endObject() called with no matching beginObject()"); + return; + } + + if (!popContext(ContextType::Object)) { + logError("endObject() called but current context is not an object"); + } + + m_indentLevel--; +} + +void +docWriterYaml::beginList(const std::string& i_name) +{ + if (m_contextStack.empty()) { + // Root level array + writeKey(i_name); + m_output << "\n"; + m_indentLevel++; + } else { + Context& ctx = m_contextStack.top(); + if (ctx.isArray()) { + // Adding array to an array + writeIndent(); + m_output << "- "; + if (!i_name.empty()) { + m_output << i_name << ":\n"; + } else { + m_output << "\n"; + } + m_indentLevel++; + ctx.firstItem = false; + } else { + // Adding array to an object + writeIndent(); + writeKey(i_name); + m_output << "\n"; + m_indentLevel++; + ctx.firstItem = false; + } + } + + pushContext(i_name, ContextType::Array); +} + +void +docWriterYaml::endList() +{ + if (m_contextStack.empty()) { + logError("endList() called with no matching beginList()"); + return; + } + + if (!popContext(ContextType::Array)) { + logError("endList() called but current context is not an array"); + } + + m_indentLevel--; +} + +void +docWriterYaml::writePrty(const prtyProperty* p) +{ + if (!p) { + return; + } + + p->Write(*this); +} + +size_t +docWriterYaml::writeBool(const std::string& name, bool value) +{ + if (m_contextStack.empty()) { + writeKey(name); + m_output << (value ? "true" : "false") << "\n"; + } else { + Context& ctx = m_contextStack.top(); + if (ctx.isArray()) { + writeIndent(); + m_output << "- " << (value ? "true" : "false") << "\n"; + ctx.firstItem = false; + } else { + writeIndent(); + writeKey(name); + m_output << (value ? "true" : "false") << "\n"; + ctx.firstItem = false; + } + } + + return sizeof(bool); +} + +size_t +docWriterYaml::writeInt8(const std::string& name, int8_t value) +{ + return writeIntegerValue(name, static_cast(value)); +} + +size_t +docWriterYaml::writeInt16(const std::string& name, int16_t value) +{ + return writeIntegerValue(name, value); +} + +size_t +docWriterYaml::writeInt32(const std::string& name, int32_t value) +{ + return writeIntegerValue(name, value); +} + +size_t +docWriterYaml::writeInt64(const std::string& name, int64_t value) +{ + return writeIntegerValue(name, value); +} + +size_t +docWriterYaml::writeUint8(const std::string& name, uint8_t value) +{ + return writeIntegerValue(name, static_cast(value)); +} + +size_t +docWriterYaml::writeUint16(const std::string& name, uint16_t value) +{ + return writeIntegerValue(name, value); +} + +size_t +docWriterYaml::writeUint32(const std::string& name, uint32_t value) +{ + return writeIntegerValue(name, value); +} + +size_t +docWriterYaml::writeUint64(const std::string& name, uint64_t value) +{ + return writeIntegerValue(name, value); +} + +size_t +docWriterYaml::writeFloat32(const std::string& name, float value) +{ + if (m_contextStack.empty()) { + writeKey(name); + m_output << std::setprecision(6) << value << "\n"; + } else { + Context& ctx = m_contextStack.top(); + if (ctx.isArray()) { + writeIndent(); + m_output << "- " << std::setprecision(6) << value << "\n"; + ctx.firstItem = false; + } else { + writeIndent(); + writeKey(name); + m_output << std::setprecision(6) << value << "\n"; + ctx.firstItem = false; + } + } + + return sizeof(float); +} + +size_t +docWriterYaml::writeFloat32Array(const std::string& name, const std::vector& value) +{ + return writeFloat32Array(name, value.size(), value.data()); +} + +size_t +docWriterYaml::writeFloat32Array(const std::string& name, size_t count, const float* values) +{ + if (m_contextStack.empty()) { + writeKey(name); + } else { + Context& ctx = m_contextStack.top(); + if (ctx.isArray()) { + writeIndent(); + m_output << "- "; + } else { + writeIndent(); + writeKey(name); + ctx.firstItem = false; + } + } + + // Write as inline array [x, y, z, ...] + m_output << "["; + for (size_t i = 0; i < count; ++i) { + if (i > 0) { + m_output << ", "; + } + m_output << std::setprecision(6) << values[i]; + } + m_output << "]\n"; + + return count * sizeof(float); +} + +size_t +docWriterYaml::writeInt32Array(const std::string& name, const std::vector& value) +{ + if (m_contextStack.empty()) { + writeKey(name); + } else { + Context& ctx = m_contextStack.top(); + if (ctx.isArray()) { + writeIndent(); + m_output << "- "; + } else { + writeIndent(); + writeKey(name); + ctx.firstItem = false; + } + } + + // Write as inline array [x, y, z, ...] + m_output << "["; + for (size_t i = 0; i < value.size(); ++i) { + if (i > 0) { + m_output << ", "; + } + m_output << value[i]; + } + m_output << "]\n"; + + return value.size() * sizeof(int32_t); +} + +size_t +docWriterYaml::writeUint32Array(const std::string& name, const std::vector& value) +{ + if (m_contextStack.empty()) { + writeKey(name); + } else { + Context& ctx = m_contextStack.top(); + if (ctx.isArray()) { + writeIndent(); + m_output << "- "; + } else { + writeIndent(); + writeKey(name); + ctx.firstItem = false; + } + } + + // Write as inline array [x, y, z, ...] + m_output << "["; + for (size_t i = 0; i < value.size(); ++i) { + if (i > 0) { + m_output << ", "; + } + m_output << value[i]; + } + m_output << "]\n"; + + return value.size() * sizeof(uint32_t); +} + +size_t +docWriterYaml::writeString(const std::string& name, const std::string& value) +{ + std::string escaped = escapeString(value); + + if (m_contextStack.empty()) { + writeKey(name); + m_output << escaped << "\n"; + } else { + Context& ctx = m_contextStack.top(); + if (ctx.isArray()) { + writeIndent(); + m_output << "- " << escaped << "\n"; + ctx.firstItem = false; + } else { + writeIndent(); + writeKey(name); + m_output << escaped << "\n"; + ctx.firstItem = false; + } + } + + return value.size(); +} + +void +docWriterYaml::pushContext(const std::string& name, ContextType type) +{ + m_contextStack.push(Context(name, type)); +} + +bool +docWriterYaml::popContext(ContextType expectedType) +{ + if (m_contextStack.empty()) { + return false; + } + + const Context& ctx = m_contextStack.top(); + bool isCorrectType = (ctx.type == expectedType); + + if (!isCorrectType) { + logError("Mismatched begin/end calls: expected " + + std::string(expectedType == ContextType::Object ? "Object" : "Array") + " but found " + + std::string(ctx.type == ContextType::Object ? "Object" : "Array") + " for context '" + ctx.name + "'"); + } + + m_contextStack.pop(); + return isCorrectType; +} + +void +docWriterYaml::writeIndent() +{ + for (int i = 0; i < m_indentLevel; ++i) { + m_output << " "; // 2 spaces per indent level + } +} + +void +docWriterYaml::writeKey(const std::string& key) +{ + m_output << key << ": "; +} + +std::string +docWriterYaml::escapeString(const std::string& str) +{ + // Check if string needs quoting + bool needsQuotes = false; + + if (str.empty()) { + return "\"\""; + } + + // Check for special characters that require quoting + for (char c : str) { + if (c == ':' || c == '#' || c == '\n' || c == '\r' || c == '\t' || c == '"' || c == '\'' || c == '\\' || c == '[' || + c == ']' || c == '{' || c == '}' || c == ',' || c == '&' || c == '*' || c == '!' || c == '|' || c == '>' || + c == '@' || c == '`') { + needsQuotes = true; + break; + } + } + + // Check if it starts with special characters + if (str[0] == '-' || str[0] == '?' || str[0] == ' ') { + needsQuotes = true; + } + + if (!needsQuotes) { + return str; + } + + // Escape the string and wrap in quotes + std::string result = "\""; + for (char c : str) { + switch (c) { + case '"': + result += "\\\""; + break; + case '\\': + result += "\\\\"; + break; + case '\n': + result += "\\n"; + break; + case '\r': + result += "\\r"; + break; + case '\t': + result += "\\t"; + break; + default: + result += c; + break; + } + } + result += "\""; + + return result; +} + +void +docWriterYaml::logError(const std::string& message) +{ + LOG_ERROR << "docWriterYaml: " << message; +} diff --git a/renderlib/serialize/docWriterYaml.h b/renderlib/serialize/docWriterYaml.h new file mode 100644 index 00000000..b105257c --- /dev/null +++ b/renderlib/serialize/docWriterYaml.h @@ -0,0 +1,107 @@ +#pragma once + +#include "docWriter.h" + +#include +#include +#include +#include + +class docWriterYaml : public docWriter +{ +public: + docWriterYaml(); + virtual ~docWriterYaml(); + + // Document lifecycle + virtual void beginDocument(std::string filePath) override; + virtual void endDocument() override; + + // Object support + virtual void beginObject(const std::string& i_name, const std::string& i_objectType, uint32_t version) override; + virtual void endObject() override; + + // List/array support + virtual void beginList(const std::string& i_name) override; + virtual void endList() override; + + // Property writing + virtual void writePrty(const prtyProperty* p) override; + + // Primitive type writing - all require a name parameter + virtual size_t writeBool(const std::string& name, bool value) override; + virtual size_t writeInt8(const std::string& name, int8_t value) override; + virtual size_t writeInt16(const std::string& name, int16_t value) override; + virtual size_t writeInt32(const std::string& name, int32_t value) override; + virtual size_t writeInt64(const std::string& name, int64_t value) override; + virtual size_t writeUint8(const std::string& name, uint8_t value) override; + virtual size_t writeUint16(const std::string& name, uint16_t value) override; + virtual size_t writeUint32(const std::string& name, uint32_t value) override; + virtual size_t writeUint64(const std::string& name, uint64_t value) override; + virtual size_t writeFloat32(const std::string& name, float value) override; + virtual size_t writeFloat32Array(const std::string& name, const std::vector& value) override; + virtual size_t writeFloat32Array(const std::string& name, size_t count, const float* values) override; + virtual size_t writeInt32Array(const std::string& name, const std::vector& value) override; + virtual size_t writeUint32Array(const std::string& name, const std::vector& value) override; + virtual size_t writeString(const std::string& name, const std::string& value) override; + +private: + enum class ContextType + { + Object, + Array + }; + + struct Context + { + std::string name; + ContextType type; + bool firstItem; + + Context(const std::string& n, ContextType t) + : name(n) + , type(t) + , firstItem(true) + { + } + + bool isArray() const { return type == ContextType::Array; } + bool isObject() const { return type == ContextType::Object; } + }; + + std::string m_filePath; + std::ostringstream m_output; + std::stack m_contextStack; + int m_indentLevel; + + void pushContext(const std::string& name, ContextType type); + bool popContext(ContextType expectedType); + void writeIndent(); + void writeKey(const std::string& key); + std::string escapeString(const std::string& str); + void logError(const std::string& message); + + // Template helper to reduce duplication in integer writing + template + size_t writeIntegerValue(const std::string& name, T value) + { + if (m_contextStack.empty()) { + writeKey(name); + m_output << value << "\n"; + } else { + Context& ctx = m_contextStack.top(); + if (ctx.isArray()) { + writeIndent(); + m_output << "- " << value << "\n"; + ctx.firstItem = false; + } else { + writeIndent(); + writeKey(name); + m_output << value << "\n"; + ctx.firstItem = false; + } + } + + return sizeof(T); + } +}; diff --git a/renderlib/uiInfo.hpp b/renderlib/uiInfo.hpp index 5a3ac8c7..8c4030c6 100644 --- a/renderlib/uiInfo.hpp +++ b/renderlib/uiInfo.hpp @@ -1,47 +1,58 @@ #pragma once +#include "core/prty/prtyPropertyUIInfo.hpp" + #include #include -struct GenericUIInfo +class CheckBoxUiInfo : public prtyPropertyUIInfo { - std::string type; - std::string formLabel; - std::string statusTip; - std::string toolTip; - - GenericUIInfo() = default; - GenericUIInfo(std::string type, std::string formLabel, std::string statusTip, std::string toolTip) - : type(type) - , formLabel(formLabel) - , statusTip(statusTip) - , toolTip(toolTip) +public: + static constexpr const char* TYPE = "CheckBox"; + CheckBoxUiInfo(prtyProperty* i_pProperty) + : prtyPropertyUIInfo(i_pProperty) + { + SetControlName(TYPE); + } + CheckBoxUiInfo(prtyProperty* i_pProperty, const std::string& i_Category, const std::string& i_Description) + : prtyPropertyUIInfo(i_pProperty, i_Category, i_Description) { + SetControlName(TYPE); } }; -struct CheckBoxUiInfo : public GenericUIInfo -{ - static constexpr const char* TYPE = "CheckBox"; - CheckBoxUiInfo() { type = CheckBoxUiInfo::TYPE; } - CheckBoxUiInfo(std::string formLabel, std::string statusTip, std::string toolTip) - : GenericUIInfo(CheckBoxUiInfo::TYPE, formLabel, statusTip, toolTip) +class ColorPickerUiInfo : public prtyPropertyUIInfo +{ +public: + static constexpr const char* TYPE = "ColorPicker"; + ColorPickerUiInfo(prtyProperty* i_pProperty) + : prtyPropertyUIInfo(i_pProperty) + { + SetControlName(TYPE); + } + ColorPickerUiInfo(prtyProperty* i_pProperty, const std::string& i_Category, const std::string& i_Description) + : prtyPropertyUIInfo(i_pProperty, i_Category, i_Description) { + SetControlName(TYPE); } }; -struct ComboBoxUiInfo : public GenericUIInfo + +class ComboBoxUiInfo : public prtyPropertyUIInfo { +public: static constexpr const char* TYPE = "ComboBox"; - std::vector items; - ComboBoxUiInfo() { type = ComboBoxUiInfo::TYPE; } - ComboBoxUiInfo(std::string formLabel, std::string statusTip, std::string toolTip, std::vector items) - : GenericUIInfo(ComboBoxUiInfo::TYPE, formLabel, statusTip, toolTip) - , items(items) + ComboBoxUiInfo(prtyProperty* i_pProperty) + : prtyPropertyUIInfo(i_pProperty) { + SetControlName(TYPE); + } + ComboBoxUiInfo(prtyProperty* i_pProperty, const std::string& i_Category, const std::string& i_Description) + : prtyPropertyUIInfo(i_pProperty, i_Category, i_Description) + { + SetControlName(TYPE); } }; - class ColorWithIntensityUiInfo : public prtyPropertyUIInfo { public: @@ -72,6 +83,7 @@ class ColorWithIntensityUiInfo : public prtyPropertyUIInfo class FloatSliderSpinnerUiInfo : public prtyPropertyUIInfo { +public: static constexpr const char* TYPE = "FloatSliderSpinner"; float min = 0.0f; float max = 0.0f; @@ -80,28 +92,39 @@ class FloatSliderSpinnerUiInfo : public prtyPropertyUIInfo int numTickMarks = 0; std::string suffix; - FloatSliderSpinnerUiInfo() { type = FloatSliderSpinnerUiInfo::TYPE; } - FloatSliderSpinnerUiInfo(std::string formLabel, - std::string statusTip, - std::string toolTip, - float min, - float max, - int decimals, - float singleStep, - int numTickMarks = 0, - std::string suffix = "") - : GenericUIInfo(FloatSliderSpinnerUiInfo::TYPE, formLabel, statusTip, toolTip) - , min(min) - , max(max) - , decimals(decimals) - , singleStep(singleStep) - , numTickMarks(numTickMarks) - , suffix(suffix) + FloatSliderSpinnerUiInfo(prtyProperty* i_pProperty) + : prtyPropertyUIInfo(i_pProperty) { + SetControlName(TYPE); + } + FloatSliderSpinnerUiInfo(prtyProperty* i_pProperty, const std::string& i_Category, const std::string& i_Description) + : prtyPropertyUIInfo(i_pProperty, i_Category, i_Description) + { + SetControlName(TYPE); } }; -struct IntSliderSpinnerUiInfo : public GenericUIInfo +// std::string statusTip, +// std::string toolTip, +// float min, +// float max, +// int decimals, +// float singleStep, +// int numTickMarks = 0, +// std::string suffix = "") +// : GenericUIInfo(FloatSliderSpinnerUiInfo::TYPE, formLabel, statusTip, toolTip) +// , min(min) +// , max(max) +// , decimals(decimals) +// , singleStep(singleStep) +// , numTickMarks(numTickMarks) +// , suffix(suffix) +// { +// } +// }; + +class IntSliderSpinnerUiInfo : public prtyPropertyUIInfo { +public: static constexpr const char* TYPE = "IntSliderSpinner"; int min; int max; @@ -109,16 +132,15 @@ struct IntSliderSpinnerUiInfo : public GenericUIInfo int numTickMarks; std::string suffix; - IntSliderSpinnerUiInfo() { type = IntSliderSpinnerUiInfo::TYPE; } -}; - -struct ColorPickerUiInfo : public GenericUIInfo -{ - static constexpr const char* TYPE = "ColorPicker"; + IntSliderSpinnerUiInfo(prtyProperty* i_pProperty) + : prtyPropertyUIInfo(i_pProperty) + { + SetControlName(TYPE); + } - ColorPickerUiInfo() { type = ColorPickerUiInfo::TYPE; } - ColorPickerUiInfo(std::string formLabel, std::string statusTip, std::string toolTip) - : GenericUIInfo(ColorPickerUiInfo::TYPE, formLabel, statusTip, toolTip) + IntSliderSpinnerUiInfo(prtyProperty* i_pProperty, const std::string& i_Category, const std::string& i_Description) + : prtyPropertyUIInfo(i_pProperty, i_Category, i_Description) { + SetControlName(TYPE); } }; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 87664863..8c92e41e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -14,8 +14,13 @@ target_include_directories(agave_test PUBLIC "${glm_SOURCE_DIR}" ) target_sources(agave_test PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/test_AppearanceObject.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/test_CameraObject.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/test_commands.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/test_docWriter.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/test_docReader.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/test_histogram.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/test_LightObjects.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/test_main.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/test_mathUtil.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/test_prty.cpp" diff --git a/test/test_AppearanceObject.cpp b/test/test_AppearanceObject.cpp new file mode 100644 index 00000000..68992607 --- /dev/null +++ b/test/test_AppearanceObject.cpp @@ -0,0 +1,358 @@ +#include +#include + +#include "renderlib/AppearanceObject.hpp" +#include "renderlib/serialize/docReader.h" +#include "renderlib/serialize/docReaderJson.h" +#include "renderlib/serialize/docReaderYaml.h" +#include "renderlib/serialize/docWriter.h" +#include "renderlib/serialize/docWriterJson.h" +#include "renderlib/serialize/docWriterYaml.h" +#include "renderlib/serialize/SerializationConstants.h" +#include "renderlib/Logging.h" + +#include +#include +#include + +// Helper function to create an AppearanceObject with known test values +AppearanceObject* +createTestAppearanceObject() +{ + AppearanceObject* appearance = new AppearanceObject(); + + // Set specific test values for all properties + appearance->appearanceDataObject().RendererType.SetValue(1); + appearance->appearanceDataObject().ShadingType.SetValue(1); + appearance->appearanceDataObject().DensityScale.SetValue(2.5f); + appearance->appearanceDataObject().GradientFactor.SetValue(0.75f); + appearance->appearanceDataObject().StepSizePrimaryRay.SetValue(1.5f); + appearance->appearanceDataObject().StepSizeSecondaryRay.SetValue(2.0f); + appearance->appearanceDataObject().Interpolate.SetValue(true); + appearance->appearanceDataObject().BackgroundColor.SetValue(glm::vec4(0.2f, 0.3f, 0.4f, 1.0f)); + appearance->appearanceDataObject().ShowBoundingBox.SetValue(true); + appearance->appearanceDataObject().BoundingBoxColor.SetValue(glm::vec4(1.0f, 0.0f, 0.0f, 1.0f)); + appearance->appearanceDataObject().ShowScaleBar.SetValue(true); + + return appearance; +} + +// Helper function to verify AppearanceObject values match expected test values +void +verifyTestAppearanceObject(AppearanceObject* appearance) +{ + REQUIRE(appearance != nullptr); + + REQUIRE(appearance->appearanceDataObject().RendererType.GetValue() == 1); + REQUIRE(appearance->appearanceDataObject().ShadingType.GetValue() == 1); + REQUIRE(appearance->appearanceDataObject().DensityScale.GetValue() == Catch::Approx(2.5f)); + REQUIRE(appearance->appearanceDataObject().GradientFactor.GetValue() == Catch::Approx(0.75f)); + REQUIRE(appearance->appearanceDataObject().StepSizePrimaryRay.GetValue() == Catch::Approx(1.5f)); + REQUIRE(appearance->appearanceDataObject().StepSizeSecondaryRay.GetValue() == Catch::Approx(2.0f)); + REQUIRE(appearance->appearanceDataObject().Interpolate.GetValue() == true); + + auto bgColor = appearance->appearanceDataObject().BackgroundColor.GetValue(); + REQUIRE(bgColor.x == Catch::Approx(0.2f)); + REQUIRE(bgColor.y == Catch::Approx(0.3f)); + REQUIRE(bgColor.z == Catch::Approx(0.4f)); + REQUIRE(bgColor.w == Catch::Approx(1.0f)); + + REQUIRE(appearance->appearanceDataObject().ShowBoundingBox.GetValue() == true); + + auto bbColor = appearance->appearanceDataObject().BoundingBoxColor.GetValue(); + REQUIRE(bbColor.x == Catch::Approx(1.0f)); + REQUIRE(bbColor.y == Catch::Approx(0.0f)); + REQUIRE(bbColor.z == Catch::Approx(0.0f)); + REQUIRE(bbColor.w == Catch::Approx(1.0f)); + + REQUIRE(appearance->appearanceDataObject().ShowScaleBar.GetValue() == true); +} + +TEST_CASE("AppearanceObject JSON roundtrip serialization", "[AppearanceObject][serialize]") +{ + std::string jsonPath = "test_appearance.json"; + + // Create and save appearance + { + AppearanceObject* appearance = createTestAppearanceObject(); + + docWriterJson writer; + writer.beginDocument(jsonPath); + appearance->toDocument(&writer); + writer.endDocument(); + + delete appearance; + } + + // Verify file exists + REQUIRE(std::filesystem::exists(jsonPath)); + + // Load and verify appearance + { + AppearanceObject* loadedAppearance = new AppearanceObject(); + + docReaderJson reader; + reader.beginDocument(jsonPath); + reader.beginObject("appearance0"); + loadedAppearance->fromDocument(&reader); + reader.endObject(); + reader.endDocument(); + + verifyTestAppearanceObject(loadedAppearance); + + delete loadedAppearance; + } + + // Cleanup + std::filesystem::remove(jsonPath); +} + +TEST_CASE("AppearanceObject YAML roundtrip serialization", "[AppearanceObject][serialize]") +{ + std::string yamlPath = "test_appearance.yaml"; + + // Create and save appearance + { + AppearanceObject* appearance = createTestAppearanceObject(); + + docWriterYaml writer; + writer.beginDocument(yamlPath); + appearance->toDocument(&writer); + writer.endDocument(); + + delete appearance; + } + + // Verify file exists + REQUIRE(std::filesystem::exists(yamlPath)); + + // Load and verify appearance + { + AppearanceObject* loadedAppearance = new AppearanceObject(); + + docReaderYaml reader; + reader.beginDocument(yamlPath); + reader.beginObject("appearance0"); + loadedAppearance->fromDocument(&reader); + reader.endObject(); + reader.endDocument(); + + verifyTestAppearanceObject(loadedAppearance); + + delete loadedAppearance; + } + + // Cleanup + std::filesystem::remove(yamlPath); +} + +TEST_CASE("AppearanceObject version 1 JSON format compatibility", "[AppearanceObject][serialize][version]") +{ + std::string jsonPath = "test_appearance_v1.json"; + + // Manually create a version 1 JSON file to simulate old format + { + std::ofstream file(jsonPath); + file << R"({ + "appearance0": { + "_type": "AppearanceObject", + "_version": 1, + "_name": "appearance0", + "RendererType": 1, + "ShadingType": 1, + "DensityScale": 2.5, + "GradientFactor": 0.75, + "StepSizePrimaryRay": 1.5, + "StepSizeSecondaryRay": 2.0, + "Interpolate": true, + "BackgroundColor": [0.2, 0.3, 0.4, 1.0], + "ShowBoundingBox": true, + "BoundingBoxColor": [1.0, 0.0, 0.0, 1.0], + "ShowScaleBar": true + } +})"; + file.close(); + } + + // Load and verify the version 1 format is correctly read + { + AppearanceObject* loadedAppearance = new AppearanceObject(); + + docReaderJson reader; + reader.beginDocument(jsonPath); + + reader.beginObject("appearance0"); + + // Check version before reading properties + std::string objectType = reader.peekObjectType(); + REQUIRE(objectType == "AppearanceObject"); + + uint32_t version = reader.peekVersion(); + REQUIRE(version == 1); + + loadedAppearance->fromDocument(&reader); + + reader.endObject(); + + reader.endDocument(); + + verifyTestAppearanceObject(loadedAppearance); + + delete loadedAppearance; + } + + // Cleanup + std::filesystem::remove(jsonPath); +} + +TEST_CASE("AppearanceObject version 1 YAML format compatibility", "[AppearanceObject][serialize][version]") +{ + std::string yamlPath = "test_appearance_v1.yaml"; + + // Manually create a version 1 YAML file to simulate old format + { + std::ofstream file(yamlPath); + file << R"(appearance0: + _type: AppearanceObject + _version: 1 + _name: appearance0 + RendererType: 1 + ShadingType: 1 + DensityScale: 2.5 + GradientFactor: 0.75 + StepSizePrimaryRay: 1.5 + StepSizeSecondaryRay: 2.0 + Interpolate: true + BackgroundColor: [0.2, 0.3, 0.4, 1.0] + ShowBoundingBox: true + BoundingBoxColor: [1.0, 0.0, 0.0, 1.0] + ShowScaleBar: true +)"; + file.close(); + } + + // Load and verify the version 1 format is correctly read + { + AppearanceObject* loadedAppearance = new AppearanceObject(); + + docReaderYaml reader; + reader.beginDocument(yamlPath); + + reader.beginObject("appearance0"); + + // Check version before reading properties + std::string objectType = reader.peekObjectType(); + REQUIRE(objectType == "AppearanceObject"); + + uint32_t version = reader.peekVersion(); + REQUIRE(version == 1); + + loadedAppearance->fromDocument(&reader); + + reader.endObject(); + + reader.endDocument(); + + verifyTestAppearanceObject(loadedAppearance); + + delete loadedAppearance; + } + + // Cleanup + std::filesystem::remove(yamlPath); +} + +TEST_CASE("AppearanceObject default values serialization", "[AppearanceObject][serialize]") +{ + std::string jsonPath = "test_appearance_defaults.json"; + + // Create appearance with default values (no modifications) + { + AppearanceObject* appearance = new AppearanceObject(); + + docWriterJson writer; + writer.beginDocument(jsonPath); + appearance->toDocument(&writer); + writer.endDocument(); + + delete appearance; + } + + // Load and verify defaults are preserved + { + AppearanceObject* loadedAppearance = new AppearanceObject(); + AppearanceObject* defaultAppearance = new AppearanceObject(); + + docReaderJson reader; + reader.beginDocument(jsonPath); + reader.beginObject("appearance0"); + loadedAppearance->fromDocument(&reader); + reader.endObject(); + reader.endDocument(); + + // Verify all values match defaults + REQUIRE(loadedAppearance->appearanceDataObject().RendererType.GetValue() == + defaultAppearance->appearanceDataObject().RendererType.GetValue()); + REQUIRE(loadedAppearance->appearanceDataObject().ShadingType.GetValue() == + defaultAppearance->appearanceDataObject().ShadingType.GetValue()); + REQUIRE(loadedAppearance->appearanceDataObject().DensityScale.GetValue() == + Catch::Approx(defaultAppearance->appearanceDataObject().DensityScale.GetValue())); + REQUIRE(loadedAppearance->appearanceDataObject().GradientFactor.GetValue() == + Catch::Approx(defaultAppearance->appearanceDataObject().GradientFactor.GetValue())); + + delete loadedAppearance; + delete defaultAppearance; + } + + // Cleanup + std::filesystem::remove(jsonPath); +} + +TEST_CASE("AppearanceObject partial data loading", "[AppearanceObject][serialize]") +{ + std::string jsonPath = "test_appearance_partial.json"; + + // Create a JSON file with only some properties + { + std::ofstream file(jsonPath); + file << R"({ + "appearance0": { + "_type": "AppearanceObject", + "_version": 1, + "_name": "appearance0", + "DensityScale": 3.0, + "Interpolate": true + } +})"; + file.close(); + } + + // Load and verify that specified properties are loaded, others remain default + { + AppearanceObject* loadedAppearance = new AppearanceObject(); + AppearanceObject* defaultAppearance = new AppearanceObject(); + + docReaderJson reader; + reader.beginDocument(jsonPath); + reader.beginObject("appearance0"); + loadedAppearance->fromDocument(&reader); + reader.endObject(); + reader.endDocument(); + + // Specified properties should be loaded + REQUIRE(loadedAppearance->appearanceDataObject().DensityScale.GetValue() == Catch::Approx(3.0f)); + REQUIRE(loadedAppearance->appearanceDataObject().Interpolate.GetValue() == true); + + // Other properties should remain at default + REQUIRE(loadedAppearance->appearanceDataObject().RendererType.GetValue() == + defaultAppearance->appearanceDataObject().RendererType.GetValue()); + REQUIRE(loadedAppearance->appearanceDataObject().GradientFactor.GetValue() == + Catch::Approx(defaultAppearance->appearanceDataObject().GradientFactor.GetValue())); + + delete loadedAppearance; + delete defaultAppearance; + } + + // Cleanup + std::filesystem::remove(jsonPath); +} diff --git a/test/test_CameraObject.cpp b/test/test_CameraObject.cpp new file mode 100644 index 00000000..ef89b7e7 --- /dev/null +++ b/test/test_CameraObject.cpp @@ -0,0 +1,485 @@ +#include +#include + +#include "renderlib/CameraObject.hpp" +#include "renderlib/serialize/docReader.h" +#include "renderlib/serialize/docReaderJson.h" +#include "renderlib/serialize/docReaderYaml.h" +#include "renderlib/serialize/docWriter.h" +#include "renderlib/serialize/docWriterJson.h" +#include "renderlib/serialize/docWriterYaml.h" +#include "renderlib/serialize/SerializationConstants.h" +#include "renderlib/Logging.h" + +#include +#include +#include + +// Helper function to create a CameraObject with known test values +CameraObject* +createTestCameraObject() +{ + CameraObject* camera = new CameraObject(); + + // Set specific test values for all properties + camera->getCameraDataObject().Exposure.SetValue(0.85f); + camera->getCameraDataObject().ExposureIterations.SetValue(2); // "4" in enum + camera->getCameraDataObject().NoiseReduction.SetValue(true); + camera->getCameraDataObject().ApertureSize.SetValue(0.05f); + camera->getCameraDataObject().FieldOfView.SetValue(45.0f); + camera->getCameraDataObject().FocalDistance.SetValue(5.5f); + camera->getCameraDataObject().Position.SetValue(glm::vec3(1.0f, 2.0f, 3.0f)); + camera->getCameraDataObject().Target.SetValue(glm::vec3(0.0f, 0.0f, 0.0f)); + camera->getCameraDataObject().NearPlane.SetValue(0.5f); + camera->getCameraDataObject().FarPlane.SetValue(500.0f); + camera->getCameraDataObject().Roll.SetValue(15.0f); + camera->getCameraDataObject().OrthoScale.SetValue(1.5f); + camera->getCameraDataObject().ProjectionMode.SetValue(0); // Perspective + + return camera; +} + +// Helper function to verify CameraObject values match expected test values +void +verifyTestCameraObject(CameraObject* camera) +{ + REQUIRE(camera != nullptr); + + REQUIRE(camera->getCameraDataObject().Exposure.GetValue() == Catch::Approx(0.85f)); + REQUIRE(camera->getCameraDataObject().ExposureIterations.GetValue() == 2); + REQUIRE(camera->getCameraDataObject().NoiseReduction.GetValue() == true); + REQUIRE(camera->getCameraDataObject().ApertureSize.GetValue() == Catch::Approx(0.05f)); + REQUIRE(camera->getCameraDataObject().FieldOfView.GetValue() == Catch::Approx(45.0f)); + REQUIRE(camera->getCameraDataObject().FocalDistance.GetValue() == Catch::Approx(5.5f)); + + auto pos = camera->getCameraDataObject().Position.GetValue(); + REQUIRE(pos.x == Catch::Approx(1.0f)); + REQUIRE(pos.y == Catch::Approx(2.0f)); + REQUIRE(pos.z == Catch::Approx(3.0f)); + + auto target = camera->getCameraDataObject().Target.GetValue(); + REQUIRE(target.x == Catch::Approx(0.0f)); + REQUIRE(target.y == Catch::Approx(0.0f)); + REQUIRE(target.z == Catch::Approx(0.0f)); + + REQUIRE(camera->getCameraDataObject().NearPlane.GetValue() == Catch::Approx(0.5f)); + REQUIRE(camera->getCameraDataObject().FarPlane.GetValue() == Catch::Approx(500.0f)); + REQUIRE(camera->getCameraDataObject().Roll.GetValue() == Catch::Approx(15.0f)); + REQUIRE(camera->getCameraDataObject().OrthoScale.GetValue() == Catch::Approx(1.5f)); + REQUIRE(camera->getCameraDataObject().ProjectionMode.GetValue() == 0); +} + +TEST_CASE("CameraObject JSON roundtrip serialization", "[CameraObject][serialize]") +{ + std::string jsonPath = "test_camera.json"; + + // Create and save camera + { + CameraObject* camera = createTestCameraObject(); + + docWriterJson writer; + writer.beginDocument(jsonPath); + camera->toDocument(&writer); + writer.endDocument(); + + delete camera; + } + + // Verify file exists + REQUIRE(std::filesystem::exists(jsonPath)); + + // Load and verify camera + { + CameraObject* loadedCamera = new CameraObject(); + + docReaderJson reader; + reader.beginDocument(jsonPath); + reader.beginObject("camera0"); + loadedCamera->fromDocument(&reader); + reader.endObject(); + reader.endDocument(); + + verifyTestCameraObject(loadedCamera); + + delete loadedCamera; + } + + // Cleanup + std::filesystem::remove(jsonPath); +} + +TEST_CASE("CameraObject YAML roundtrip serialization", "[CameraObject][serialize]") +{ + std::string yamlPath = "test_camera.yaml"; + + // Create and save camera + { + CameraObject* camera = createTestCameraObject(); + + docWriterYaml writer; + writer.beginDocument(yamlPath); + camera->toDocument(&writer); + writer.endDocument(); + + delete camera; + } + + // Verify file exists + REQUIRE(std::filesystem::exists(yamlPath)); + + // Load and verify camera + { + CameraObject* loadedCamera = new CameraObject(); + + docReaderYaml reader; + reader.beginDocument(yamlPath); + reader.beginObject("camera0"); + loadedCamera->fromDocument(&reader); + reader.endObject(); + reader.endDocument(); + + verifyTestCameraObject(loadedCamera); + + delete loadedCamera; + } + + // Cleanup + std::filesystem::remove(yamlPath); +} + +TEST_CASE("CameraObject version 1 JSON format compatibility", "[CameraObject][serialize][version]") +{ + std::string jsonPath = "test_camera_v1.json"; + + // Manually create a version 1 JSON file to simulate old format + { + std::ofstream file(jsonPath); + file << R"({ + "camera0": { + "_type": "CameraObject", + "_version": 1, + "_name": "camera0", + "Exposure": 0.85, + "ExposureIterations": 2, + "NoiseReduction": true, + "ApertureSize": 0.05, + "FieldOfView": 45.0, + "FocalDistance": 5.5, + "Position": [1.0, 2.0, 3.0], + "Target": [0.0, 0.0, 0.0], + "NearPlane": 0.5, + "FarPlane": 500.0, + "Roll": 15.0, + "OrthoScale": 1.5, + "ProjectionMode": 0 + } +})"; + file.close(); + } + + // Load and verify the version 1 format is correctly read + { + CameraObject* loadedCamera = new CameraObject(); + + docReaderJson reader; + reader.beginDocument(jsonPath); + + reader.beginObject("camera0"); + + // Check version before reading properties + std::string objectType = reader.peekObjectType(); + REQUIRE(objectType == "CameraObject"); + + int version = reader.peekVersion(); + REQUIRE(version == 1); + + loadedCamera->fromDocument(&reader); + + reader.endObject(); + + reader.endDocument(); + + verifyTestCameraObject(loadedCamera); + + delete loadedCamera; + } + + // Cleanup + std::filesystem::remove(jsonPath); +} + +TEST_CASE("CameraObject version 1 YAML format compatibility", "[CameraObject][serialize][version]") +{ + std::string yamlPath = "test_camera_v1.yaml"; + + // Manually create a version 1 YAML file to simulate old format + { + std::ofstream file(yamlPath); + file << R"(camera0: + _type: CameraObject + _version: 1 + _name: camera0 + Exposure: 0.85 + ExposureIterations: 2 + NoiseReduction: true + ApertureSize: 0.05 + FieldOfView: 45.0 + FocalDistance: 5.5 + Position: [1.0, 2.0, 3.0] + Target: [0.0, 0.0, 0.0] + NearPlane: 0.5 + FarPlane: 500.0 + Roll: 15.0 + OrthoScale: 1.5 + ProjectionMode: 0 +)"; + file.close(); + } + + // Load and verify the version 1 format is correctly read + { + CameraObject* loadedCamera = new CameraObject(); + + docReaderYaml reader; + reader.beginDocument(yamlPath); + + reader.beginObject("camera0"); + + // Check version before reading properties + std::string objectType = reader.peekObjectType(); + REQUIRE(objectType == "CameraObject"); + + int version = reader.peekVersion(); + REQUIRE(version == 1); + + loadedCamera->fromDocument(&reader); + + reader.endObject(); + + reader.endDocument(); + + verifyTestCameraObject(loadedCamera); + + delete loadedCamera; + } + + // Cleanup + std::filesystem::remove(yamlPath); +} + +TEST_CASE("CameraObject default values serialization", "[CameraObject][serialize]") +{ + std::string jsonPath = "test_camera_defaults.json"; + + // Create camera with default values (no modifications) + { + CameraObject* camera = new CameraObject(); + + docWriterJson writer; + writer.beginDocument(jsonPath); + camera->toDocument(&writer); + writer.endDocument(); + + delete camera; + } + + // Load and verify defaults are preserved + { + CameraObject* loadedCamera = new CameraObject(); + CameraObject* defaultCamera = new CameraObject(); + + docReaderJson reader; + reader.beginDocument(jsonPath); + reader.beginObject("camera0"); + loadedCamera->fromDocument(&reader); + reader.endObject(); + reader.endDocument(); + + // Verify all values match defaults + REQUIRE(loadedCamera->getCameraDataObject().Exposure.GetValue() == + Catch::Approx(defaultCamera->getCameraDataObject().Exposure.GetValue())); + REQUIRE(loadedCamera->getCameraDataObject().ExposureIterations.GetValue() == + defaultCamera->getCameraDataObject().ExposureIterations.GetValue()); + REQUIRE(loadedCamera->getCameraDataObject().NoiseReduction.GetValue() == + defaultCamera->getCameraDataObject().NoiseReduction.GetValue()); + REQUIRE(loadedCamera->getCameraDataObject().ApertureSize.GetValue() == + Catch::Approx(defaultCamera->getCameraDataObject().ApertureSize.GetValue())); + REQUIRE(loadedCamera->getCameraDataObject().FieldOfView.GetValue() == + Catch::Approx(defaultCamera->getCameraDataObject().FieldOfView.GetValue())); + + delete loadedCamera; + delete defaultCamera; + } + + // Cleanup + std::filesystem::remove(jsonPath); +} + +TEST_CASE("CameraObject partial data loading", "[CameraObject][serialize]") +{ + std::string jsonPath = "test_camera_partial.json"; + + // Create a JSON file with only some properties + { + std::ofstream file(jsonPath); + file << R"({ + "camera0": { + "_type": "CameraObject", + "_version": 1, + "_name": "camera0", + "Exposure": 0.5, + "FieldOfView": 60.0 + } +})"; + file.close(); + } + + // Load and verify that specified properties are loaded, others remain default + { + CameraObject* loadedCamera = new CameraObject(); + CameraObject* defaultCamera = new CameraObject(); + + docReaderJson reader; + reader.beginDocument(jsonPath); + reader.beginObject("camera0"); + loadedCamera->fromDocument(&reader); + reader.endObject(); + reader.endDocument(); + + // Specified properties should be loaded + REQUIRE(loadedCamera->getCameraDataObject().Exposure.GetValue() == Catch::Approx(0.5f)); + REQUIRE(loadedCamera->getCameraDataObject().FieldOfView.GetValue() == Catch::Approx(60.0f)); + + // Other properties should remain at default + REQUIRE(loadedCamera->getCameraDataObject().ExposureIterations.GetValue() == + defaultCamera->getCameraDataObject().ExposureIterations.GetValue()); + REQUIRE(loadedCamera->getCameraDataObject().ApertureSize.GetValue() == + Catch::Approx(defaultCamera->getCameraDataObject().ApertureSize.GetValue())); + + delete loadedCamera; + delete defaultCamera; + } + + // Cleanup + std::filesystem::remove(jsonPath); +} + +TEST_CASE("CameraObject invalid version handling", "[CameraObject][serialize][version]") +{ + std::string jsonPath = "test_camera_invalid_version.json"; + + // Create a JSON file with a future version number + { + std::ofstream file(jsonPath); + file << R"({ + "camera0": { + "_type": "CameraObject", + "_version": 999, + "_name": "camera0", + "Exposure": 0.85, + "FieldOfView": 45.0 + } +})"; + file.close(); + } + + // Load - should still work, just log a warning + { + CameraObject* loadedCamera = new CameraObject(); + + docReaderJson reader; + reader.beginDocument(jsonPath); + + // Enter the CameraObject before checking version + reader.beginObject("camera0"); + + uint32_t version = reader.peekVersion(); + REQUIRE(version == 999); + + loadedCamera->fromDocument(&reader); + + reader.endObject(); + + reader.endDocument(); + + REQUIRE(loadedCamera->getCameraDataObject().Exposure.GetValue() == Catch::Approx(0.85f)); + REQUIRE(loadedCamera->getCameraDataObject().FieldOfView.GetValue() == Catch::Approx(45.0f)); + + delete loadedCamera; + } + + // Cleanup + std::filesystem::remove(jsonPath); +} + +TEST_CASE("CameraObject enum property serialization", "[CameraObject][serialize]") +{ + std::string jsonPath = "test_camera_enum.json"; + + // Test different enum values + for (int i = 0; i < 4; ++i) { + // Create and save camera with specific enum value + { + CameraObject* camera = new CameraObject(); + camera->getCameraDataObject().ExposureIterations.SetValue(i); + + docWriterJson writer; + writer.beginDocument(jsonPath); + camera->toDocument(&writer); + writer.endDocument(); + + delete camera; + } + + // Load and verify enum value + { + CameraObject* loadedCamera = new CameraObject(); + + docReaderJson reader; + reader.beginDocument(jsonPath); + reader.beginObject("camera0"); + loadedCamera->fromDocument(&reader); + reader.endObject(); + reader.endDocument(); + + REQUIRE(loadedCamera->getCameraDataObject().ExposureIterations.GetValue() == i); + + delete loadedCamera; + } + } + + // Test ProjectionMode enum + for (int i = 0; i < 2; ++i) { + // Create and save camera with specific projection mode + { + CameraObject* camera = new CameraObject(); + camera->getCameraDataObject().ProjectionMode.SetValue(i); + + docWriterJson writer; + writer.beginDocument(jsonPath); + camera->toDocument(&writer); + writer.endDocument(); + + delete camera; + } + + // Load and verify projection mode + { + CameraObject* loadedCamera = new CameraObject(); + + docReaderJson reader; + reader.beginDocument(jsonPath); + reader.beginObject("camera0"); + loadedCamera->fromDocument(&reader); + reader.endObject(); + reader.endDocument(); + + REQUIRE(loadedCamera->getCameraDataObject().ProjectionMode.GetValue() == i); + + delete loadedCamera; + } + } + + // Cleanup + std::filesystem::remove(jsonPath); +} diff --git a/test/test_LightObjects.cpp b/test/test_LightObjects.cpp new file mode 100644 index 00000000..bec1c5b1 --- /dev/null +++ b/test/test_LightObjects.cpp @@ -0,0 +1,421 @@ +#include +#include + +#include "renderlib/AreaLightObject.hpp" +#include "renderlib/SkyLightObject.hpp" +#include "renderlib/serialize/docReader.h" +#include "renderlib/serialize/docReaderJson.h" +#include "renderlib/serialize/docReaderYaml.h" +#include "renderlib/serialize/docWriter.h" +#include "renderlib/serialize/docWriterJson.h" +#include "renderlib/serialize/docWriterYaml.h" +#include "renderlib/Logging.h" + +#include +#include + +// ============================================================================ +// AreaLightObject Tests +// ============================================================================ + +// Helper function to create an AreaLightObject with known test values +AreaLightObject* +createTestAreaLightObject() +{ + AreaLightObject* light = new AreaLightObject(); + + // Set specific test values for all properties + light->getDataObject().Theta.SetValue(45.0f); + light->getDataObject().Phi.SetValue(90.0f); + light->getDataObject().Size.SetValue(2.5f); + light->getDataObject().Distance.SetValue(15.0f); + light->getDataObject().Intensity.SetValue(250.0f); + light->getDataObject().Color.SetValue(glm::vec4(0.8f, 0.9f, 1.0f, 1.0f)); + + return light; +} + +// Helper function to verify AreaLightObject values match expected test values +void +verifyTestAreaLightObject(AreaLightObject* light) +{ + REQUIRE(light != nullptr); + + const ArealightDataObject& data = light->getDataObject(); + REQUIRE(data.Theta.GetValue() == Catch::Approx(45.0f)); + REQUIRE(data.Phi.GetValue() == Catch::Approx(90.0f)); + REQUIRE(data.Size.GetValue() == Catch::Approx(2.5f)); + REQUIRE(data.Distance.GetValue() == Catch::Approx(15.0f)); + REQUIRE(data.Intensity.GetValue() == Catch::Approx(250.0f)); + + glm::vec4 color = data.Color.GetValue(); + REQUIRE(color.x == Catch::Approx(0.8f)); + REQUIRE(color.y == Catch::Approx(0.9f)); + REQUIRE(color.z == Catch::Approx(1.0f)); + REQUIRE(color.w == Catch::Approx(1.0f)); +} + +TEST_CASE("AreaLightObject creation and initialization", "[AreaLightObject]") +{ + AreaLightObject* light = new AreaLightObject(); + + REQUIRE(light != nullptr); + REQUIRE(light->getSceneLight() != nullptr); + + // Verify default values + const ArealightDataObject& data = light->getDataObject(); + REQUIRE(data.Theta.GetValue() == Catch::Approx(0.0f)); + REQUIRE(data.Phi.GetValue() == Catch::Approx(90.0f)); + REQUIRE(data.Size.GetValue() == Catch::Approx(1.0f)); + REQUIRE(data.Distance.GetValue() == Catch::Approx(10.0f)); + REQUIRE(data.Intensity.GetValue() == Catch::Approx(100.0f)); + + delete light; +} + +TEST_CASE("AreaLightObject property setting", "[AreaLightObject]") +{ + AreaLightObject* light = new AreaLightObject(); + + // Test setting properties + light->getDataObject().Theta.SetValue(30.0f); + light->getDataObject().Phi.SetValue(60.0f); + light->getDataObject().Size.SetValue(5.0f); + light->getDataObject().Distance.SetValue(20.0f); + light->getDataObject().Intensity.SetValue(150.0f); + light->getDataObject().Color.SetValue(glm::vec4(1.0f, 0.5f, 0.25f, 1.0f)); + + REQUIRE(light->getDataObject().Theta.GetValue() == Catch::Approx(30.0f)); + REQUIRE(light->getDataObject().Phi.GetValue() == Catch::Approx(60.0f)); + REQUIRE(light->getDataObject().Size.GetValue() == Catch::Approx(5.0f)); + REQUIRE(light->getDataObject().Distance.GetValue() == Catch::Approx(20.0f)); + REQUIRE(light->getDataObject().Intensity.GetValue() == Catch::Approx(150.0f)); + + glm::vec4 color = light->getDataObject().Color.GetValue(); + REQUIRE(color.x == Catch::Approx(1.0f)); + REQUIRE(color.y == Catch::Approx(0.5f)); + REQUIRE(color.z == Catch::Approx(0.25f)); + + delete light; +} + +TEST_CASE("AreaLightObject JSON roundtrip serialization", "[AreaLightObject][serialize]") +{ + std::string jsonPath = "test_arealight.json"; + + // Create and save light + { + AreaLightObject* light = createTestAreaLightObject(); + + docWriterJson writer; + writer.beginDocument(jsonPath); + light->toDocument(&writer); + writer.endDocument(); + + delete light; + } + + // Verify file exists + REQUIRE(std::filesystem::exists(jsonPath)); + + // Load and verify light + { + AreaLightObject* loadedLight = new AreaLightObject(); + + docReaderJson reader; + reader.beginDocument(jsonPath); + reader.beginObject("areaLight0"); + loadedLight->fromDocument(&reader); + reader.endObject(); + reader.endDocument(); + + verifyTestAreaLightObject(loadedLight); + + delete loadedLight; + } + + // Cleanup + std::filesystem::remove(jsonPath); +} + +TEST_CASE("AreaLightObject YAML roundtrip serialization", "[AreaLightObject][serialize]") +{ + std::string yamlPath = "test_arealight.yaml"; + + // Create and save light + { + AreaLightObject* light = createTestAreaLightObject(); + + docWriterYaml writer; + writer.beginDocument(yamlPath); + light->toDocument(&writer); + writer.endDocument(); + + delete light; + } + + // Verify file exists + REQUIRE(std::filesystem::exists(yamlPath)); + + // Load and verify light + { + AreaLightObject* loadedLight = new AreaLightObject(); + + docReaderYaml reader; + reader.beginDocument(yamlPath); + reader.beginObject("areaLight0"); + loadedLight->fromDocument(&reader); + reader.endObject(); + reader.endDocument(); + + verifyTestAreaLightObject(loadedLight); + + delete loadedLight; + } + + // Cleanup + std::filesystem::remove(yamlPath); +} + +TEST_CASE("AreaLightObject dirty callback", "[AreaLightObject]") +{ + AreaLightObject* light = new AreaLightObject(); + bool callbackCalled = false; + + light->setDirtyCallback([&callbackCalled]() { callbackCalled = true; }); + + // The callback should be invoked when properties change + light->ThetaChanged(nullptr, false); + REQUIRE(callbackCalled); + + callbackCalled = false; + light->PhiChanged(nullptr, false); + REQUIRE(callbackCalled); + + callbackCalled = false; + light->IntensityChanged(nullptr, false); + REQUIRE(callbackCalled); + + delete light; +} + +// ============================================================================ +// SkyLightObject Tests +// ============================================================================ + +// Helper function to create a SkyLightObject with known test values +SkyLightObject* +createTestSkyLightObject() +{ + SkyLightObject* light = new SkyLightObject(); + + // Set specific test values for all properties + light->getDataObject().TopIntensity.SetValue(2.5f); + light->getDataObject().TopColor.SetValue(glm::vec4(0.9f, 0.95f, 1.0f, 1.0f)); + light->getDataObject().MiddleIntensity.SetValue(1.5f); + light->getDataObject().MiddleColor.SetValue(glm::vec4(0.6f, 0.6f, 0.6f, 1.0f)); + light->getDataObject().BottomIntensity.SetValue(0.8f); + light->getDataObject().BottomColor.SetValue(glm::vec4(0.3f, 0.25f, 0.2f, 1.0f)); + + return light; +} + +// Helper function to verify SkyLightObject values match expected test values +void +verifyTestSkyLightObject(SkyLightObject* light) +{ + REQUIRE(light != nullptr); + + const SkylightDataObject& data = light->getDataObject(); + REQUIRE(data.TopIntensity.GetValue() == Catch::Approx(2.5f)); + REQUIRE(data.MiddleIntensity.GetValue() == Catch::Approx(1.5f)); + REQUIRE(data.BottomIntensity.GetValue() == Catch::Approx(0.8f)); + + glm::vec4 topColor = data.TopColor.GetValue(); + REQUIRE(topColor.x == Catch::Approx(0.9f)); + REQUIRE(topColor.y == Catch::Approx(0.95f)); + REQUIRE(topColor.z == Catch::Approx(1.0f)); + + glm::vec4 middleColor = data.MiddleColor.GetValue(); + REQUIRE(middleColor.x == Catch::Approx(0.6f)); + REQUIRE(middleColor.y == Catch::Approx(0.6f)); + REQUIRE(middleColor.z == Catch::Approx(0.6f)); + + glm::vec4 bottomColor = data.BottomColor.GetValue(); + REQUIRE(bottomColor.x == Catch::Approx(0.3f)); + REQUIRE(bottomColor.y == Catch::Approx(0.25f)); + REQUIRE(bottomColor.z == Catch::Approx(0.2f)); +} + +TEST_CASE("SkyLightObject creation and initialization", "[SkyLightObject]") +{ + SkyLightObject* light = new SkyLightObject(); + + REQUIRE(light != nullptr); + REQUIRE(light->getSceneLight() != nullptr); + + // Verify default values + const SkylightDataObject& data = light->getDataObject(); + REQUIRE(data.TopIntensity.GetValue() == Catch::Approx(1.0f)); + REQUIRE(data.MiddleIntensity.GetValue() == Catch::Approx(1.0f)); + REQUIRE(data.BottomIntensity.GetValue() == Catch::Approx(1.0f)); + + glm::vec4 topColor = data.TopColor.GetValue(); + REQUIRE(topColor.x == Catch::Approx(1.0f)); + REQUIRE(topColor.y == Catch::Approx(1.0f)); + REQUIRE(topColor.z == Catch::Approx(1.0f)); + + glm::vec4 bottomColor = data.BottomColor.GetValue(); + REQUIRE(bottomColor.x == Catch::Approx(0.2f)); + REQUIRE(bottomColor.y == Catch::Approx(0.2f)); + REQUIRE(bottomColor.z == Catch::Approx(0.2f)); + + delete light; +} + +TEST_CASE("SkyLightObject property setting", "[SkyLightObject]") +{ + SkyLightObject* light = new SkyLightObject(); + + // Test setting properties + light->getDataObject().TopIntensity.SetValue(3.0f); + light->getDataObject().TopColor.SetValue(glm::vec4(1.0f, 1.0f, 0.8f, 1.0f)); + light->getDataObject().MiddleIntensity.SetValue(2.0f); + light->getDataObject().MiddleColor.SetValue(glm::vec4(0.7f, 0.7f, 0.7f, 1.0f)); + light->getDataObject().BottomIntensity.SetValue(1.0f); + light->getDataObject().BottomColor.SetValue(glm::vec4(0.4f, 0.3f, 0.2f, 1.0f)); + + REQUIRE(light->getDataObject().TopIntensity.GetValue() == Catch::Approx(3.0f)); + REQUIRE(light->getDataObject().MiddleIntensity.GetValue() == Catch::Approx(2.0f)); + REQUIRE(light->getDataObject().BottomIntensity.GetValue() == Catch::Approx(1.0f)); + + glm::vec4 topColor = light->getDataObject().TopColor.GetValue(); + REQUIRE(topColor.x == Catch::Approx(1.0f)); + REQUIRE(topColor.y == Catch::Approx(1.0f)); + REQUIRE(topColor.z == Catch::Approx(0.8f)); + + delete light; +} + +TEST_CASE("SkyLightObject JSON roundtrip serialization", "[SkyLightObject][serialize]") +{ + std::string jsonPath = "test_skylight.json"; + + // Create and save light + { + SkyLightObject* light = createTestSkyLightObject(); + + docWriterJson writer; + writer.beginDocument(jsonPath); + light->toDocument(&writer); + writer.endDocument(); + + delete light; + } + + // Verify file exists + REQUIRE(std::filesystem::exists(jsonPath)); + + // Load and verify light + { + SkyLightObject* loadedLight = new SkyLightObject(); + + docReaderJson reader; + reader.beginDocument(jsonPath); + reader.beginObject("skyLight0"); + loadedLight->fromDocument(&reader); + reader.endObject(); + reader.endDocument(); + + verifyTestSkyLightObject(loadedLight); + + delete loadedLight; + } + + // Cleanup + std::filesystem::remove(jsonPath); +} + +TEST_CASE("SkyLightObject YAML roundtrip serialization", "[SkyLightObject][serialize]") +{ + std::string yamlPath = "test_skylight.yaml"; + + // Create and save light + { + SkyLightObject* light = createTestSkyLightObject(); + + docWriterYaml writer; + writer.beginDocument(yamlPath); + light->toDocument(&writer); + writer.endDocument(); + + delete light; + } + + // Verify file exists + REQUIRE(std::filesystem::exists(yamlPath)); + + // Load and verify light + { + SkyLightObject* loadedLight = new SkyLightObject(); + + docReaderYaml reader; + reader.beginDocument(yamlPath); + reader.beginObject("skyLight0"); + loadedLight->fromDocument(&reader); + reader.endObject(); + reader.endDocument(); + + verifyTestSkyLightObject(loadedLight); + + delete loadedLight; + } + + // Cleanup + std::filesystem::remove(yamlPath); +} + +TEST_CASE("SkyLightObject dirty callback", "[SkyLightObject]") +{ + SkyLightObject* light = new SkyLightObject(); + bool callbackCalled = false; + + light->setDirtyCallback([&callbackCalled]() { callbackCalled = true; }); + + // The callback should be invoked when properties change + light->TopIntensityChanged(nullptr, false); + REQUIRE(callbackCalled); + + callbackCalled = false; + light->TopColorChanged(nullptr, false); + REQUIRE(callbackCalled); + + callbackCalled = false; + light->MiddleIntensityChanged(nullptr, false); + REQUIRE(callbackCalled); + + callbackCalled = false; + light->BottomColorChanged(nullptr, false); + REQUIRE(callbackCalled); + + delete light; +} + +TEST_CASE("SkyLightObject and AreaLightObject independent instances", "[LightObjects]") +{ + AreaLightObject* areaLight = new AreaLightObject(); + SkyLightObject* skyLight = new SkyLightObject(); + + // Verify they have different scene lights + REQUIRE(areaLight->getSceneLight() != skyLight->getSceneLight()); + + // Modify one and verify the other is unaffected + areaLight->getDataObject().Intensity.SetValue(500.0f); + REQUIRE(skyLight->getDataObject().TopIntensity.GetValue() == Catch::Approx(1.0f)); + + skyLight->getDataObject().TopIntensity.SetValue(5.0f); + REQUIRE(areaLight->getDataObject().Intensity.GetValue() == Catch::Approx(500.0f)); + + delete areaLight; + delete skyLight; +} diff --git a/test/test_docReader.cpp b/test/test_docReader.cpp new file mode 100644 index 00000000..6eb2ff9a --- /dev/null +++ b/test/test_docReader.cpp @@ -0,0 +1,436 @@ +#include +#include + +#include "renderlib/serialize/docReader.h" +#include "renderlib/serialize/docReaderJson.h" +#include "renderlib/serialize/docReaderYaml.h" +#include "renderlib/serialize/docWriter.h" +#include "renderlib/serialize/docWriterJson.h" +#include "renderlib/serialize/docWriterYaml.h" +#include "renderlib/serialize/SerializationConstants.h" +#include "renderlib/core/prty/prtyObject.hpp" +#include "renderlib/core/prty/prtyPropertyUIInfo.hpp" +#include "renderlib/core/prty/prtyProperty.hpp" +#include "renderlib/core/prty/prtyIntegerTemplate.hpp" +#include "renderlib/core/prty/prtyFloat.hpp" +#include "renderlib/core/prty/prtyText.hpp" +#include "renderlib/core/prty/prtyBoolean.hpp" +#include "renderlib/Logging.h" + +#include +#include +#include + +// Helper function to create a test prtyObject +prtyObject* +createTestObject() +{ + prtyObject* obj = new prtyObject(); + + auto intProp = std::make_shared(new prtyInt32("testInt", 42), "", "Test Integer"); + auto floatProp = std::make_shared(new prtyFloat("testFloat", 3.14f), "", "Test Float"); + auto stringProp = std::make_shared(new prtyText("testString", "Hello World"), "", "Test String"); + auto boolProp = std::make_shared(new prtyBoolean("testBool", true), "", "Test Boolean"); + auto int8Prop = std::make_shared(new prtyInt8("testInt8", 127), "", "Test Int8"); + + obj->AddProperty(intProp); + obj->AddProperty(floatProp); + obj->AddProperty(stringProp); + obj->AddProperty(boolProp); + obj->AddProperty(int8Prop); + + return obj; +} + +// Helper function to write a test JSON file +void +createTestJsonFile(const std::string& filePath) +{ + prtyObject* obj = createTestObject(); + + docWriterJson writer; + writer.beginDocument(filePath); + writer.beginObject("testObject", "MYTYPE", 1); + + // Write properties directly + for (const auto& propUIInfo : obj->GetList()) { + int numProps = propUIInfo->GetNumberOfProperties(); + for (int i = 0; i < numProps; ++i) { + prtyProperty* prop = propUIInfo->GetProperty(i); + if (prop) { + prop->Write(writer); + } + } + } + + writer.endObject(); + writer.endDocument(); + + delete obj; +} + +// Helper function to write a test YAML file +void +createTestYamlFile(const std::string& filePath) +{ + prtyObject* obj = createTestObject(); + + docWriterYaml writer; + writer.beginDocument(filePath); + writer.beginObject("testObject", "MYTYPE", 1); + + // Write properties directly + for (const auto& propUIInfo : obj->GetList()) { + int numProps = propUIInfo->GetNumberOfProperties(); + for (int i = 0; i < numProps; ++i) { + prtyProperty* prop = propUIInfo->GetProperty(i); + if (prop) { + prop->Write(writer); + } + } + } + + writer.endObject(); + writer.endDocument(); + + delete obj; +} + +TEST_CASE("Read prtyObject from JSON", "[serialize][docReader]") +{ + std::string jsonPath = "test_read.json"; + + // Create a test file + createTestJsonFile(jsonPath); + REQUIRE(std::filesystem::exists(jsonPath)); + + // Read the file + docReaderJson reader; + REQUIRE(reader.beginDocument(jsonPath)); + + // Navigate to the object + REQUIRE(reader.beginObject("testObject")); + + // Read individual properties + prtyInt32 testInt("testInt"); + reader.readPrty(&testInt); + int32_t intValue = testInt.GetValue(); + REQUIRE(intValue == 42); + + prtyFloat testFloat("testFloat"); + reader.readPrty(&testFloat); + float floatValue = testFloat.GetValue(); + REQUIRE(floatValue == Catch::Approx(3.14f)); + + prtyText testString("testString"); + reader.readPrty(&testString); + std::string stringValue = testString.GetValue(); + REQUIRE(stringValue == "Hello World"); + + prtyBoolean testBool("testBool"); + reader.readPrty(&testBool); + bool boolValue = testBool.GetValue(); + REQUIRE(boolValue == true); + + prtyInt8 testInt8("testInt8"); + reader.readPrty(&testInt8); + int8_t int8Value = testInt8.GetValue(); + REQUIRE(int8Value == 127); + + reader.endObject(); + reader.endDocument(); + + // Cleanup + std::filesystem::remove(jsonPath); +} + +TEST_CASE("Read prtyObject from YAML", "[serialize][docReader]") +{ + std::string yamlPath = "test_read.yaml"; + + // Create a test file + createTestYamlFile(yamlPath); + REQUIRE(std::filesystem::exists(yamlPath)); + + // Read the file + docReaderYaml reader; + REQUIRE(reader.beginDocument(yamlPath)); + + // Navigate to the object + REQUIRE(reader.beginObject("testObject")); + + // Read individual properties + prtyInt32 testInt("testInt"); + reader.readPrty(&testInt); + int32_t intValue = testInt.GetValue(); + REQUIRE(intValue == 42); + + prtyFloat testFloat("testFloat"); + reader.readPrty(&testFloat); + float floatValue = testFloat.GetValue(); + REQUIRE(floatValue == Catch::Approx(3.14f)); + + prtyText testString("testString"); + reader.readPrty(&testString); + std::string stringValue = testString.GetValue(); + REQUIRE(stringValue == "Hello World"); + + prtyBoolean testBool("testBool"); + reader.readPrty(&testBool); + bool boolValue = testBool.GetValue(); + REQUIRE(boolValue == true); + + prtyInt8 testInt8("testInt8"); + reader.readPrty(&testInt8); + int8_t int8Value = testInt8.GetValue(); + REQUIRE(int8Value == 127); + + reader.endObject(); + reader.endDocument(); + + // Cleanup + std::filesystem::remove(yamlPath); +} + +TEST_CASE("Read and write roundtrip JSON", "[serialize][docReader]") +{ + std::string jsonPath = "test_roundtrip.json"; + + // Create original object and write + prtyObject* originalObj = createTestObject(); + docWriterJson writer; + writer.beginDocument(jsonPath); + writer.beginObject("testObject", "MYTYPE", 1); + + // Write properties directly + for (const auto& propUIInfo : originalObj->GetList()) { + int numProps = propUIInfo->GetNumberOfProperties(); + for (int i = 0; i < numProps; ++i) { + prtyProperty* prop = propUIInfo->GetProperty(i); + if (prop) { + prop->Write(writer); + } + } + } + + writer.endObject(); + writer.endDocument(); + + // Read back + docReaderJson reader; + REQUIRE(reader.beginDocument(jsonPath)); + REQUIRE(reader.beginObject("testObject")); + + // Create a new object and read properties into it + prtyObject* readObj = new prtyObject(); + + auto intProp = std::make_shared(new prtyInt32("testInt", 0), "", "Test Integer"); + auto floatProp = std::make_shared(new prtyFloat("testFloat", 0.0f), "", "Test Float"); + auto stringProp = std::make_shared(new prtyText("testString", ""), "", "Test String"); + auto boolProp = std::make_shared(new prtyBoolean("testBool", false), "", "Test Boolean"); + auto int8Prop = std::make_shared(new prtyInt8("testInt8", 0), "", "Test Int8"); + + readObj->AddProperty(intProp); + readObj->AddProperty(floatProp); + readObj->AddProperty(stringProp); + readObj->AddProperty(boolProp); + readObj->AddProperty(int8Prop); + + // Read properties + reader.readPrty(intProp->GetProperty(0)); + reader.readPrty(floatProp->GetProperty(0)); + reader.readPrty(stringProp->GetProperty(0)); + reader.readPrty(boolProp->GetProperty(0)); + reader.readPrty(int8Prop->GetProperty(0)); + + reader.endObject(); + reader.endDocument(); + + // Verify values match + REQUIRE(static_cast(intProp->GetProperty(0))->GetValue() == 42); + REQUIRE(static_cast(floatProp->GetProperty(0))->GetValue() == Catch::Approx(3.14f)); + REQUIRE(static_cast(stringProp->GetProperty(0))->GetValue() == "Hello World"); + REQUIRE(static_cast(boolProp->GetProperty(0))->GetValue() == true); + REQUIRE(static_cast(int8Prop->GetProperty(0))->GetValue() == 127); + + // Cleanup + delete originalObj; + delete readObj; + std::filesystem::remove(jsonPath); +} + +TEST_CASE("Read and write roundtrip YAML", "[serialize][docReader]") +{ + std::string yamlPath = "test_roundtrip.yaml"; + + // Create original object and write + prtyObject* originalObj = createTestObject(); + docWriterYaml writer; + writer.beginDocument(yamlPath); + writer.beginObject("testObject", "MYTYPE", 1); + + // Write properties directly + for (const auto& propUIInfo : originalObj->GetList()) { + int numProps = propUIInfo->GetNumberOfProperties(); + for (int i = 0; i < numProps; ++i) { + prtyProperty* prop = propUIInfo->GetProperty(i); + if (prop) { + prop->Write(writer); + } + } + } + + writer.endObject(); + writer.endDocument(); + + // Read back + docReaderYaml reader; + REQUIRE(reader.beginDocument(yamlPath)); + REQUIRE(reader.beginObject("testObject")); + + // Create a new object and read properties into it + prtyObject* readObj = new prtyObject(); + + auto intProp = std::make_shared(new prtyInt32("testInt", 0), "", "Test Integer"); + auto floatProp = std::make_shared(new prtyFloat("testFloat", 0.0f), "", "Test Float"); + auto stringProp = std::make_shared(new prtyText("testString", ""), "", "Test String"); + auto boolProp = std::make_shared(new prtyBoolean("testBool", false), "", "Test Boolean"); + auto int8Prop = std::make_shared(new prtyInt8("testInt8", 0), "", "Test Int8"); + + readObj->AddProperty(intProp); + readObj->AddProperty(floatProp); + readObj->AddProperty(stringProp); + readObj->AddProperty(boolProp); + readObj->AddProperty(int8Prop); + + // Read properties + reader.readPrty(intProp->GetProperty(0)); + reader.readPrty(floatProp->GetProperty(0)); + reader.readPrty(stringProp->GetProperty(0)); + reader.readPrty(boolProp->GetProperty(0)); + reader.readPrty(int8Prop->GetProperty(0)); + + reader.endObject(); + reader.endDocument(); + + // Verify values match + REQUIRE(static_cast(intProp->GetProperty(0))->GetValue() == 42); + REQUIRE(static_cast(floatProp->GetProperty(0))->GetValue() == Catch::Approx(3.14f)); + REQUIRE(static_cast(stringProp->GetProperty(0))->GetValue() == "Hello World"); + REQUIRE(static_cast(boolProp->GetProperty(0))->GetValue() == true); + REQUIRE(static_cast(int8Prop->GetProperty(0))->GetValue() == 127); + + // Cleanup + delete originalObj; + delete readObj; + std::filesystem::remove(yamlPath); +} + +TEST_CASE("Test peek operations JSON", "[serialize][docReader]") +{ + std::string jsonPath = "test_peek.json"; + + // Create a JSON file with type and version + std::ofstream file(jsonPath); + file << "{\n"; + file << " \"" << SerializationConstants::TYPE_KEY << "\": \"Camera\",\n"; + file << " \"" << SerializationConstants::VERSION_KEY << "\": 2,\n"; + file << " \"testData\": 123\n"; + file << "}\n"; + file.close(); + + // Read and peek + docReaderJson reader; + REQUIRE(reader.beginDocument(jsonPath)); + + std::string type = reader.peekObjectType(); + REQUIRE(type == "Camera"); + + int version = reader.peekVersion(); + REQUIRE(version == 2); + + reader.endDocument(); + + // Cleanup + std::filesystem::remove(jsonPath); +} + +TEST_CASE("Test peek operations YAML", "[serialize][docReader]") +{ + std::string yamlPath = "test_peek.yaml"; + + // Create a YAML file with type and version + std::ofstream file(yamlPath); + file << "---\n"; + file << SerializationConstants::TYPE_KEY << ": Camera\n"; + file << SerializationConstants::VERSION_KEY << ": 2\n"; + file << "testData: 123\n"; + file.close(); + + // Read and peek + docReaderYaml reader; + REQUIRE(reader.beginDocument(yamlPath)); + + std::string type = reader.peekObjectType(); + REQUIRE(type == "Camera"); + + int version = reader.peekVersion(); + REQUIRE(version == 2); + + reader.endDocument(); + + // Cleanup + std::filesystem::remove(yamlPath); +} + +TEST_CASE("Test hasKey operation JSON", "[serialize][docReader]") +{ + std::string jsonPath = "test_haskey.json"; + + createTestJsonFile(jsonPath); + + docReaderJson reader; + REQUIRE(reader.beginDocument(jsonPath)); + REQUIRE(reader.beginObject("testObject")); + + // Check for existing keys + REQUIRE(reader.hasKey("testInt")); + REQUIRE(reader.hasKey("testFloat")); + REQUIRE(reader.hasKey("testString")); + REQUIRE(reader.hasKey("testBool")); + + // Check for non-existing key + REQUIRE_FALSE(reader.hasKey("nonExistentKey")); + + reader.endObject(); + reader.endDocument(); + + // Cleanup + std::filesystem::remove(jsonPath); +} + +TEST_CASE("Test hasKey operation YAML", "[serialize][docReader]") +{ + std::string yamlPath = "test_haskey.yaml"; + + createTestYamlFile(yamlPath); + + docReaderYaml reader; + REQUIRE(reader.beginDocument(yamlPath)); + REQUIRE(reader.beginObject("testObject")); + + // Check for existing keys + REQUIRE(reader.hasKey("testInt")); + REQUIRE(reader.hasKey("testFloat")); + REQUIRE(reader.hasKey("testString")); + REQUIRE(reader.hasKey("testBool")); + + // Check for non-existing key + REQUIRE_FALSE(reader.hasKey("nonExistentKey")); + + reader.endObject(); + reader.endDocument(); + + // Cleanup + std::filesystem::remove(yamlPath); +} diff --git a/test/test_docWriter.cpp b/test/test_docWriter.cpp new file mode 100644 index 00000000..0bbab6f0 --- /dev/null +++ b/test/test_docWriter.cpp @@ -0,0 +1,216 @@ +#include + +#include "renderlib/serialize/docWriter.h" +#include "renderlib/serialize/docWriterJson.h" +#include "renderlib/serialize/docWriterYaml.h" +#include "renderlib/AppearanceObject.hpp" +#include "renderlib/core/prty/prtyObject.hpp" +#include "renderlib/core/prty/prtyPropertyUIInfo.hpp" +#include "renderlib/core/prty/prtyProperty.hpp" +#include "renderlib/core/prty/prtyIntegerTemplate.hpp" +#include "renderlib/core/prty/prtyFloat.hpp" +#include "renderlib/core/prty/prtyText.hpp" +#include "renderlib/core/prty/prtyBoolean.hpp" +#include "renderlib/Logging.h" + +#include +#include +#include +#include + +// Helper function to write a prtyObject to a docWriter +void +writePrtyObject(docWriter& writer, prtyObject* obj, const std::string& name) +{ + if (!obj) { + return; + } + + writer.beginObject(name.c_str(), "MYTYPE", 1); + + // Write properties directly + for (const auto& propUIInfo : obj->GetList()) { + int numProps = propUIInfo->GetNumberOfProperties(); + for (int i = 0; i < numProps; ++i) { + prtyProperty* prop = propUIInfo->GetProperty(i); + if (prop) { + prop->Write(writer); + } + } + } + + writer.endObject(); +} + +TEST_CASE("Serialize prtyObject to JSON", "[serialize][docWriter]") +{ + // Create a simple prtyObject with some properties + prtyObject obj; + + auto intProp = std::make_shared(new prtyInt32("testInt", 42), "", "Test Integer"); + auto floatProp = std::make_shared(new prtyFloat("testFloat", 3.14f), "", "Test Float"); + auto stringProp = std::make_shared(new prtyText("testString", "Hello World"), "", "Test String"); + auto boolProp = std::make_shared(new prtyBoolean("testBool", true), "", "Test Boolean"); + + obj.AddProperty(intProp); + obj.AddProperty(floatProp); + obj.AddProperty(stringProp); + obj.AddProperty(boolProp); + + // Write to JSON + docWriterJson jsonWriter; + std::string jsonPath = "test_output.json"; + + jsonWriter.beginDocument(jsonPath); + writePrtyObject(jsonWriter, &obj, "testObject"); + jsonWriter.endDocument(); + + // Verify the file was created + REQUIRE(std::filesystem::exists(jsonPath)); + + // Read and verify contents + std::ifstream file(jsonPath); + REQUIRE(file.is_open()); + + std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + file.close(); + + // Check that the JSON contains expected values + REQUIRE(content.find("testObject") != std::string::npos); + REQUIRE(content.find("testInt") != std::string::npos); + REQUIRE(content.find("42") != std::string::npos); + REQUIRE(content.find("testFloat") != std::string::npos); + REQUIRE(content.find("3.14") != std::string::npos); + REQUIRE(content.find("testString") != std::string::npos); + REQUIRE(content.find("Hello World") != std::string::npos); + REQUIRE(content.find("testBool") != std::string::npos); + + // Cleanup + std::filesystem::remove(jsonPath); +} + +TEST_CASE("Serialize prtyObject to YAML", "[serialize][docWriter]") +{ + // Create a simple prtyObject with some properties + prtyObject obj; + + auto intProp = std::make_shared(new prtyInt32("testInt", 42), "", "Test Integer"); + auto floatProp = std::make_shared(new prtyFloat("testFloat", 3.14f), "", "Test Float"); + auto stringProp = std::make_shared(new prtyText("testString", "Hello World"), "", "Test String"); + auto boolProp = std::make_shared(new prtyBoolean("testBool", true), "", "Test Boolean"); + + obj.AddProperty(intProp); + obj.AddProperty(floatProp); + obj.AddProperty(stringProp); + obj.AddProperty(boolProp); + + // Write to YAML + docWriterYaml yamlWriter; + std::string yamlPath = "test_output.yaml"; + + yamlWriter.beginDocument(yamlPath); + writePrtyObject(yamlWriter, &obj, "testObject"); + yamlWriter.endDocument(); + + // Verify the file was created + REQUIRE(std::filesystem::exists(yamlPath)); + + // Read and verify contents + std::ifstream file(yamlPath); + REQUIRE(file.is_open()); + + std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + file.close(); + + // Check that the YAML contains expected values + REQUIRE(content.find("---") != std::string::npos); // YAML header + REQUIRE(content.find("testObject:") != std::string::npos); + REQUIRE(content.find("testInt:") != std::string::npos); + REQUIRE(content.find("42") != std::string::npos); + REQUIRE(content.find("testFloat:") != std::string::npos); + REQUIRE(content.find("3.14") != std::string::npos); + REQUIRE(content.find("testString:") != std::string::npos); + REQUIRE(content.find("Hello World") != std::string::npos); + REQUIRE(content.find("testBool:") != std::string::npos); + + // Cleanup + std::filesystem::remove(yamlPath); +} + +TEST_CASE("Serialize AppearanceObject", "[serialize][docWriter][AppearanceObject]") +{ + AppearanceObject appearance; + + // Write to JSON + docWriterJson jsonWriter; + std::string jsonPath = "test_appearance.json"; + + jsonWriter.beginDocument(jsonPath); + writePrtyObject(jsonWriter, &appearance, "appearance"); + jsonWriter.endDocument(); + + // Verify the file was created + REQUIRE(std::filesystem::exists(jsonPath)); + + // Read and verify contents + std::ifstream file(jsonPath); + REQUIRE(file.is_open()); + + std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + file.close(); + + // Check that the JSON contains the appearance object + REQUIRE(content.find("appearance") != std::string::npos); + + // Cleanup + std::filesystem::remove(jsonPath); + + // Write to YAML + docWriterYaml yamlWriter; + std::string yamlPath = "test_appearance.yaml"; + + yamlWriter.beginDocument(yamlPath); + writePrtyObject(yamlWriter, &appearance, "appearance"); + yamlWriter.endDocument(); + + // Verify the file was created + REQUIRE(std::filesystem::exists(yamlPath)); + + // Cleanup + std::filesystem::remove(yamlPath); +} + +TEST_CASE("Validate nesting protection in docWriter", "[serialize][docWriter]") +{ + SECTION("Extra endObject call") + { + docWriterJson writer; + writer.beginDocument("test_nesting_error.json"); + writer.beginObject("obj1", "MYTYPE", 1); + writer.endObject(); + writer.endObject(); // Extra end - should log error but not crash + writer.endDocument(); + std::filesystem::remove("test_nesting_error.json"); + } + + SECTION("Mismatched begin/end types") + { + docWriterJson writer; + writer.beginDocument("test_mismatch_error.json"); + writer.beginObject("obj1", "MYTYPE", 1); + writer.endList(); // Wrong type - should log error + writer.endDocument(); + std::filesystem::remove("test_mismatch_error.json"); + } + + SECTION("Unclosed contexts") + { + docWriterJson writer; + writer.beginDocument("test_unclosed_error.json"); + writer.beginObject("obj1", "MYTYPE", 1); + writer.beginObject("obj2", "MYTYPE", 1); + // Missing endObject calls - should log error on endDocument + writer.endDocument(); + std::filesystem::remove("test_unclosed_error.json"); + } +} diff --git a/test/test_prty.cpp b/test/test_prty.cpp index 63d0f3b6..b53f95d5 100644 --- a/test/test_prty.cpp +++ b/test/test_prty.cpp @@ -3,7 +3,7 @@ #include #include "core/prty/prtyProperty.hpp" -#include "core/prty/prtyInt8.hpp" +#include "core/prty/prtyIntegerTemplate.hpp" #include "core/prty/prtyText.hpp" #include "core/prty/prtyPropertyTemplate.hpp" @@ -102,11 +102,11 @@ TEST_CASE("prtyProperty struct", "[prtyProperty]") } virtual const char* GetType() override { return "Foo"; } - virtual void Read(chReader& io_Reader) override + virtual void Read(docReader& io_Reader) override { // Implement reading from a reader if needed } - virtual void Write(chWriter& io_Writer) const override + virtual void Write(docWriter& io_Writer) const override { // Implement writing to a writer if needed }