664 lines
19 KiB
C++
664 lines
19 KiB
C++
// url.cpp
|
|
//
|
|
// Copyright (C) 2002-present, the Celestia Development Team
|
|
// Original version written by Chris Teyssier (chris@tux.teyssier.org)
|
|
//
|
|
// This program is free software; you can redistribute it and/or
|
|
// modify it under the terms of the GNU General Public License
|
|
// as published by the Free Software Foundation; either version 2
|
|
// of the License, or (at your option) any later version.
|
|
|
|
#include <iostream>
|
|
#include <fmt/ostream.h>
|
|
#include <fmt/printf.h>
|
|
#include <celcompat/charconv.h>
|
|
#include <celutil/bigfix.h>
|
|
#include <celutil/gettext.h>
|
|
#include <celutil/logger.h>
|
|
#include <celutil/stringutils.h>
|
|
#include "celestiacore.h"
|
|
#include "url.h"
|
|
|
|
using celestia::util::GetLogger;
|
|
|
|
namespace
|
|
{
|
|
|
|
std::string getBodyName(Universe* universe, Body* body)
|
|
{
|
|
std::string name = body->getName();
|
|
PlanetarySystem* parentSystem = body->getSystem();
|
|
const Body* parentBody = nullptr;
|
|
|
|
if (parentSystem != nullptr)
|
|
parentBody = parentSystem->getPrimaryBody();
|
|
else
|
|
assert(0);
|
|
// TODO: Figure out why the line below was added.
|
|
//parentBody = body->getOrbitBarycenter();
|
|
|
|
while (parentBody != nullptr)
|
|
{
|
|
name = parentBody->getName() + ":" + name;
|
|
parentSystem = parentBody->getSystem();
|
|
if (parentSystem == nullptr)
|
|
break;
|
|
parentBody = parentSystem->getPrimaryBody();
|
|
}
|
|
|
|
auto *star = body->getSystem()->getStar();
|
|
if (star != nullptr)
|
|
name = universe->getStarCatalog()->getStarName(*star) + ":" + name;
|
|
|
|
return name;
|
|
}
|
|
|
|
// We use std::string here because we pass result to C API (gettext())
|
|
std::string getBodyShortName(const std::string &body)
|
|
{
|
|
if (!body.empty())
|
|
{
|
|
auto pos = body.rfind(":");
|
|
if (pos != std::string::npos)
|
|
return D_(body.substr(pos + 1).c_str());
|
|
}
|
|
return D_(body.c_str());
|
|
}
|
|
|
|
std::string_view
|
|
getCoordSysName(ObserverFrame::CoordinateSystem mode)
|
|
{
|
|
switch (mode)
|
|
{
|
|
case ObserverFrame::Universal:
|
|
return "Freeflight";
|
|
case ObserverFrame::Ecliptical:
|
|
return "Follow";
|
|
case ObserverFrame::BodyFixed:
|
|
return "SyncOrbit";
|
|
case ObserverFrame::Chase:
|
|
return "Chase";
|
|
case ObserverFrame::PhaseLock:
|
|
return "PhaseLock";
|
|
case ObserverFrame::Equatorial:
|
|
return "Unknown";
|
|
case ObserverFrame::ObserverLocal:
|
|
return "Unknown";
|
|
default:
|
|
return "Unknown";
|
|
}
|
|
}
|
|
|
|
} // anon namespace
|
|
|
|
Url::Url(CelestiaCore *core) :
|
|
m_appCore(core)
|
|
{
|
|
}
|
|
|
|
Url::Url(const CelestiaState &appState, int version, Url::TimeSource timeSource) :
|
|
m_appCore(appState.m_appCore),
|
|
m_state(appState),
|
|
m_version(version),
|
|
m_timeSource(timeSource)
|
|
{
|
|
assert(version == 3);
|
|
std::ostringstream u;
|
|
|
|
switch (m_state.m_coordSys)
|
|
{
|
|
case ObserverFrame::Universal:
|
|
m_nBodies = 0;
|
|
break;
|
|
case ObserverFrame::PhaseLock:
|
|
m_nBodies = 2;
|
|
break;
|
|
default:
|
|
m_nBodies = 1;
|
|
}
|
|
|
|
u << Url::proto() << getCoordSysName(m_state.m_coordSys);
|
|
|
|
if (appState.m_coordSys != ObserverFrame::Universal)
|
|
{
|
|
u << '/' << m_state.m_refBodyName;
|
|
if (appState.m_coordSys == ObserverFrame::PhaseLock)
|
|
u << '/' << m_state.m_targetBodyName;
|
|
}
|
|
|
|
m_date = astro::Date(m_state.m_tdb);
|
|
u << '/' << m_date.toCStr(astro::Date::ISO8601);
|
|
|
|
// observer position
|
|
u << fmt::format("?x={}&y={}&z={}",
|
|
m_state.m_observerPosition.x.toString(),
|
|
m_state.m_observerPosition.y.toString(),
|
|
m_state.m_observerPosition.z.toString());
|
|
|
|
// observer orientation
|
|
u << fmt::format("&ow={}&ox={}&oy={}&oz={}",
|
|
m_state.m_observerOrientation.w(),
|
|
m_state.m_observerOrientation.x(),
|
|
m_state.m_observerOrientation.y(),
|
|
m_state.m_observerOrientation.z());
|
|
|
|
if (!m_state.m_trackedBodyName.empty())
|
|
u << "&track=" << m_state.m_trackedBodyName;
|
|
if (!m_state.m_selectedBodyName.empty())
|
|
u << "&select=" << m_state.m_selectedBodyName;
|
|
|
|
u << fmt::format("&fov={}&ts={}<d={}&p={}",
|
|
m_state.m_fieldOfView,
|
|
m_state.m_timeScale,
|
|
m_state.m_lightTimeDelay ? 1 : 0,
|
|
m_state.m_pauseState ? 1 : 0);
|
|
|
|
|
|
// ShowTintedIllumination == 0x04000000, the last 1.6 parameter
|
|
// we keep only old parameters and clear new ones
|
|
int rf = static_cast<int>(m_state.m_renderFlags & 0x04ffffffull);
|
|
// 1.6 uses ShowPlanets to control display of all types of solar
|
|
// system objects. So set it if any one is displayed.
|
|
if ((m_state.m_renderFlags & Renderer::ShowSolarSystemObjects) != 0)
|
|
rf |= static_cast<int>(Renderer::ShowPlanets);
|
|
// But we need to store actual value of the bit which controls
|
|
// planets display. 27th bit is unused in 1.6.
|
|
if ((m_state.m_renderFlags & Renderer::ShowPlanets) != 0)
|
|
rf |= (1<<27);
|
|
int nrf = static_cast<int>(m_state.m_renderFlags >> 27);
|
|
|
|
u << fmt::format("&rf={}&nrf={}&lm={}",
|
|
rf, nrf, m_state.m_labelMode);
|
|
|
|
// Append the url settings: time source and version
|
|
u << "&tsrc=" << (int) m_timeSource;
|
|
u << "&ver=" << m_version;
|
|
|
|
m_url = u.str();
|
|
m_valid = true;
|
|
evalName();
|
|
}
|
|
|
|
bool
|
|
Url::goTo()
|
|
{
|
|
if (!m_valid)
|
|
return false;
|
|
|
|
assert(m_appCore != nullptr);
|
|
auto *sim = m_appCore->getSimulation();
|
|
auto *renderer = m_appCore->getRenderer();
|
|
|
|
sim->update(0.0);
|
|
sim->setFrame(m_ref.getCoordinateSystem(), m_ref.getRefObject(), m_ref.getTargetObject());
|
|
sim->getActiveObserver()->setFOV(celmath::degToRad(m_state.m_fieldOfView));
|
|
m_appCore->setZoomFromFOV();
|
|
sim->setTimeScale(m_state.m_timeScale);
|
|
sim->setPauseState(m_state.m_pauseState);
|
|
m_appCore->setLightDelayActive(m_state.m_lightTimeDelay);
|
|
if (!m_state.m_selectedBodyName.empty())
|
|
{
|
|
auto body = m_state.m_selectedBodyName;
|
|
std::replace(body.begin(), body.end(), ':', '/');
|
|
auto sel = sim->findObjectFromPath(body);
|
|
sim->setSelection(sel);
|
|
}
|
|
else
|
|
{
|
|
sim->setSelection(Selection());
|
|
}
|
|
|
|
if (!m_state.m_trackedBodyName.empty())
|
|
{
|
|
auto body = m_state.m_trackedBodyName;
|
|
std::replace(body.begin(), body.end(), ':', '/');
|
|
auto sel = sim->findObjectFromPath(body);
|
|
sim->setTrackedObject(sel);
|
|
}
|
|
else
|
|
{
|
|
if (!sim->getTrackedObject().empty())
|
|
sim->setTrackedObject(Selection());
|
|
}
|
|
|
|
renderer->setRenderFlags(m_state.m_renderFlags);
|
|
renderer->setLabelMode(m_state.m_labelMode);
|
|
|
|
switch (m_timeSource)
|
|
{
|
|
case UseUrlTime:
|
|
sim->setTime(m_state.m_tdb);
|
|
break;
|
|
case UseSimulationTime:
|
|
// Leave the current simulation time unmodified
|
|
break;
|
|
case UseSystemTime:
|
|
sim->setTime(astro::UTCtoTDB(astro::Date::systemDate()));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Position and orientation stored in frame coordinates; convert them
|
|
// to universal and set the observer position.
|
|
double tdb = sim->getTime();
|
|
auto coord = sim->getObserver().getFrame()->convertToUniversal(m_state.m_observerPosition, m_state.m_tdb);
|
|
Eigen::Quaterniond q = m_state.m_observerOrientation.cast<double>();
|
|
q = sim->getObserver().getFrame()->convertToUniversal(q, m_state.m_tdb);
|
|
sim->setObserverPosition(coord);
|
|
sim->setObserverOrientation(q.cast<float>());
|
|
|
|
return true;
|
|
}
|
|
|
|
std::string
|
|
Url::getAsString() const
|
|
{
|
|
return m_url;
|
|
}
|
|
|
|
std::string
|
|
Url::getEncodedObjectName(const Selection& selection, const CelestiaCore* appCore)
|
|
{
|
|
auto *universe = appCore->getSimulation()->getUniverse();
|
|
std::string name;
|
|
Body* parentBody = nullptr;
|
|
|
|
switch (selection.getType())
|
|
{
|
|
case Selection::Type_Body:
|
|
name = getBodyName(universe, selection.body());
|
|
break;
|
|
|
|
case Selection::Type_Star:
|
|
name = universe->getStarCatalog()->getStarName(*selection.star());
|
|
break;
|
|
|
|
case Selection::Type_DeepSky:
|
|
name = universe->getDSOCatalog()->getDSOName(selection.deepsky());
|
|
break;
|
|
|
|
case Selection::Type_Location:
|
|
name = selection.location()->getName();
|
|
parentBody = selection.location()->getParentBody();
|
|
if (parentBody != nullptr)
|
|
name = getBodyName(universe, parentBody) + ":" + name;
|
|
break;
|
|
|
|
default:
|
|
return {};
|
|
}
|
|
|
|
return Url::encodeString(name);
|
|
}
|
|
|
|
std::string
|
|
Url::decodeString(std::string_view str)
|
|
{
|
|
std::string_view::size_type a = 0, b = 0;
|
|
std::string out;
|
|
|
|
b = str.find('%');
|
|
while (b != std::string_view::npos && a < str.length())
|
|
{
|
|
auto s = str.substr(a, b - a);
|
|
out.append(s.data(), s.length());
|
|
std::string_view c_code = str.substr(b + 1, 2);
|
|
uint8_t c;
|
|
if (to_number(c_code, c, 16))
|
|
{
|
|
out += static_cast<std::string::value_type>(c);
|
|
}
|
|
else
|
|
{
|
|
GetLogger()->warn(_("Incorrect hex value \"{}\"\n"), c_code);
|
|
out += '%';
|
|
out.append(c_code.data(), c_code.length());
|
|
}
|
|
a = b + 1 + c_code.length();
|
|
b = str.find('%', a);
|
|
}
|
|
if (a < str.length())
|
|
{
|
|
auto s = str.substr(a);
|
|
out.append(s.data(), s.length());
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
std::string
|
|
Url::encodeString(std::string_view str)
|
|
{
|
|
std::ostringstream enc;
|
|
|
|
for (const auto _ch : str)
|
|
{
|
|
auto ch = static_cast<unsigned char>(_ch);
|
|
bool encode = false;
|
|
if (ch <= 32 || ch >= 128)
|
|
{
|
|
encode = true;
|
|
}
|
|
else
|
|
{
|
|
switch (ch)
|
|
{
|
|
case '%':
|
|
case '?':
|
|
case '"':
|
|
case '#':
|
|
case '+':
|
|
case ',':
|
|
case '=':
|
|
case '@':
|
|
case '[':
|
|
case ']':
|
|
encode = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (encode)
|
|
enc << fmt::sprintf("%%%02x", ch);
|
|
else
|
|
enc << _ch;
|
|
}
|
|
|
|
return enc.str();
|
|
}
|
|
|
|
struct Mode
|
|
{
|
|
std::string_view modeStr;
|
|
ObserverFrame::CoordinateSystem mode;
|
|
int nBodies;
|
|
};
|
|
|
|
static Mode modes[] =
|
|
{
|
|
{ "Freeflight", ObserverFrame::Universal, 0 },
|
|
{ "Follow", ObserverFrame::Ecliptical, 1 },
|
|
{ "SyncOrbit", ObserverFrame::BodyFixed, 1 },
|
|
{ "Chase", ObserverFrame::Chase, 1 },
|
|
{ "PhaseLock", ObserverFrame::PhaseLock, 2 },
|
|
};
|
|
|
|
auto ParseURLParams(std::string_view paramsStr)
|
|
-> std::map<std::string_view, std::string>;
|
|
|
|
bool Url::parse(std::string_view urlStr)
|
|
{
|
|
constexpr auto npos = std::string_view::npos;
|
|
|
|
// proper URL string must start with protocol (cel://)
|
|
if (urlStr.compare(0, Url::proto().length(), Url::proto()) != 0)
|
|
{
|
|
GetLogger()->error(_("URL must start with \"{}\"!\n"), Url::proto());
|
|
return false;
|
|
}
|
|
|
|
// extract @path and @params from the URL
|
|
auto pos = urlStr.find('?');
|
|
auto pathStr = urlStr.substr(Url::proto().length(), pos - Url::proto().length());
|
|
while (pathStr.back() == '/')
|
|
pathStr.remove_suffix(1);
|
|
std::string_view paramsStr;
|
|
if (pos != npos)
|
|
paramsStr = urlStr.substr(pos + 1);
|
|
|
|
pos = pathStr.find('/');
|
|
if (pos == npos)
|
|
{
|
|
GetLogger()->error(_("URL must have at least mode and time!\n"));
|
|
return false;
|
|
}
|
|
auto modeStr = pathStr.substr(0, pos);
|
|
|
|
int nBodies = -1;
|
|
CelestiaState state;
|
|
auto lambda = [modeStr](Mode &m) { return compareIgnoringCase(modeStr, m.modeStr) == 0; };
|
|
auto it = std::find_if(std::begin(modes), std::end(modes), lambda);
|
|
if (it == std::end(modes))
|
|
{
|
|
GetLogger()->error(_("Unsupported URL mode \"{}\"!\n"), modeStr);
|
|
return false;
|
|
}
|
|
state.m_coordSys = it->mode;
|
|
nBodies = it->nBodies;
|
|
|
|
auto timepos = nBodies == 0 ? pos : pathStr.rfind('/');
|
|
auto timeStr = pathStr.substr(timepos + 1);
|
|
|
|
Selection bodies[2];
|
|
if (nBodies > 0)
|
|
{
|
|
auto bodiesStr = pathStr.substr(pos + 1, timepos - pos - 1);
|
|
pos = bodiesStr.find('/');
|
|
if (nBodies == 1)
|
|
{
|
|
if (pos != npos)
|
|
{
|
|
GetLogger()->error(_("URL must contain only one body\n"));
|
|
return false;
|
|
}
|
|
auto body = Url::decodeString(bodiesStr);
|
|
std::replace(body.begin(), body.end(), ':', '/');
|
|
bodies[0] = m_appCore->getSimulation()->findObjectFromPath(body);
|
|
state.m_refBodyName = std::move(body);
|
|
}
|
|
else if (nBodies == 2)
|
|
{
|
|
if (pos == npos || bodiesStr.find('/', pos + 1) != npos)
|
|
{
|
|
GetLogger()->error(_("URL must contain 2 bodies\n"));
|
|
return false;
|
|
}
|
|
auto body = Url::decodeString(bodiesStr.substr(0, pos));
|
|
std::replace(body.begin(), body.end(), ':', '/');
|
|
bodies[0] = m_appCore->getSimulation()->findObjectFromPath(body);
|
|
state.m_refBodyName = std::move(body);
|
|
|
|
body = Url::decodeString(bodiesStr.substr(pos + 1));
|
|
std::replace(body.begin(), body.end(), ':', '/');
|
|
bodies[1] = m_appCore->getSimulation()->findObjectFromPath(body);
|
|
state.m_targetBodyName = std::move(body);
|
|
}
|
|
}
|
|
|
|
ObserverFrame ref;
|
|
switch (nBodies)
|
|
{
|
|
case 0:
|
|
ref = ObserverFrame();
|
|
break;
|
|
case 1:
|
|
ref = ObserverFrame(state.m_coordSys, bodies[0]);
|
|
break;
|
|
case 2:
|
|
ref = ObserverFrame(state.m_coordSys, bodies[0], bodies[1]);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
auto params = ParseURLParams(paramsStr);
|
|
|
|
// Version labelling of cel URLs was only added in Celestia 1.5, cel URL
|
|
// version 2. Assume any URL without a version is version 1.
|
|
int version = 1;
|
|
if (params.count("ver") != 0)
|
|
{
|
|
auto &p = params["ver"];
|
|
if (!to_number(p, version))
|
|
{
|
|
GetLogger()->error(_("Invalid URL version \"{}\"!\n"), p);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (version != 3 && version != 4)
|
|
{
|
|
GetLogger()->error(_("Unsupported URL version: {}\n"), version);
|
|
return false;
|
|
}
|
|
|
|
m_ref = ref;
|
|
m_state = state;
|
|
m_nBodies = nBodies;
|
|
if (version == 4 && !initVersion4(params, timeStr))
|
|
return false;
|
|
else if (!initVersion3(params, timeStr))
|
|
return false;
|
|
m_valid = true;
|
|
evalName();
|
|
|
|
return true;
|
|
}
|
|
|
|
auto ParseURLParams(std::string_view paramsStr)
|
|
-> std::map<std::string_view, std::string>
|
|
{
|
|
std::map<std::string_view, std::string> params;
|
|
if (paramsStr.empty())
|
|
return params;
|
|
|
|
constexpr auto npos = std::string_view::npos;
|
|
for (auto iter = paramsStr;;)
|
|
{
|
|
auto pos = iter.find('&');
|
|
auto kv = iter.substr(0, pos);
|
|
auto vpos = kv.find('=');
|
|
if (vpos == npos)
|
|
{
|
|
GetLogger()->error(_("URL parameter must look like key=value\n"));
|
|
break;
|
|
}
|
|
params[kv.substr(0, vpos)] = Url::decodeString(kv.substr(vpos + 1));
|
|
if (pos == npos)
|
|
break;
|
|
iter.remove_prefix(pos + 1);
|
|
}
|
|
|
|
return params;
|
|
}
|
|
|
|
bool Url::initVersion3(std::map<std::string_view, std::string> ¶ms, std::string_view timeStr)
|
|
{
|
|
m_version = 3;
|
|
|
|
if (!astro::parseDate(std::string(timeStr), m_date))
|
|
return false;
|
|
m_state.m_tdb = (double) m_date;
|
|
|
|
if (params.count("x") == 0 || params.count("y") == 0 || params.count("z") == 0)
|
|
return false;
|
|
m_state.m_observerPosition = UniversalCoord(BigFix(params["x"]),
|
|
BigFix(params["y"]),
|
|
BigFix(params["z"]));
|
|
|
|
float ow, ox, oy, oz;
|
|
if (to_number(params["ow"], ow) &&
|
|
to_number(params["ox"], ox) &&
|
|
to_number(params["oy"], oy) &&
|
|
to_number(params["oz"], oz))
|
|
m_state.m_observerOrientation = Eigen::Quaternionf(ow, ox, oy, oz);
|
|
else
|
|
return false;
|
|
|
|
if (params.count("select") != 0)
|
|
m_state.m_selectedBodyName = params["select"];
|
|
if (params.count("track") != 0)
|
|
m_state.m_trackedBodyName = params["track"];
|
|
if (params.count("ltd") != 0)
|
|
m_state.m_lightTimeDelay = params["ltd"] != "0";
|
|
|
|
if (params.count("fov") != 0 && !to_number(params["fov"], m_state.m_fieldOfView))
|
|
return false;
|
|
if (params.count("ts") != 0 && !to_number(params["ts"], m_state.m_timeScale))
|
|
return false;
|
|
|
|
if (params.count("p") != 0)
|
|
m_state.m_pauseState = params["p"] != "0";
|
|
|
|
// Render settings
|
|
bool hasNewRenderFlags = false;
|
|
uint64_t newFlags = 0ull, oldFlags = 0ull;
|
|
if (params.count("nrf") != 0)
|
|
{
|
|
hasNewRenderFlags = true;
|
|
int nrf;
|
|
if (!to_number(params["nrf"], nrf))
|
|
return false;
|
|
newFlags = static_cast<uint64_t>(nrf) << 27;
|
|
}
|
|
if (params.count("rf") != 0)
|
|
{
|
|
// old renderer flags are int
|
|
int rf;
|
|
if (!to_number(params["rf"], rf))
|
|
return false;
|
|
// older celestia versions don't know about the new renderer flags
|
|
if (hasNewRenderFlags)
|
|
{
|
|
|
|
oldFlags = static_cast<uint64_t>(rf & 0x04ffffff);
|
|
// get actual Renderer::ShowPlanets value in 27th bit
|
|
// clear ShowPlanets if 27th bit is unset
|
|
if ((rf & (1<<27)) == 0)
|
|
oldFlags &= ~Renderer::ShowPlanets;
|
|
}
|
|
else
|
|
{
|
|
oldFlags = static_cast<uint64_t>(rf);
|
|
// new options enabled by default in 1.7
|
|
oldFlags |= Renderer::ShowPlanetRings | Renderer::ShowFadingOrbits;
|
|
// old ShowPlanets == new ShowSolarSystemObjects
|
|
if ((oldFlags & Renderer::ShowPlanets) != 0)
|
|
oldFlags |= Renderer::ShowSolarSystemObjects;
|
|
}
|
|
m_state.m_renderFlags = newFlags | oldFlags;
|
|
}
|
|
if (params.count("lm") != 0 && !to_number(params["lm"], m_state.m_labelMode))
|
|
return false;
|
|
|
|
int tsrc = 0;
|
|
if (params.count("tsrc") != 0 && !to_number(params["tsrc"], tsrc))
|
|
return false;
|
|
if (tsrc >= 0 && tsrc < TimeSourceCount)
|
|
m_timeSource = static_cast<TimeSource>(tsrc);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Url::initVersion4(std::map<std::string_view, std::string> ¶ms, std::string_view timeStr)
|
|
{
|
|
if (params.count("rf") != 0)
|
|
{
|
|
uint64_t rf;
|
|
if (!to_number(params["rf"], rf))
|
|
return false;
|
|
int nrf = rf >> 27;
|
|
int _rf = rf & 0x07ffffff;
|
|
if ((rf & Renderer::ShowPlanets) != 0)
|
|
_rf |= (1 << 27); // Set the 27th bits to ShowPlanets
|
|
params["nrf"] = std::to_string(nrf);
|
|
params["rf"] = std::to_string(_rf);
|
|
}
|
|
return initVersion3(params, timeStr);
|
|
}
|
|
|
|
void Url::evalName()
|
|
{
|
|
std::string name;
|
|
if (!m_state.m_refBodyName.empty())
|
|
name += fmt::sprintf(" %s", getBodyShortName(m_state.m_refBodyName));
|
|
if (!m_state.m_targetBodyName.empty())
|
|
name += fmt::sprintf(" %s", getBodyShortName(m_state.m_targetBodyName));
|
|
if (!m_state.m_trackedBodyName.empty())
|
|
name += fmt::sprintf(" -> %s", getBodyShortName(m_state.m_trackedBodyName));
|
|
if (!m_state.m_selectedBodyName.empty())
|
|
name += fmt::sprintf(" [%s]", getBodyShortName(m_state.m_selectedBodyName));
|
|
m_name = std::move(name);
|
|
}
|