Parameters: expand logarithmic scale patch coverage
The control logic for the various combinations of linear and logarithmic scales is nasty, and the added path coverage indeed unveiled several bugs. One of these issues, an unjustified limitation on the input value type of `asLogarithm`, convinced me to refactor the conversion functions into free functions, since they effectively ''always span the relation of two scales'' -- writing them as member function introduces an unnecessary asymetry.
This commit is contained in:
parent
206f9635c9
commit
00b0aa0c5b
4 changed files with 181 additions and 101 deletions
|
|
@ -90,16 +90,6 @@ namespace par {
|
|||
VAL join (VAL const&, SRC const&, Scale<SRC> const&) const;
|
||||
VAL join (VAL const&, VAL const&) const;
|
||||
|
||||
/* ===== conversion helpers ===== */
|
||||
|
||||
template<typename TAR>
|
||||
auto asFactor (VAL const&) const;
|
||||
|
||||
template<typename TAR>
|
||||
auto baseScale (Metric targetMetric) const;
|
||||
|
||||
template<typename TAR>
|
||||
auto asLogarithm (Metric targetMetric, VAL const&) const;
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -208,6 +198,80 @@ namespace par {
|
|||
}
|
||||
|
||||
|
||||
namespace { // logarithmic scale conversions...
|
||||
|
||||
/** transform a logarithmic value into an exponential factor */
|
||||
template<typename TAR, typename VAL>
|
||||
inline auto
|
||||
expFactor (Metric metric, VAL const& value)
|
||||
{
|
||||
using WorkType = CommonComputeType<VAL,TAR>;
|
||||
switch (metric)
|
||||
{
|
||||
case NAT: return std::exp (WorkType(value));
|
||||
case BIN: return std::exp2 (WorkType(value));
|
||||
case DEC: return std::pow (WorkType(10), WorkType(value/10.0));
|
||||
default:
|
||||
throw err::Logic{_Fmt{"Attempt to compute exponential "
|
||||
"from a non-logarithmic value with metric %d"}
|
||||
% metric};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/** transform this value into a suitable logarithm for the target scale */
|
||||
template<typename TAR, typename VAL>
|
||||
inline auto
|
||||
asLogarithm (Metric targetMetric, VAL const& value)
|
||||
{
|
||||
if (isNeg (value))
|
||||
throw err::Invalid{_Fmt{"Attempt to take logarithm from %f < 0"}
|
||||
% value};
|
||||
using WorkType = CommonComputeType<VAL,TAR>;
|
||||
switch (targetMetric)
|
||||
{
|
||||
case NAT: return std::log (WorkType(value));
|
||||
case BIN: return std::log2 (WorkType(value));
|
||||
case DEC: return std::log10 (WorkType(value)) * 10;
|
||||
default:
|
||||
throw err::Logic{_Fmt{"Attempt to compute logarithm "
|
||||
"for a non-logarithmic metric %d"}
|
||||
% targetMetric};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/** provide the adaptation factor from this logarithmic scale
|
||||
* to the given other logarithmic scale and target type \a VAL
|
||||
* @remarks
|
||||
* - use the logarithm conversion formula logb(x) ≡ logb(k)·logk(x)
|
||||
* - if target is in decibel, have to apply factor 10; asLogarithm() does that already
|
||||
* - if we are in decibel, log10 = dB /10; fold that factor in the logarithm of the base,
|
||||
* by using log(b)·x ≡ log(b^x) and rewrite that as b^x = exp(ln(b)·x)
|
||||
*/
|
||||
|
||||
template<typename TAR, typename VAL>
|
||||
inline auto
|
||||
baseScale (Metric targetMetric, Metric srcMetric)
|
||||
{
|
||||
using WorkType = CommonComputeType<VAL,TAR>;
|
||||
if (srcMetric == targetMetric)
|
||||
return WorkType(1);
|
||||
|
||||
auto srcBase = [&]{switch (srcMetric)
|
||||
{
|
||||
case BIN: return WorkType(2);
|
||||
case NAT: return std::exp (WorkType(1));
|
||||
case DEC: return std::exp (std::log (WorkType(10)) / 10);
|
||||
default:
|
||||
throw err::Logic{"Attempt to treat non-logarithmic scales as logarithms"};
|
||||
break;
|
||||
}};
|
||||
return asLogarithm<WorkType> (targetMetric, srcBase());
|
||||
}
|
||||
}//(End) log helpers
|
||||
|
||||
|
||||
/**
|
||||
* Join and combine an additional feed value with an anchor value.
|
||||
* @note actual joining operation is picked based on both scales and types involved.
|
||||
|
|
@ -226,7 +290,7 @@ namespace par {
|
|||
if (LIN == metric)
|
||||
{
|
||||
if (feedScale.isLogarithmic())
|
||||
assignConverted (res, value * feedScale.template asFactor<VAL> (srcFeed));
|
||||
assignConverted (res, value * expFactor<VAL> (feedScale.metric, srcFeed));
|
||||
else
|
||||
if (feedScale.isFactor())
|
||||
assignConverted (res, JoinT(value) * JoinT(srcFeed));
|
||||
|
|
@ -237,10 +301,10 @@ namespace par {
|
|||
if (this->isLogarithmic())
|
||||
{
|
||||
if (feedScale.isLogarithmic())
|
||||
assignConverted (res, value + feedScale.template baseScale<VAL>(metric) * srcFeed);
|
||||
assignConverted (res, value + baseScale<VAL,SRC> (metric,feedScale.metric) * srcFeed);
|
||||
else
|
||||
if (feedScale.isFactor())
|
||||
assignConverted (res, value + feedScale.template asLogarithm<VAL> (metric, srcFeed));
|
||||
assignConverted (res, value + asLogarithm<VAL> (metric, srcFeed));
|
||||
else
|
||||
throw err::Invalid {"adding a non-logarithmic data feed on top of a logarithmic base value is pointless."};
|
||||
}
|
||||
|
|
@ -264,81 +328,5 @@ namespace par {
|
|||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/** transform a logarithmic value into an exponential factor */
|
||||
template<typename VAL>
|
||||
template<typename TAR>
|
||||
inline auto
|
||||
Scale<VAL>::asFactor (VAL const& value) const
|
||||
{
|
||||
using WorkType = CommonComputeType<VAL,TAR>;
|
||||
switch (metric)
|
||||
{
|
||||
case NAT: return std::exp (WorkType(value));
|
||||
case BIN: return std::exp2 (WorkType(value));
|
||||
case DEC: return std::pow (WorkType(10), WorkType(value/10.0));
|
||||
default:
|
||||
throw err::Logic{_Fmt{"Attempt to compute exponential "
|
||||
"from a non-logarithmic value with metric %d"}
|
||||
% metric};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/** transform this value into a suitable logarithm for the target scale */
|
||||
template<typename VAL>
|
||||
template<typename TAR>
|
||||
inline auto
|
||||
Scale<VAL>::asLogarithm (Metric targetMetric, VAL const& value) const
|
||||
{
|
||||
if (isNeg (value))
|
||||
throw err::Invalid{_Fmt{"Attempt to take logarithm from %f < 0"}
|
||||
% value};
|
||||
using WorkType = CommonComputeType<VAL,TAR>;
|
||||
switch (targetMetric)
|
||||
{
|
||||
case NAT: return std::log (WorkType(value));
|
||||
case BIN: return std::logb (WorkType(value));
|
||||
case DEC: return std::log10 (WorkType(value)) * 10;
|
||||
default:
|
||||
throw err::Logic{_Fmt{"Attempt to compute logarithm "
|
||||
"for a non-logarithmic metric %d"}
|
||||
% targetMetric};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/** provide the adaptation factor from this logarithmic scale
|
||||
* to the given other logarithmic scale and target type \a VAL
|
||||
* @remarks
|
||||
* - use the logarithm conversion formula logb(x) ≡ logb(k)·logk(x)
|
||||
* - if target is in decibel, have to apply factor 10; asLogarithm() does that already
|
||||
* - if we are in decibel, log10 = dB /10; fold that factor in the logarithm of the base,
|
||||
* by using log(b)·x ≡ log(b^x) and rewrite that as b^x = exp(ln(b)·x)
|
||||
*/
|
||||
|
||||
template<typename VAL>
|
||||
template<typename TAR>
|
||||
inline auto
|
||||
Scale<VAL>::baseScale (Metric targetMetric) const
|
||||
{
|
||||
using WorkType = CommonComputeType<VAL,TAR>;
|
||||
if (metric == targetMetric)
|
||||
return WorkType(1);
|
||||
|
||||
auto myBase = [&]{switch (metric)
|
||||
{
|
||||
case NAT: return std::exp(WorkType(1));
|
||||
case BIN: return WorkType(2);
|
||||
case DEC: return std::exp(std::log(WorkType(10)) / 10);
|
||||
default:
|
||||
throw err::Logic{"Attempt to treat non-logarithmic scales as logarithms"};
|
||||
break;
|
||||
}};
|
||||
return asLogarithm<WorkType> (targetMetric, myBase());
|
||||
}
|
||||
|
||||
|
||||
}} // namespace lib::par
|
||||
#endif /*LIB_PAR_SCALE_H*/
|
||||
|
|
|
|||
|
|
@ -158,6 +158,10 @@ _Pragma("GCC diagnostic pop")
|
|||
/**
|
||||
* the best-precision floating point type to carry out
|
||||
* a numeric operation to combine two values
|
||||
* @note the result type is always some floating point,
|
||||
* since the goal is to use the exponential / logarithm family.
|
||||
* @todo 9/26 we _could_ go from integral to long double for 64bit,
|
||||
* yet it is not clear if we ever need that tiny bit of extra precision...
|
||||
*/
|
||||
template<number V, number W>
|
||||
using CommonComputeType = std::conditional_t<integral<V> and integral<W>, double
|
||||
|
|
|
|||
|
|
@ -202,23 +202,46 @@ namespace test{
|
|||
//
|
||||
Scale<float> fscale = {.metric = NAT};
|
||||
Scale<float> feed_dB = {.metric = DEC};
|
||||
CHECK (roughEQ (fscale.join (0, float(20), feed_dB) , log(100.0f) )); // dB ⟼ natural logarithm; note 10dB ≙ factor 10 * lg(10) value divided / 10
|
||||
|
||||
CHECK (roughEQ (fscale.join (0, float(20), feed_dB) , log(100.0f) )); // dB ⟼ natural logarithm; note 20dB ≙ factor 10 * lg(100); input 20 divided by 10
|
||||
fscale.metric = BIN;
|
||||
CHECK (roughEQ (fscale.join (0, float(20), feed_dB) , log2(100.0f))); // dB ⟼ binary logarithm; (factor 1/10 on input is folded into the log factor)
|
||||
fscale.metric = DEC;
|
||||
CHECK (roughEQ (fscale.join (0, float(20), feed_dB) , 20.0f )); // dB ⟼ dB just passed-through and then added to value ≔ 0.0
|
||||
|
||||
fscale.metric = DEC;
|
||||
feedScale.metric = BIN;
|
||||
CHECK (roughEQ (fscale.join (0, int64_t(-3), feedScale), log10(1.0f/8) *10)); // binary log (given as int value) ⟼ dB
|
||||
feedScale.metric = NAT;
|
||||
CHECK (roughEQ (fscale.join (0, int64_t(-3), feedScale), log10(exp(-3))*10)); // natural log (input as int) ⟼ dB
|
||||
feedScale.metric = DEC;
|
||||
CHECK (roughEQ (fscale.join (0, int64_t(-3), feedScale), -3 )); // dB ⟼ dB just passed-through
|
||||
|
||||
fscale.metric = BIN;
|
||||
feedScale.metric = BIN;
|
||||
CHECK (roughEQ (fscale.join (0, int64_t(-3), feedScale), -3 )); // lb ⟼ lb just passed-through
|
||||
feedScale.metric = NAT;
|
||||
CHECK (roughEQ (fscale.join (0, int64_t(-3), feedScale), log2 (exp(-3)) )); // natural log ⟼ binary log
|
||||
feedScale.metric = DEC;
|
||||
CHECK (roughEQ (fscale.join (0, int64_t(-30),feedScale), log2 (1.0/1000) )); // conversion factor: dB/10 ⟼ binary log
|
||||
|
||||
fscale.metric = LIN;
|
||||
feedScale.metric = BIN;
|
||||
CHECK (roughEQ (fscale.join (1, int64_t(-3), feedScale), 1.0f/8 )); // binary log applied as factor to a linear scale
|
||||
feedScale.metric = NAT;
|
||||
CHECK (roughEQ (fscale.join (1, int64_t(-1), feedScale), 1.0f/exp(1))); // natural log applied as factor 1/e
|
||||
feedScale.metric = DEC;
|
||||
CHECK (roughEQ (fscale.join (1, int64_t(-30),feedScale), 1.0f/1000 )); // decibels applied as factor : -30 dB ≙ 10^-3
|
||||
|
||||
fscale.metric = BIN;
|
||||
feedScale.metric = LIN;
|
||||
feedScale.neutral = 1;
|
||||
CHECK (feedScale.isFactor());
|
||||
CHECK (fscale.isLogarithmic());
|
||||
CHECK (1 + 3 == fscale.join (1, int64_t(8), feedScale)); // feed factor 8 ⟼ to binary logarithmic fscale (8 ≡ 2^3) and added there
|
||||
CHECK ( fscale.join (1, int64_t(8), feedScale) == 1 + 3); // feed factor 8 ⟼ to binary logarithmic fscale (8 ≡ 2^3) and added there (exact)
|
||||
fscale.metric = NAT;
|
||||
CHECK (roughEQ (fscale.join (1, int64_t(8), feedScale), 1 + logf(8))); // feed factor 8 ⟼ now applied to a natural log scale (and added to one, in float)
|
||||
fscale.metric = DEC;
|
||||
CHECK (roughEQ (fscale.join (1, int64_t(100),feedScale), 1 + 20)); // feed factor 8 ⟼ now applied as offset in decibels
|
||||
|
||||
feedScale.neutral = 0;
|
||||
CHECK (not feedScale.isFactor());
|
||||
|
|
|
|||
|
|
@ -85973,7 +85973,7 @@
|
|||
<linktarget COLOR="#c50711" DESTINATION="ID_621869035" ENDARROW="Default" ENDINCLINATION="108;0;" ID="Arrow_ID_191032855" SOURCE="ID_1491788669" STARTARROW="None" STARTINCLINATION="34;-37;"/>
|
||||
<icon BUILTIN="messagebox_warning"/>
|
||||
</node>
|
||||
<node CREATED="1789003479184" ID="ID_833220665" MODIFIED="1789003552060" TEXT="The two doors that actually close">
|
||||
<node CREATED="1789003479184" ID="ID_833220665" MODIFIED="1789003479184" TEXT="The two doors that actually close">
|
||||
<richcontent TYPE="NOTE"><html>
|
||||
<head/>
|
||||
<body>
|
||||
|
|
@ -85985,7 +85985,7 @@
|
|||
<b>The persisted form</b>. The moment a session file records a parameter with a scale, the meaning of its qualifiers is frozen or needs migration. It's worth spending more thought on what a Scale looks like when written out than on what it looks like in memory.
|
||||
</li>
|
||||
<li>
|
||||
<b>Keeping Scale reducible to data</b>. You mention a possible connection to rules later. A rules system can only reason about a Scale if the Scale <em>is</em>  a small tuple of facts (metric, bounds, cyclic flag, domain-set id, default) with a fixed interpreter, rather than a polymorphic object whose behaviour exists only as C++. That constraint costs you nothing today, since your qualifier-based design already satisfies it. It's just worth adopting as a deliberate rule so that a future <code>conform()</code>  override doing something clever doesn't quietly break it.
|
||||
<b>Keeping Scale reducible to data</b>. You mention a possible connection to rules later. A rules system can only reason about a Scale if the Scale <em>is</em> a small tuple of facts (metric, bounds, cyclic flag, domain-set id, default) with a fixed interpreter, rather than a polymorphic object whose behaviour exists only as C++. That constraint costs you nothing today, since your qualifier-based design already satisfies it. It's just worth adopting as a deliberate rule so that a future <code><font color="#673a3a">conform()</font></code> override doing something clever doesn't quietly break it.
|
||||
</li>
|
||||
</ul>
|
||||
</body>
|
||||
|
|
@ -87265,14 +87265,48 @@ Prop.1: dist%3 1 2 0 1 2 0
|
|||
</node>
|
||||
<node COLOR="#338800" CREATED="1789961742643" ID="ID_949990390" MODIFIED="1789961760832" TEXT="Verbindung verschiedener log-Scales">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
<node COLOR="#435e98" CREATED="1789961765581" ID="ID_1536983378" MODIFIED="1789961783683" TEXT="Umwandlung linear ⟷ log"/>
|
||||
<node COLOR="#435e98" CREATED="1789961765581" ID="ID_1536983378" MODIFIED="1789961783683" TEXT="Umwandlung linear ⟷ log">
|
||||
<node CREATED="1790021597423" ID="ID_5025438" MODIFIED="1790021610941" TEXT="die Konvertierungs-Funktionen sind strukturell eigenartig">
|
||||
<icon BUILTIN="messagebox_warning"/>
|
||||
<node CREATED="1790021612645" ID="ID_1614531537" MODIFIED="1790021624855" TEXT="sie hängen eigentlich zwischen zwei Scales"/>
|
||||
<node CREATED="1790021853785" ID="ID_1972131978" LINK="https://math.stackexchange.com/questions/14133/clarify-why-all-logarithms-differ-by-a-constant" MODIFIED="1790022525577" TEXT="sie reflektieren die (verwirrende) Struktur der Logarithmen-Konvertierung">
|
||||
<richcontent TYPE="NOTE"><html>
|
||||
<head/>
|
||||
<body>
|
||||
<p>
|
||||
...ich muß die Formeln jedes mal wieder nachschlagen, bzw mir herleiten, weil diese Formeln so eigenartig <i>über Kreuz</i> aufgebaut sind:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<font color="#79320b" face="Monospaced">log</font><font color="#79320b" face="Monospaced" size="2"><i>b</i></font><font color="#79320b" face="Monospaced">(x) ≡ log</font><font color="#79320b" face="Monospaced" size="2"><i>k</i></font><font color="#79320b" face="Monospaced">(x)/log</font><font color="#79320b" face="Monospaced" size="2"><i>k</i></font><font color="#79320b" face="Monospaced">(b)</font>
|
||||
</li>
|
||||
<li>
|
||||
<font color="#79320b" face="Monospaced">log</font><font color="#79320b" face="Monospaced" size="2"><i>b</i></font><font color="#79320b" face="Monospaced">(x) ≡ log</font><font color="#79320b" face="Monospaced" size="2"><i>b</i></font><font color="#79320b" face="Monospaced">(k)·log</font><font color="#79320b" face="Monospaced" size="2"><i>k</i></font><font color="#79320b" face="Monospaced">(x)</font>
|
||||
</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
</richcontent>
|
||||
</node>
|
||||
<node CREATED="1790021537463" ID="ID_1193038289" MODIFIED="1790022831555" TEXT="Funktion Scale<VAL>::asLogarithm ist zudem gar nicht von der Quell-Skala abhängig"/>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#c8c0b6" COLOR="#435e98" CREATED="1790022911017" ID="ID_761833274" MODIFIED="1790024210457" TEXT="Code-Struktur / Design-Frage : als freie Funktionen?">
|
||||
<node CREATED="1790022936425" ID="ID_1681792313" MODIFIED="1790023005221" TEXT="als Member-Funktionen spart man sich 2 Parameter in zwei Fällen">
|
||||
<icon BUILTIN="smiley-oh"/>
|
||||
</node>
|
||||
<node CREATED="1790022966402" ID="ID_1905759427" MODIFIED="1790022996702" TEXT="allerdings müssen sie dann auf das Scale-API, und man ruft sie mit "template"-Qualifier auf">
|
||||
<icon BUILTIN="smiley-neutral"/>
|
||||
</node>
|
||||
<node COLOR="#135457" CREATED="1790023008316" ID="ID_1604048781" MODIFIED="1790024202124" TEXT="also: in den sauren Apfel beißen — isbesserso">
|
||||
<icon BUILTIN="yes"/>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node COLOR="#435e98" CREATED="1789961784810" ID="ID_245445484" MODIFIED="1789961910375" TEXT="Basis-Konvertierung logarithmischer werte"/>
|
||||
<node COLOR="#435e98" CREATED="1789961796062" ID="ID_1145409092" MODIFIED="1789961910354" TEXT="spezieller Twist: in Decibels steckt ein Faktor 10">
|
||||
<node CREATED="1789961817086" ID="ID_909916918" MODIFIED="1789961906542" TEXT="wenn als Input verwendet, muß der rausdividiert werden">
|
||||
<richcontent TYPE="NOTE"><html>
|
||||
<head>
|
||||
|
||||
</head>
|
||||
<head/>
|
||||
<body>
|
||||
<p>
|
||||
...und dieses Herausdividieren falte ich trickreich in den log-Basis-Konvertierungsfaktor.
|
||||
|
|
@ -87288,6 +87322,27 @@ Prop.1: dist%3 1 2 0 1 2 0
|
|||
</node>
|
||||
<node COLOR="#435e98" CREATED="1789961912393" ID="ID_1778190029" MODIFIED="1789961925432" TEXT="linearer Offset auf Logarithmus wird verweigert"/>
|
||||
</node>
|
||||
<node COLOR="#5928b5" CREATED="1789961961491" ID="ID_219252546" MODIFIED="1790021532727" TEXT="und ja: wer testet, der findet">
|
||||
<richcontent TYPE="NOTE"><html>
|
||||
<head/>
|
||||
<body>
|
||||
<ul>
|
||||
<li>
|
||||
Type-promotion-Bug in der Anwendung der pow()-Funktion
|
||||
</li>
|
||||
<li>
|
||||
logb() liefert nur den exponenten, log2() den 2-er-Logarithmus
|
||||
</li>
|
||||
<li>
|
||||
das <i>Wiederverwenden</i> der asLogarithm()-Funktion macht deutlich, daß diese einen unnötigen Typ-Constraint auf den VAL-Parameter der Ausgans-Scale erzwingt
|
||||
</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
</richcontent>
|
||||
<arrowlink COLOR="#3f3ba8" DESTINATION="ID_1207373600" ENDARROW="Default" ENDINCLINATION="534;0;" ID="Arrow_ID_625720992" STARTARROW="None" STARTINCLINATION="16;533;"/>
|
||||
<font ITALIC="true" NAME="SansSerif" SIZE="12"/>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
|
|
@ -87560,6 +87615,16 @@ Prop.1: dist%3 1 2 0 1 2 0
|
|||
</node>
|
||||
<node BACKGROUND_COLOR="#eef0c5" COLOR="#990000" CREATED="1789675604211" ID="ID_201071963" MODIFIED="1789676070358" TEXT="verify_valueJoining">
|
||||
<icon BUILTIN="pencil"/>
|
||||
<node COLOR="#338800" CREATED="1790015115678" ID="ID_1510723485" MODIFIED="1790015142255" TEXT="Schritt-1 : teste Typ-Konvertierung und Zusammenspiel mit conform()">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
</node>
|
||||
<node COLOR="#338800" CREATED="1790015143816" ID="ID_1173177816" MODIFIED="1790015164942" TEXT="Schritt-2 : kläre und repariere das noch bestehende signed/unsigned-Problem">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#fdfdcf" COLOR="#ff0000" CREATED="1790015165677" ID="ID_1207373600" MODIFIED="1790021319393" TEXT="Schritt-2 : Pfadabdeckung der Logarithmus-Skalen-Behandlung">
|
||||
<linktarget COLOR="#3f3ba8" DESTINATION="ID_1207373600" ENDARROW="Default" ENDINCLINATION="534;0;" ID="Arrow_ID_625720992" SOURCE="ID_219252546" STARTARROW="None" STARTINCLINATION="16;533;"/>
|
||||
<icon BUILTIN="flag-pink"/>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
|
|
|
|||
Loading…
Reference in a new issue