diff --git a/apps/OpenSpace/ext/launcher/include/launcherwindow.h b/apps/OpenSpace/ext/launcher/include/launcherwindow.h index cbe15c696f..c8b63783de 100644 --- a/apps/OpenSpace/ext/launcher/include/launcherwindow.h +++ b/apps/OpenSpace/ext/launcher/include/launcherwindow.h @@ -62,18 +62,16 @@ public: bool wasLaunchSelected() const; /** - * Returns the selected profile name when launcher window closed. + * Returns the selected profile name when the launcher window closed. * - * \return The name of selected profile (this is only the name without file extension - * and without path) + * \return The path to the selected profile */ std::string selectedProfile() const; /** - * Returns the selected sgct window configuration when launcher window closed. + * Returns the selected SGCT window configuration when the launcher window closed. * - * \return The name of selected profile (this is only the name without file extension - * and without path) + * \return The path to the selected profile */ std::string selectedWindowConfig() const; diff --git a/apps/OpenSpace/ext/launcher/include/profile/timedialog.h b/apps/OpenSpace/ext/launcher/include/profile/timedialog.h index 13eaf03910..cb08267e1b 100644 --- a/apps/OpenSpace/ext/launcher/include/profile/timedialog.h +++ b/apps/OpenSpace/ext/launcher/include/profile/timedialog.h @@ -30,10 +30,10 @@ #include class QCheckBox; -class QComboBox; class QDateTimeEdit; class QLabel; class QLineEdit; +class QTabWidget; class TimeDialog final : public QDialog { Q_OBJECT @@ -48,18 +48,15 @@ public: TimeDialog(QWidget* parent, std::optional* time); private slots: - void enableAccordingToType(int); void approved(); private: void createWidgets(); - void enableFormatForAbsolute(bool enableAbs); std::optional* _time = nullptr; openspace::Profile::Time _timeData; - bool _initializedAsAbsolute = true; - QComboBox* _typeCombo = nullptr; + QTabWidget* _tabWidget = nullptr; QLabel* _absoluteLabel = nullptr; QDateTimeEdit* _absoluteEdit = nullptr; QLabel* _relativeLabel = nullptr; diff --git a/apps/OpenSpace/ext/launcher/src/profile/timedialog.cpp b/apps/OpenSpace/ext/launcher/src/profile/timedialog.cpp index 22be9609f9..a1c86a2f7b 100644 --- a/apps/OpenSpace/ext/launcher/src/profile/timedialog.cpp +++ b/apps/OpenSpace/ext/launcher/src/profile/timedialog.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -45,8 +46,6 @@ TimeDialog::TimeDialog(QWidget* parent, std::optional* setWindowTitle("Time"); createWidgets(); - const QStringList types = { "Absolute", "Relative" }; - _typeCombo->addItems(types); if (_time->has_value()) { _timeData = **_time; if (_timeData.type == Profile::Time::Type::Relative) { @@ -66,44 +65,75 @@ TimeDialog::TimeDialog(QWidget* parent, std::optional* } _startPaused->setChecked(_timeData.startPaused); - _initializedAsAbsolute = (_timeData.type == Profile::Time::Type::Absolute); - enableAccordingToType(static_cast(_timeData.type)); + if (_timeData.type == Profile::Time::Type::Relative) { + _relativeEdit->setText(QString::fromStdString(_timeData.value)); + _relativeEdit->setFocus(Qt::OtherFocusReason); + } + else { + const size_t tIdx = _timeData.value.find_first_of('T', 0); + const QString importDate = QString::fromStdString( + _timeData.value.substr(0, tIdx) + ); + const QString importTime = QString::fromStdString( + _timeData.value.substr(tIdx + 1) + ); + _absoluteEdit->setDate(QDate::fromString(importDate, Qt::DateFormat::ISODate)); + _absoluteEdit->setTime(QTime::fromString(importTime)); + _relativeEdit->clear(); + _absoluteEdit->setFocus(Qt::OtherFocusReason); + } + + _tabWidget->setCurrentIndex(static_cast(_timeData.type)); } void TimeDialog::createWidgets() { QBoxLayout* layout = new QVBoxLayout(this); + + _tabWidget = new QTabWidget; + { - layout->addWidget(new QLabel("Time Type")); - _typeCombo = new QComboBox; - _typeCombo->setAccessibleName("Time type"); - _typeCombo->setToolTip("Types: Absolute defined time or Relative to actual time"); - connect( - _typeCombo, QOverload::of(&QComboBox::currentIndexChanged), - this, &TimeDialog::enableAccordingToType - ); - layout->addWidget(_typeCombo); - } - { + QWidget* container = new QWidget; + QBoxLayout* l = new QVBoxLayout(container); _absoluteLabel = new QLabel("Absolute UTC:"); - layout->addWidget(_absoluteLabel); + l->addWidget(_absoluteLabel); _absoluteEdit = new QDateTimeEdit; _absoluteEdit->setDisplayFormat("yyyy-MM-dd T hh:mm:ss"); _absoluteEdit->setDateTime(QDateTime::currentDateTime()); _absoluteEdit->setAccessibleName("Set absolute time"); - layout->addWidget(_absoluteEdit); + l->addWidget(_absoluteEdit); + + l->addStretch(); + + _tabWidget->addTab(container, "Absolute"); } { + QWidget* container = new QWidget; + QBoxLayout* l = new QVBoxLayout(container); _relativeLabel = new QLabel("Relative Time:"); - layout->addWidget(_relativeLabel); + l->addWidget(_relativeLabel); _relativeEdit = new QLineEdit; _relativeEdit->setAccessibleName("Set relative time"); _relativeEdit->setToolTip( "String for relative time to actual (e.g. \"-1d\" for back 1 day)" ); - layout->addWidget(_relativeEdit); + l->addWidget(_relativeEdit); + + QLabel* desc = new QLabel( + "This field modifies the default start time. It has to be of the form " + "[-]XX(s,m,h,d,M,y). For example '-1d' will cause the profile to start at " + "yesterday's date." + ); + desc->setObjectName("information"); + desc->setWordWrap(true); + l->addWidget(desc); + + _tabWidget->addTab(container, "Relative"); } + + layout->addWidget(_tabWidget); + { _startPaused = new QCheckBox("Start with time paused"); _startPaused->setChecked(false); @@ -123,47 +153,9 @@ void TimeDialog::createWidgets() { } } -void TimeDialog::enableAccordingToType(int idx) { - const Profile::Time::Type comboIdx = static_cast(idx); - const bool setFormatForAbsolute = (comboIdx == Profile::Time::Type::Absolute); - enableFormatForAbsolute(setFormatForAbsolute); - _typeCombo->setCurrentIndex(idx); - if (comboIdx == Profile::Time::Type::Relative) { - _relativeEdit->setText("Relative Time:"); - if (_initializedAsAbsolute) { - _relativeEdit->setText("0d"); - } - else { - _relativeEdit->setText(QString::fromStdString(_timeData.value)); - } - _relativeEdit->setFocus(Qt::OtherFocusReason); - } - else { - _relativeEdit->setText("Relative Time:"); - const size_t tIdx = _timeData.value.find_first_of('T', 0); - const QString importDate = QString::fromStdString( - _timeData.value.substr(0, tIdx) - ); - const QString importTime = QString::fromStdString( - _timeData.value.substr(tIdx + 1) - ); - _absoluteEdit->setDate(QDate::fromString(importDate, Qt::DateFormat::ISODate)); - _absoluteEdit->setTime(QTime::fromString(importTime)); - _relativeEdit->clear(); - _absoluteEdit->setFocus(Qt::OtherFocusReason); - } -} - -void TimeDialog::enableFormatForAbsolute(bool enableAbs) { - _absoluteLabel->setEnabled(enableAbs); - _absoluteEdit->setEnabled(enableAbs); - _relativeLabel->setEnabled(!enableAbs); - _relativeEdit->setEnabled(!enableAbs); -} - void TimeDialog::approved() { constexpr int Relative = static_cast(Profile::Time::Type::Relative); - if (_typeCombo->currentIndex() == Relative) { + if (_tabWidget->currentIndex() == Relative) { if (_relativeEdit->text().isEmpty()) { *_time = std::nullopt; } diff --git a/apps/OpenSpace/main.cpp b/apps/OpenSpace/main.cpp index d403c4fe6a..ba64091dbb 100644 --- a/apps/OpenSpace/main.cpp +++ b/apps/OpenSpace/main.cpp @@ -1415,9 +1415,39 @@ int main(int argc, char* argv[]) { openspace::Settings settings = loadSettings(); settings.hasStartedBefore = true; - const std::filesystem::path p = global::configuration->profile; - const std::filesystem::path reducedName = p.filename().replace_extension(); - settings.profile = reducedName.string(); + const std::filesystem::path profile = global::configuration->profile; + + const bool isDefaultProfile = ghoul::filesystem::isSubdirectory( + profile, + absPath("${PROFILES}") + ); + const bool isUserProfile = ghoul::filesystem::isSubdirectory( + profile, + absPath("${USER_PROFILES}") + ); + + if (isDefaultProfile) { + std::filesystem::path p = std::filesystem::relative( + profile, + absPath("${PROFILES}") + ); + p.replace_extension(); + settings.profile = p.string(); + } + else if (isUserProfile) { + std::filesystem::path p = std::filesystem::relative( + profile, + absPath("${USER_PROFILES}") + ); + p.replace_extension(); + settings.profile = p.string(); + } + else { + LWARNING( + "Cannot save remembered profile when starting a profile that is not in " + "the data/profiles or user/data/profiles folder." + ); + } settings.configuration = isGeneratedWindowConfig ? "" : global::configuration->windowConfiguration; diff --git a/ext/ghoul b/ext/ghoul index e7ff4891f4..2972e7b963 160000 --- a/ext/ghoul +++ b/ext/ghoul @@ -1 +1 @@ -Subproject commit e7ff4891f43e1358e03f344faba81e137334b6dc +Subproject commit 2972e7b963abe1a0ddd57efd8aaf28840a8da37e diff --git a/include/openspace/scene/assetmanager.h b/include/openspace/scene/assetmanager.h index f3577dbecf..bb10f252d0 100644 --- a/include/openspace/scene/assetmanager.h +++ b/include/openspace/scene/assetmanager.h @@ -77,6 +77,18 @@ public: */ void remove(const std::string& path); + /** + * Reloads the asset at the provided \p path as a new root asset of the AssetManager. If the + * asset at that path was not previously loaded, acts the same as the add function. + * + * \param path The path from which the Asset is loaded. This path can be either + * relative to the base directory (the path starting with . or ..), an absolute + * path (that path starting with *:/ or /) or relative to the global asset root + * (if the path starts any other way) + * \pre \p path must not be the empty string + */ + void reload(const std::string& path); + /** * Update function that should be called at least once per frame that will load all * queued asset loads and asset removal. @@ -180,6 +192,9 @@ private: std::filesystem::path generateAssetPath(const std::filesystem::path& baseDirectory, const std::string& assetPath) const; + void runRemoveQueue(); + void runAddQueue(); + // // Assets // diff --git a/modules/globebrowsing/src/geojson/geojsonmanager.cpp b/modules/globebrowsing/src/geojson/geojsonmanager.cpp index b3fcda6e70..a615c3f71e 100644 --- a/modules/globebrowsing/src/geojson/geojsonmanager.cpp +++ b/modules/globebrowsing/src/geojson/geojsonmanager.cpp @@ -37,7 +37,7 @@ namespace { namespace openspace::globebrowsing { GeoJsonManager::GeoJsonManager() - : properties::PropertyOwner({ "GeoJson", "Geometry Overlays" }) + : properties::PropertyOwner({ "GeographicOverlays", "Geographic Overlays" }) {} void GeoJsonManager::initialize(RenderableGlobe* globe) { diff --git a/openspace.cfg b/openspace.cfg index 66a3c1063d..a3cc664bf3 100644 --- a/openspace.cfg +++ b/openspace.cfg @@ -20,25 +20,15 @@ SGCTConfig = sgct.config.single{vsync=false} -- Streaming OpenSpace via Spout to OBS -- SGCTConfig = sgct.config.single{2560, 1440, shared=true, name="WV_OBS_SPOUT1"} --- Elumenati Configs --- SGCTConfig = "${CONFIG}/spout_output_flat.json" --- SGCTConfig = "${CONFIG}/spout_output_cubemap.json" --- SGCTConfig = "${CONFIG}/spout_output_equirectangular.json" --- SGCTConfig = "${CONFIG}/spout_output_fisheye.json" - -- for more details about sgct configuration options see: -- https://sgct.github.io/configuration-files.html -- To use a sgct configuration file set the variable like below --- SGCTConfig = "${CONFIG}/single_gui.xml" +-- SGCTConfig = "${CONFIG}/single_gui.json" -- In the config/ folder we provide the some predefined files which you can modify. -- fullscreen1080.json: fullscreen window at 1920x1080 -- gui_projector.json: one window for the gui and a second fullscreen window for rendering -- on the second monitor --- openvr_htcVive.json: window for VR on HTC Vive, only works if OpenSpace is compiled --- custom with the openvr support --- openvr_oculusRiftCv1.json: window for VR on Oculus Rift, only works if OpenSpace is --- compiled custom with the openvr support -- single.json: regular window -- single_fisheye.json: regular window with fisheye rendering -- single_fisheye_gui.json: one window for the gui, one window for fisheye rendering @@ -50,14 +40,8 @@ SGCTConfig = sgct.config.single{vsync=false} -- mirror rendering -- two_nodes.json: a configuration for running two instances of openspace, used for -- multiple projection systems --- spout_output_flat.json: a window where the rendering is sent to spout instead of the --- display -- spout_output_cubemap.json: a window where the rendering is sent to spout (max of 6 spout -- instances) instead of the display --- spout_output_equirectangular.json: a window where the rendering is sent to spout --- (equirectangular output) instead of the display --- spout_output_fisheye.json: a window where the rendering is sent to spout (fisheye --- output) instead of the display -- Variable: Profile -- Sets the profile that should be loaded by OpenSpace. @@ -120,7 +104,7 @@ Paths = { SCRIPTS = "${BASE}/scripts", SHADERS = "${BASE}/shaders", TEMPORARY = "${BASE}/temp", - GLOBEBROWSING = os.getenv("OPENSPACE_GLOBEBROWSING") or "${BASE}/../OpenSpaceData" + GLOBEBROWSING = os.getenv("OPENSPACE_GLOBEBROWSING") or "${USER}/globebrowsing" } ModuleConfigurations = { diff --git a/scripts/configuration_helper.lua b/scripts/configuration_helper.lua index 3ab2f6e18f..c24b79eb52 100644 --- a/scripts/configuration_helper.lua +++ b/scripts/configuration_helper.lua @@ -1,4 +1,8 @@ --- Helper functions that are useful to customize the openspace.cfg loading +-- Helper functions that are useful to customize the openspace.cfg loading. Only edit this +-- file if you know what you are doing. There are a few implicit variables that are passed +-- into this file and are available: `ScreenResolution` contains the size (in pixels) of +-- the main monitor. `TableToJson` is a function that receives a Lua table and returns the +-- string-representation of that table converted to JSON -- ####################################################################################### -- ## Public functions ## @@ -8,7 +12,7 @@ sgct = {} sgct.config = {} --- Creates a configuration file similar to the default 'single.xml': +-- Creates a configuration file similar to the default 'single.json': -- The parameter is a table and can contain the follow attributes: -- first argument: horizontal window size {default: 1280} @@ -40,38 +44,6 @@ function sgct.config.single(arg) end --- Creates a configuration file similar to the default 'single_fisheye.xml' --- The parameter is a table and can contain the follow attributes: - --- first argument: horizontal window size {default: 1280} --- second argument: vertical window size {default: 720} --- res: A table containing the horizontal and vertical resolution [example: res={3840, 2160}] --- pos: The position of the window on the screen [example: pos={50, 100}] {default: {50, 50}} --- fullScreen: Whether the application should run in exclusive full screen [example: fullScreen=true] {default: false} --- border: Whether the application should have window decorations (aka. border) [example: border=false] {default: true} --- monitor: Determines the monitor on which the application is started [example: monitor=2] {default: 0} --- shared: Determines whether the contents of the window should be shared using the SPOUT library [example: shared=true] {default: false} - --- Expert settings: --- vsync: Whether the rendering speed is locked to the refreshrate [example: vsync=true] {default: true} --- refreshRate: If vsync is enabled, this is the target framerate [example: refreshRate=30] {default: infinity} --- stereo: Select the stereo rendering mode as supported by SGCT [example: stereo='anaglyph_red_cyan'] {default: 'none'} --- msaa: The multisampling anti-aliasing factor [example: msaa=8] {default: 4} --- scene: Global settings to all scene objects (offset, orientation, scaling; each optional) --- [example: scene = {offset = {x = 1.0, y = 1.0, z = 2.0}, orientation = { yaw = 120, pitch = 15, roll = 0.0 }, scale = 10.0}] --- sgctDebug: Determines whether a higher debug level in SGCT is enabled [example: sgctDebug=true] {default: false} --- fov: The field of view for the fisheye [example: fov=360] {default: 180} --- quality: The quality setting for the cubemap textures [example: quality="4k"] {default: "1k"} --- tilt: The forwards tilt of the fisheye, relative to the center [example: tilt=90] {default: 0.0} --- background: The background color used outside of the fisheye rendering [example: backgruound={r=1.0, g=0.25, b=0.25, a=1.0}] {default: {r=0.1, g=0.1, b=0.1, a=1.0}} - --- Thus this function can be called the following ways: --- sgct.config.fisheye() -> Leading to a 1280x720 resolution window --- sgct.config.fisheye(640, 640) -> Leading to a 640x650 resolution window --- sgct.config.fisheye(640, 360, res={3840, 3840}) -> 640x360 window with 4K rendering resolution --- sgct.config.fisheye(msaa=1) -> 1280x720 resolution without multisampling -function sgct.config.fisheye(arg) end - -- Global variable storing the name of the config function called at initialization sgctconfiginitializeString = "" @@ -84,233 +56,6 @@ sgctconfiginitializeString = "" ########################################################################################## ]]-- -function generateSingleViewportFOV(down, up, left, right, tracked) - local result = {} - - table.insert(result, [[ {]]) - - if tracked ~= nil then - table.insert(result, [[ "tracked": ]] .. tostring(tracked) .. [[,]]) - end - - table.insert(result, [[ "projection": {]]) - table.insert(result, [[ "type": "PlanarProjection",]]) - table.insert(result, [[ "fov": {]]) - table.insert(result, [[ "left": ]] .. left .. [[,]]) - table.insert(result, [[ "right": ]] .. right .. [[,]]) - table.insert(result, [[ "up": ]] .. up .. [[,]]) - table.insert(result, [[ "down": ]] .. down) - table.insert(result, [[ }]]) - table.insert(result, [[ }]]) - table.insert(result, [[ }]]) - - return table.concat(result, "\n") -end - - - -function generateFisheyeViewport(fov, quality, tilt, background, crop, offset, tracked) - local result = {} - - table.insert(result, [[ {]]) - - if tracked ~= nil then - table.insert(result, [[ "tracked": ]] .. tostring(tracked) .. [[,]]) - end - - table.insert(result, [[ "projection": {]]) - table.insert(result, [[ "type": "FisheyeProjection",]]) - - if fov then - table.insert(result, [[ "fov": ]] .. fov .. [[,]]) - end - table.insert(result, [[ "quality": "]] .. quality .. [[",]]) - table.insert(result, [[ "tilt": ]] .. tilt .. [[,]]) - - if crop then - table.insert(result, [[ "crop": {]]) - table.insert(result, [[ "left": ]] .. crop["left"] .. [[,]]) - table.insert(result, [[ "right": ]] .. crop["right"] .. [[,]]) - table.insert(result, [[ "top": ]] .. crop["top"] .. [[,]]) - table.insert(result, [[ "bottom": ]] .. crop["bottom"]) - table.insert(result, [[ },]]) - end - - if offset then - table.insert(result, [[ "offset": {]]) - table.insert(result, [[ "x": ]] .. offset["x"] .. [[,]]) - table.insert(result, [[ "y": ]] .. offset["y"] .. [[,]]) - table.insert(result, [[ "z": ]] .. offset["z"]) - table.insert(result, [[ },]]) - end - - table.insert(result, [[ "background": {]]) - table.insert(result, [[ "r": ]] .. background["r"] .. [[,]]) - table.insert(result, [[ "g": ]] .. background["g"] .. [[,]]) - table.insert(result, [[ "b": ]] .. background["b"] .. [[,]]) - table.insert(result, [[ "a": ]] .. background["a"]) - table.insert(result, [[ }]]) - table.insert(result, [[ }]]) - table.insert(result, [[ }]]) - - return table.concat(result, "\n") -end - - - -function generateWindow(result, fullScreen, msaa, border, monitor, tags, stereo, pos, - size, res, viewport) - table.insert(result, [[ "name": "OpenSpace",]]) - - if fullScreen ~= nil then - table.insert(result, [[ "fullscreen": ]] .. tostring(fullScreen) .. [[,]]) - end - - if msaa then - table.insert(result, [[ "msaa": ]] .. msaa .. [[,]]) - end - - if border ~= nil then - table.insert(result, [[ "border": ]] .. tostring(border) .. [[,]]) - end - - if monitor then - table.insert(result, [[ "monitor": ]] .. monitor .. [[,]]) - end - - if #(tags) > 0 then - for i, v in ipairs(tags) do - tags[i] = "\"" .. v .. "\"" - end - local t = table.concat(tags, [[,]]) - table.insert(result, [[ "tags": [ ]] .. t .. [[ ], ]]) - end - - if stereo then - table.insert(result, [[ "stereo": "]] .. stereo .. [[",]]) - end - - local px = pos[1] - local py = pos[2] - table.insert(result, [[ "pos": { "x": ]] .. px .. [[, "y": ]] .. py .. [[ },]]) - - local sx = size[1] - local sy = size[2] - table.insert(result, [[ "size": { "x": ]] .. sx .. [[, "y": ]] .. sy .. [[ },]]) - - if res then - res[1] = res[1] or size[1] - res[2] = res[2] or size[2] - table.insert(result, [[ "res": {]]) - table.insert(result, [[ "x": ]] .. res[1] .. [[,]]) - table.insert(result, [[ "y": ]] .. res[2]) - table.insert(result, [[ },]]) - end - - table.insert(result, [[ "viewports": []]) - - table.insert(result, viewport) - table.insert(result, [[ ] ]]) -end - - - -function generateUser(result) - table.insert(result, [[ {]]) - table.insert(result, [[ "eyeseparation": 0.06,]]) - table.insert(result, [[ "pos": { "x": 0.0, "y": 0.0, "z": 0.0 }]]) - table.insert(result, [[ }]]) -end - - - -function generateScene(result, scene) - if scene == nil then - return nil - end - - local offset = scene["offset"] or { x = 0.0, y = 0.0, z = 0.0 } - local orientation = scene["orientation"] or { yaw = 0.0, pitch = 0.0, roll = 0.0 } - local scale = scene["scale"] or 1.0 - - table.insert(result, [[{]]) - table.insert(result, [[ "offset": {]]) - table.insert(result, [[ "x": ]] .. offset["x"] .. [[,]]) - table.insert(result, [[ "y": ]] .. offset["y"] .. [[,]]) - table.insert(result, [[ "z": ]] .. offset["z"] .. [[,]]) - table.insert(result, [[ },]]) - table.insert(result, [[ "orientation": {]]) - table.insert(result, [[ "yaw": ]] .. orientation["yaw"] .. [[,]]) - table.insert(result, [[ "pitch": ]] .. orientation["pitch"] .. [[,]]) - table.insert(result, [[ "roll": ]] .. orientation["roll"] .. [[,]]) - table.insert(result, [[ },]]) - table.insert(result, [[ "scale": ]] .. scale) - table.insert(result, [[}]]) -end - - - -function generateSettings(result, refreshRate, vsync) - table.insert(result, [[ "display": {]]) - - if refreshRate then - table.insert(result, [[ "refreshrate": ]] .. refreshRate .. [[,]]) - end - - if vsync then - table.insert(result, [[ "swapinterval": 1]]) - else - table.insert(result, [[ "swapinterval": 0]]) - end - - table.insert(result, [[ }]]) -end - - - -function generateCluster(arg, viewport) - local result = {} - - table.insert(result, [[{]]) - table.insert(result, [[ "version": 1,]]) - table.insert(result, [[ "masteraddress": "127.0.0.1",]]) - - if arg["sgctDebug"] ~= nil then - table.insert(result, [[ "debug": ]] .. tostring(arg["sgctdebug"]) .. [[,]]) - end - - table.insert(result, [[ "settings": {]]) - generateSettings(result, arg["refreshRate"], arg["vsync"]) - table.insert(result, [[ },]]) - - if arg["scene"] then - table.insert(result, [[ "scene": {]]) - generateScene(result, arg["scene"]) - table.insert(result, [[ },]]) - end - - table.insert(result, [[ "nodes": []]) - table.insert(result, [[ {]]) - table.insert(result, [[ "address": "127.0.0.1",]]) - table.insert(result, [[ "port": 20401,]]) - table.insert(result, [[ "windows": []]) - table.insert(result, [[ {]]) - generateWindow(result, arg["fullScreen"], arg["msaa"], arg["border"], arg["monitor"], - arg["tags"], arg["stereo"], arg["pos"], arg["size"], arg["res"], viewport) - table.insert(result, [[ }]]) - table.insert(result, [[ ] ]]) - table.insert(result, [[ }]]) - table.insert(result, [[ ],]]) - table.insert(result, [[ "users": []]) - generateUser(result) - table.insert(result, [[ ] ]]) - table.insert(result, [[}]]) - - return table.concat(result, "\n") -end - - - function check(type_str, arg, param, subparam_type) local t = type(arg[param]) assert(t == type_str or t == "nil", param .. " must be a " .. type_str .. " or nil") @@ -327,55 +72,151 @@ end -function generateSingleWindowConfig(arg, viewport) - check("table", arg, "res", "number") - check("table", arg, "tags", "string") - check("table", arg, "pos", "number") - check("boolean", arg, "shared") - check("boolean", arg, "fullScreen") - check("boolean", arg, "border") - check("boolean", arg, "vsync") - check("boolean", arg, "sgctDebug") - check("number", arg, "monitor") - check("number", arg, "msaa") - check("number", arg, "refreshRate") - check("string", arg, "stereo") +function generateSingleViewportFOV(down, up, left, right, tracked) + local result = { + projection = { + type = "PlanarProjection", + fov = { + left = left, + right = right, + up = up, + down = down + } + } + } - check("table", arg, "scene") - if arg["scene"] then - check("table", arg["scene"], "offset", "number") - check("table", arg["scene"], "orientation", "number") - check("number", arg["scene"], "scale") + if tracked ~= nil then + result["tracked"] = tracked end - if arg["shared"] ~= nil then - local t = arg["tags"] - t[#t + 1] = "Spout" - end - - return generateCluster(arg, viewport) + return result end -function sgct.makeConfig(config) - -- - -- Create a file in the operating system's `temp` folder - -- +function generateWindow(fullScreen, msaa, border, monitor, tags, stereo, pos, size, res, + viewport) + local result = { + name = "OpenSpace", + pos = { x = pos[1], y = pos[2] }, + size = { x = size[1], y = size[2] }, + viewports = { viewport } + } - -- tmpname returns the name to a randomly chosen file within the `temp` folder - local tempFile = os.tmpname() - -- `package.config` contains the path separator used on the current platform - local directorySeparator = package.config:match("([^\n]*)\n?") - -- Remove the filename and only get the path to the temp folder - local separatorIdx = tempFile:reverse():find(directorySeparator) - local tempFolder = tempFile:sub(1, #tempFile - separatorIdx + 1) + if fullScreen ~= nil then + result["fullscreen"] = fullScreen + end - local configFile = tempFolder .. "openspace-window-configuration.json" - local file = io.open(configFile, "w+") - file:write(config) - io.close(file) - return configFile + if msaa then + result["msaa"] = msaa + end + + if border ~= nil then + result["border"] = border + end + + if monitor then + result["monitor"] = monitor + end + + if #(tags) > 0 then + for i, v in ipairs(tags) do + tags[i] = "\"" .. v .. "\"" + end + local t = table.concat(tags, [[,]]) + result["tags"] = t + end + + if stereo then + result["stereo"] = stereo + end + + if res then + res[1] = res[1] or size[1] + res[2] = res[2] or size[2] + result["res"] = { x = res[1], y = res[2] } + end + + return result +end + + + +function generateScene(result, scene) + if scene == nil then + return nil + end + + local offset = scene["offset"] or { x = 0.0, y = 0.0, z = 0.0 } + local orientation = scene["orientation"] or { yaw = 0.0, pitch = 0.0, roll = 0.0 } + local scale = scene["scale"] or 1.0 + + local result = { + offset = offset, + orientation = orientation, + scale = scale + } + + return result +end + + + +function generateSettings(refreshRate, vsync) + local result = { + display = {} + } + + if refreshRate then + result["display"]["refreshrate"] = refreshRate + end + + if vsync then + result["display"]["swapinterval"] = 1 + else + result["display"]["swapinterval"] = 0 + end + + return result +end + + + +function generateCluster(arg) + local viewport = generateSingleViewportFOV(arg["fov"]["down"], arg["fov"]["up"], + arg["fov"]["left"], arg["fov"]["right"], arg["tracked"]) + + local result = { + version = 1, + masteraddress = "127.0.0.1", + settings = generateSettings(arg["refreshRate"], arg["vsync"]), + nodes = { + { + address = "127.0.0.1", + port = 20401, + windows = { + generateWindow(arg["fullScreen"], arg["msaa"], arg["border"], arg["monitor"], + arg["tags"], arg["stereo"], arg["pos"], arg["size"], arg["res"], viewport) + } + } + }, + users = { + { + eyeseparation = 0.06, + pos = { x = 0.0, y = 0.0, z = 0.0 } + } + } + } + + if arg["sgctDebug"] ~= nil then + result["debug"] = arg["sgctdebug"] + end + + if arg["scene"] then + result["scene"] = generateScene(arg["scene"]) + end + + return result end @@ -428,59 +269,51 @@ function sgct.config.single(arg) end check("boolean", arg, "tracked") + check("table", arg, "res", "number") + check("table", arg, "tags", "string") + check("table", arg, "pos", "number") + check("boolean", arg, "shared") + check("boolean", arg, "fullScreen") + check("boolean", arg, "border") + check("boolean", arg, "vsync") + check("boolean", arg, "sgctDebug") + check("number", arg, "monitor") + check("number", arg, "msaa") + check("number", arg, "refreshRate") + check("string", arg, "stereo") + + check("table", arg, "scene") + if arg["scene"] then + check("table", arg["scene"], "offset", "number") + check("table", arg["scene"], "orientation", "number") + check("number", arg["scene"], "scale") + end + + if arg["shared"] ~= nil then + local t = arg["tags"] + t[#t + 1] = "Spout" + end + sgctconfiginitializeString = "sgct.config.single" arg["tracked"] = arg["tracked"] or true - local viewport = generateSingleViewportFOV(arg["fov"]["down"], arg["fov"]["up"], - arg["fov"]["left"], arg["fov"]["right"], arg["tracked"]) + -- + -- Create a file in the operating system's `temp` folder + -- - return sgct.makeConfig(generateSingleWindowConfig(arg, viewport)) -end - - - -function sgct.config.fisheye(arg) - arg = arg or {} - - check("number", arg, 1) - check("number", arg, 2) - - if type(arg[1]) == "number" and type(arg[2]) == "number" then - arg["size"] = { arg[1], arg[2] } - arg[1] = nil - arg[2] = nil - else - -- No numbers specified, therefore we want to use the screen resolution of the primary - -- monitor to derive the resolution - -- ScreenResolution is a variable that got injected into the openspace.cfg by the - -- OpenSpace code prior to loading this file - - local scale_factor = 2.0/3.0; - arg["size"] = { ScreenResolution.x * scale_factor, ScreenResolution.y * scale_factor } - end - - check("number", arg, "fov") - check("number", arg, "tilt") - check("string", arg, "quality") - check("table", arg, "background", "number") - check("table", arg, "crop", "number") - check("table", arg, "offset", "number") - - sgctconfiginitializeString = "sgct.config.fisheye" - - arg["vsync"] = arg["vsync"] or false - arg["tags"] = arg["tags"] or {} - arg["pos"] = arg["pos"] or { 50, 50 } - arg["size"] = arg["size"] or { 1024, 1024 } - - arg["quality"] = arg["quality"] or "1k" - arg["tilt"] = arg["tilt"] or 90.0 - arg["background"] = arg["background"] or { r = 0.0, g = 0.0, b = 0.0, a = 1.0 } - arg["tracked"] = arg["tracked"] or false - - local viewport = generateFisheyeViewport(arg["fov"], arg["quality"], arg["tilt"], - arg["background"], arg["crop"], arg["offset"], arg["tracked"]) - - return sgct.makeConfig(generateSingleWindowConfig(arg, viewport)) + local config = generateCluster(arg) + -- tmpname returns the name to a randomly chosen file within the `temp` folder + local tempFile = os.tmpname() + -- `package.config` contains the path separator used on the current platform + local directorySeparator = package.config:match("([^\n]*)\n?") + -- Remove the filename and only get the path to the temp folder + local separatorIdx = tempFile:reverse():find(directorySeparator) + local tempFolder = tempFile:sub(1, #tempFile - separatorIdx + 1) + + local configFile = tempFolder .. "openspace-window-configuration.json" + local file = io.open(configFile, "w+") + file:write(TableToJson(config)) + io.close(file) + return configFile end diff --git a/src/engine/configuration.cpp b/src/engine/configuration.cpp index 1f06e2ca93..64ccb7213d 100644 --- a/src/engine/configuration.cpp +++ b/src/engine/configuration.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -770,6 +771,19 @@ Configuration loadConfigurationFromFile(const std::filesystem::path& configurati ); ghoul::lua::runScript(result.state, script); + // Local function to convert a dictionary to its JSON object's string representation + constexpr auto TableToJson = [](lua_State* state) { + if (!ghoul::lua::hasValue(state)) { + throw ghoul::lua::LuaError("TableToJson must receive a table object"); + } + ghoul::Dictionary dict = ghoul::lua::value(state); + std::string stringRepresentation = formatJson(dict); + ghoul::lua::push(state, std::move(stringRepresentation)); + return 1; + }; + lua_pushcfunction(result.state, TableToJson); + lua_setglobal(result.state, "TableToJson"); + // If there is an initial config helper file, load it into the state if (std::filesystem::is_regular_file(absPath(InitialConfigHelper))) { ghoul::lua::runScriptFile(result.state, absPath(InitialConfigHelper)); diff --git a/src/scene/assetmanager.cpp b/src/scene/assetmanager.cpp index 181f9ce1cb..9860688f89 100644 --- a/src/scene/assetmanager.cpp +++ b/src/scene/assetmanager.cpp @@ -144,6 +144,90 @@ void AssetManager::deinitialize() { _toBeDeleted.clear(); } +void AssetManager::runRemoveQueue() { + ZoneScoped; + for (const std::string& asset : _assetRemoveQueue) { + std::filesystem::path path = generateAssetPath(_assetRootDirectory, asset); + + const auto it = std::find_if( + _assets.cbegin(), + _assets.cend(), + [&path](const std::unique_ptr& a) { return a->path() == path; } + ); + if (it == _assets.cend()) { + LWARNING(std::format("Tried to remove unknown asset '{}'. Skipping", asset)); + continue; + } + + Asset* a = it->get(); + auto jt = std::find(_rootAssets.cbegin(), _rootAssets.cend(), a); + if (jt == _rootAssets.cend()) { + // Trying to remove an asset from the middle of the tree might have some + // unexpected behavior since if we were to remove an asset with children, we + // would have to unload those children as well. Also, we don't know if that + // Asset's parents are actually requiring the asset we are about to remove so + // we might break things horribly for people. + // We should figure out a way to fix this without reintroducing the + // require/request confusion, but until then, we'll prohibit removing non-root + // assets + + LWARNING("Tried to remove an asset that was not on the root level. Skipping"); + continue; + } + + _rootAssets.erase(jt); + // Even though we are removing a root asset, we might not be the only person that + // is interested in the asset, so we can only deinitialize it if we were, in fact, + // the only person, meaning that the asset never had any parents + if (!a->hasInitializedParent()) { + a->deinitialize(); + } + if (!a->hasLoadedParent()) { + a->unload(); + } + global::profile->removeAsset(asset); + } + _assetRemoveQueue.clear(); +} + +void AssetManager::runAddQueue() { + ZoneScoped; + for (const std::string& asset : _assetAddQueue) { + + const std::filesystem::path path = generateAssetPath(_assetRootDirectory, asset); + Asset* a = nullptr; + try { + a = retrieveAsset(path, ""); + } + catch (const ghoul::RuntimeError& e) { + LERRORC(e.component, e.message); + continue; + } + + const auto it = std::find(_rootAssets.cbegin(), _rootAssets.cend(), a); + if (it != _rootAssets.cend()) { + // Do nothing if the asset has already been requested on a root level. Even if + // another asset has already required this asset, calling `load`, + // `startSynchronization`, etc on it are still fine since all of those + // functions bail out early if they already have been called before + continue; + } + + a->load(nullptr); + if (a->isFailed()) { + // The loading might fail because of any number of reasons, most likely of + // them some Lua syntax error + continue; + } + _rootAssets.push_back(a); + a->startSynchronizations(); + + _toBeInitialized.push_back(a); + global::profile->addAsset(asset); + } + _assetAddQueue.clear(); +} + void AssetManager::update() { ZoneScoped; @@ -187,87 +271,10 @@ void AssetManager::update() { break; } - // Add all assets that have been queued for loading since the last `update` call - for (const std::string& asset : _assetAddQueue) { - ZoneScopedN("Adding queued assets"); - - const std::filesystem::path path = generateAssetPath(_assetRootDirectory, asset); - Asset* a = nullptr; - try { - a = retrieveAsset(path, ""); - } - catch (const ghoul::RuntimeError& e) { - LERRORC(e.component, e.message); - continue; - } - - const auto it = std::find(_rootAssets.cbegin(), _rootAssets.cend(), a); - if (it != _rootAssets.cend()) { - // Do nothing if the asset has already been requested on a root level. Even if - // another asset has already required this asset, calling `load`, - // `startSynchronization`, etc on it are still fine since all of those - // functions bail out early if they already have been called before - continue; - } - - a->load(nullptr); - if (a->isFailed()) { - // The loading might fail because of any number of reasons, most likely of - // them some Lua syntax error - continue; - } - _rootAssets.push_back(a); - a->startSynchronizations(); - - _toBeInitialized.push_back(a); - global::profile->addAsset(asset); - } - _assetAddQueue.clear(); - // Remove assets - for (const std::string& asset : _assetRemoveQueue) { - ZoneScopedN("Removing queued assets"); - std::filesystem::path path = generateAssetPath(_assetRootDirectory, asset); - - const auto it = std::find_if( - _assets.cbegin(), - _assets.cend(), - [&path](const std::unique_ptr& a) { return a->path() == path; } - ); - if (it == _assets.cend()) { - LWARNING(std::format("Tried to remove unknown asset '{}'. Skipping", asset)); - continue; - } - - Asset* a = it->get(); - auto jt = std::find(_rootAssets.cbegin(), _rootAssets.cend(), a); - if (jt == _rootAssets.cend()) { - // Trying to remove an asset from the middle of the tree might have some - // unexpected behavior since if we were to remove an asset with children, we - // would have to unload those children as well. Also, we don't know if that - // Asset's parents are actually requiring the asset we are about to remove so - // we might break things horribly for people. - // We should figure out a way to fix this without reintroducing the - // require/request confusion, but until then, we'll prohibit removing non-root - // assets - - LWARNING("Tried to remove an asset that was not on the root level. Skipping"); - continue; - } - - _rootAssets.erase(jt); - // Even though we are removing a root asset, we might not be the only person that - // is interested in the asset, so we can only deinitialize it if we were, in fact, - // the only person, meaning that the asset never had any parents - if (!a->hasInitializedParent()) { - a->deinitialize(); - } - if (!a->hasLoadedParent()) { - a->unload(); - } - global::profile->removeAsset(asset); - } - _assetRemoveQueue.clear(); + runRemoveQueue(); + // Add all assets that have been queued for loading since the last `update` call + runAddQueue(); // Change state based on synchronizations. If any of the unfinished synchronizations // has finished since the last call of this function, we should notify the assets and @@ -317,6 +324,12 @@ void AssetManager::remove(const std::string& path) { _assetRemoveQueue.push_back(path); } +void AssetManager::reload(const std::string& path) { + ghoul_precondition(!path.empty(), "Path must not be empty"); + _assetRemoveQueue.push_back(path); + _assetAddQueue.push_back(path); +} + std::vector AssetManager::allAssets() const { std::vector res; res.reserve(_assets.size()); @@ -1042,6 +1055,7 @@ scripting::LuaLibrary AssetManager::luaLibrary() { { codegen::lua::Add, codegen::lua::Remove, + codegen::lua::Reload, codegen::lua::RemoveAll, codegen::lua::IsLoaded, codegen::lua::AllAssets, diff --git a/src/scene/assetmanager_lua.inl b/src/scene/assetmanager_lua.inl index 95377f680b..58c2d96cdf 100644 --- a/src/scene/assetmanager_lua.inl +++ b/src/scene/assetmanager_lua.inl @@ -41,6 +41,15 @@ namespace { openspace::global::openSpaceEngine->assetManager().remove(assetName); } +/** + * Reloads the asset with the specified name. If the asset was previously loaded explicity + * it will be removed and then re-added. If the asset was not previously loaded, it will + * only be loaded instead. + */ +[[codegen::luawrap]] void reload(std::string assetName) { + openspace::global::openSpaceEngine->assetManager().reload(assetName); +} + /** * Removes all assets that are currently loaded */ diff --git a/src/scene/scene.cpp b/src/scene/scene.cpp index 36f60772ce..b2108d30a7 100644 --- a/src/scene/scene.cpp +++ b/src/scene/scene.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include