Support all 5 xToys pattern types
- script-v3: reworked with initialActions channel mapping, freq detection
by name, single-channel duplication, full expression evaluator (pow, arithmetic)
- basic-v2: handle frequencyControl:true, 3-channel (ch3 ignored)
- funscript/draw: resample {at,pos} timeline to 100ms ticks via linear interpolation
- audio: resample flat float array to 100ms ticks using length in seconds
- EvaluateExpression replaces ResolveExpr: DataTable.Compute + pow() pre-processing
- All types: single-channel patterns duplicate to both A/B
- All types: missing frequency data uses fixed 150ms default
This commit is contained in:
+361
-53
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Data;
|
||||||
|
using System.Globalization;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -29,7 +31,6 @@ public static class XToysPatternImporter
|
|||||||
static int FreqPctToMs(byte freqPct)
|
static int FreqPctToMs(byte freqPct)
|
||||||
{
|
{
|
||||||
// xToys frequency: 0% = deep (1000ms), 100% = buzzy (10ms)
|
// xToys frequency: 0% = deep (1000ms), 100% = buzzy (10ms)
|
||||||
// freqPct is the raw 0-100 value from the pattern (already clamped in EvaluatePattern)
|
|
||||||
return (int)Math.Round(1000 - 9.9 * freqPct);
|
return (int)Math.Round(1000 - 9.9 * freqPct);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,9 +46,332 @@ public static class XToysPatternImporter
|
|||||||
|
|
||||||
var pattern = doc.RootElement.GetProperty("result").GetProperty("pattern");
|
var pattern = doc.RootElement.GetProperty("result").GetProperty("pattern");
|
||||||
var name = pattern.GetProperty("name").GetString() ?? "Unknown";
|
var name = pattern.GetProperty("name").GetString() ?? "Unknown";
|
||||||
|
var patternType = pattern.GetProperty("type").GetString() ?? "script-v3";
|
||||||
var patternData = pattern.GetProperty("patternData");
|
var patternData = pattern.GetProperty("patternData");
|
||||||
|
|
||||||
// Resolve slider defaults
|
return patternType switch
|
||||||
|
{
|
||||||
|
"basic-v2" => ParseBasicV2(name, patternData),
|
||||||
|
"funscript" => ParseFunscript(name, patternData),
|
||||||
|
"draw" => ParseFunscript(name, patternData),
|
||||||
|
"audio" => ParseAudio(name, pattern, patternData),
|
||||||
|
"script-v3" => ParseScriptV3(name, patternData),
|
||||||
|
_ => ParseScriptV3(name, patternData)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── script-v3: slider-driven, arbitrary pattern names, initialActions channel mapping ──
|
||||||
|
|
||||||
|
static XToysPattern ParseScriptV3(string name, JsonElement patternData)
|
||||||
|
{
|
||||||
|
var sliders = ResolveSliders(patternData);
|
||||||
|
bool hasFreq = patternData.TryGetProperty("frequencyControl", out var fc) && fc.GetBoolean();
|
||||||
|
int channelCount = patternData.TryGetProperty("channels", out var cc) ? cc.GetInt32() : 2;
|
||||||
|
|
||||||
|
// Parse initialActions to map pattern names → channels
|
||||||
|
var patternToChannel = new Dictionary<string, int>();
|
||||||
|
if (patternData.TryGetProperty("initialActions", out var actions))
|
||||||
|
{
|
||||||
|
foreach (var action in actions.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (action.TryGetProperty("type", out var t) && t.GetString() == "updatePattern")
|
||||||
|
{
|
||||||
|
var patName = action.GetProperty("pattern").GetString()!;
|
||||||
|
var channels = action.GetProperty("channels");
|
||||||
|
int ch = 1;
|
||||||
|
if (channels.TryGetProperty("1", out var c1) && c1.GetBoolean()) ch = 1;
|
||||||
|
else if (channels.TryGetProperty("2", out var c2) && c2.GetBoolean()) ch = 2;
|
||||||
|
patternToChannel[patName] = ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all pattern names
|
||||||
|
var patterns = patternData.GetProperty("patterns");
|
||||||
|
var patternNames = new List<string>();
|
||||||
|
foreach (var prop in patterns.EnumerateObject())
|
||||||
|
patternNames.Add(prop.Name);
|
||||||
|
|
||||||
|
// Classify patterns: frequency (name contains "freq") vs intensity
|
||||||
|
var intPatterns = new List<(string name, int channel)>();
|
||||||
|
var freqPatterns = new List<(string name, int channel)>();
|
||||||
|
|
||||||
|
foreach (var pname in patternNames)
|
||||||
|
{
|
||||||
|
int ch = patternToChannel.GetValueOrDefault(pname, 1);
|
||||||
|
if (hasFreq && pname.Contains("freq", StringComparison.OrdinalIgnoreCase))
|
||||||
|
freqPatterns.Add((pname, ch));
|
||||||
|
else
|
||||||
|
intPatterns.Add((pname, ch));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate patterns
|
||||||
|
var intByChannel = new Dictionary<int, byte[]>();
|
||||||
|
var freqByChannel = new Dictionary<int, byte[]>();
|
||||||
|
|
||||||
|
foreach (var (pname, ch) in intPatterns)
|
||||||
|
intByChannel[ch] = EvaluateLoops(patterns.GetProperty(pname), sliders);
|
||||||
|
|
||||||
|
foreach (var (pname, ch) in freqPatterns)
|
||||||
|
freqByChannel[ch] = EvaluateLoops(patterns.GetProperty(pname), sliders);
|
||||||
|
|
||||||
|
// Get arrays for each channel (default to empty)
|
||||||
|
var intA = intByChannel.GetValueOrDefault(1, Array.Empty<byte>());
|
||||||
|
var intB = channelCount >= 2
|
||||||
|
? intByChannel.GetValueOrDefault(2, intA)
|
||||||
|
: intA; // single channel → duplicate
|
||||||
|
var freqA = freqByChannel.GetValueOrDefault(1, Array.Empty<byte>());
|
||||||
|
var freqB = channelCount >= 2
|
||||||
|
? freqByChannel.GetValueOrDefault(2, freqA)
|
||||||
|
: freqA;
|
||||||
|
|
||||||
|
int tickCount = Math.Max(intA.Length, Math.Max(intB.Length, Math.Max(freqA.Length, freqB.Length)));
|
||||||
|
if (tickCount == 0) tickCount = 1;
|
||||||
|
|
||||||
|
byte defaultFreq = Freq.Compress(150);
|
||||||
|
var channelA = new List<WaveFrame>(tickCount);
|
||||||
|
var channelB = new List<WaveFrame>(tickCount);
|
||||||
|
|
||||||
|
for (int i = 0; i < tickCount; i++)
|
||||||
|
{
|
||||||
|
byte ia = i < intA.Length ? intA[i] : (byte)0;
|
||||||
|
byte ib = i < intB.Length ? intB[i] : (byte)0;
|
||||||
|
byte fa = i < freqA.Length ? Freq.Compress(FreqPctToMs(freqA[i])) : defaultFreq;
|
||||||
|
byte fb = i < freqB.Length ? Freq.Compress(FreqPctToMs(freqB[i])) : defaultFreq;
|
||||||
|
|
||||||
|
channelA.Add(new WaveFrame(new[] { fa, fa, fa, fa }, new[] { ia, ia, ia, ia }));
|
||||||
|
channelB.Add(new WaveFrame(new[] { fb, fb, fb, fb }, new[] { ib, ib, ib, ib }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new XToysPattern(name, channelA, channelB);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── basic-v2: channelData with loops/steps, optional frequencyControl ──
|
||||||
|
|
||||||
|
static XToysPattern ParseBasicV2(string name, JsonElement patternData)
|
||||||
|
{
|
||||||
|
var emptySliders = new Dictionary<string, double>();
|
||||||
|
const int defaultFreqMs = 150;
|
||||||
|
bool hasFreq = patternData.TryGetProperty("frequencyControl", out var fc) && fc.GetBoolean();
|
||||||
|
|
||||||
|
byte freqByte = Freq.Compress(defaultFreqMs);
|
||||||
|
|
||||||
|
var intA = Array.Empty<byte>();
|
||||||
|
var intB = Array.Empty<byte>();
|
||||||
|
var freqA = Array.Empty<byte>();
|
||||||
|
var freqB = Array.Empty<byte>();
|
||||||
|
|
||||||
|
if (patternData.TryGetProperty("channelData", out var cd))
|
||||||
|
{
|
||||||
|
if (cd.TryGetProperty("1", out var ch1))
|
||||||
|
{
|
||||||
|
intA = EvaluateLoops(ch1, emptySliders);
|
||||||
|
if (hasFreq) freqA = intA; // basic-v2 with freq: same data is freq
|
||||||
|
}
|
||||||
|
if (cd.TryGetProperty("2", out var ch2))
|
||||||
|
{
|
||||||
|
intB = EvaluateLoops(ch2, emptySliders);
|
||||||
|
if (hasFreq) freqB = intB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single channel → duplicate
|
||||||
|
if (intB.Length == 0 && intA.Length > 0)
|
||||||
|
{
|
||||||
|
intB = intA;
|
||||||
|
freqB = freqA;
|
||||||
|
}
|
||||||
|
if (intA.Length == 0 && intB.Length > 0)
|
||||||
|
{
|
||||||
|
intA = intB;
|
||||||
|
freqA = freqB;
|
||||||
|
}
|
||||||
|
|
||||||
|
int tickCount = Math.Max(intA.Length, intB.Length);
|
||||||
|
if (tickCount == 0) tickCount = 1;
|
||||||
|
|
||||||
|
var channelA = new List<WaveFrame>(tickCount);
|
||||||
|
var channelB = new List<WaveFrame>(tickCount);
|
||||||
|
|
||||||
|
for (int i = 0; i < tickCount; i++)
|
||||||
|
{
|
||||||
|
byte ia = i < intA.Length ? intA[i] : (byte)0;
|
||||||
|
byte ib = i < intB.Length ? intB[i] : (byte)0;
|
||||||
|
byte fa = hasFreq && i < freqA.Length ? Freq.Compress(FreqPctToMs(freqA[i])) : freqByte;
|
||||||
|
byte fb = hasFreq && i < freqB.Length ? Freq.Compress(FreqPctToMs(freqB[i])) : freqByte;
|
||||||
|
|
||||||
|
channelA.Add(new WaveFrame(new[] { fa, fa, fa, fa }, new[] { ia, ia, ia, ia }));
|
||||||
|
channelB.Add(new WaveFrame(new[] { fb, fb, fb, fb }, new[] { ib, ib, ib, ib }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new XToysPattern(name, channelA, channelB);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── funscript / draw: {at, pos} timeline, resample to 100ms ticks ──
|
||||||
|
|
||||||
|
static XToysPattern ParseFunscript(string name, JsonElement patternData)
|
||||||
|
{
|
||||||
|
byte freqByte = Freq.Compress(150);
|
||||||
|
|
||||||
|
var intA = Array.Empty<byte>();
|
||||||
|
var intB = Array.Empty<byte>();
|
||||||
|
|
||||||
|
if (patternData.TryGetProperty("channelData", out var cd))
|
||||||
|
{
|
||||||
|
if (cd.TryGetProperty("1", out var ch1))
|
||||||
|
intA = ResampleTimeline(ch1);
|
||||||
|
if (cd.TryGetProperty("2", out var ch2))
|
||||||
|
intB = ResampleTimeline(ch2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single channel → duplicate
|
||||||
|
if (intB.Length == 0 && intA.Length > 0) intB = intA;
|
||||||
|
if (intA.Length == 0 && intB.Length > 0) intA = intB;
|
||||||
|
|
||||||
|
int tickCount = Math.Max(intA.Length, intB.Length);
|
||||||
|
if (tickCount == 0) tickCount = 1;
|
||||||
|
|
||||||
|
var channelA = new List<WaveFrame>(tickCount);
|
||||||
|
var channelB = new List<WaveFrame>(tickCount);
|
||||||
|
|
||||||
|
for (int i = 0; i < tickCount; i++)
|
||||||
|
{
|
||||||
|
byte ia = i < intA.Length ? intA[i] : (byte)0;
|
||||||
|
byte ib = i < intB.Length ? intB[i] : (byte)0;
|
||||||
|
|
||||||
|
channelA.Add(new WaveFrame(new[] { freqByte, freqByte, freqByte, freqByte }, new[] { ia, ia, ia, ia }));
|
||||||
|
channelB.Add(new WaveFrame(new[] { freqByte, freqByte, freqByte, freqByte }, new[] { ib, ib, ib, ib }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new XToysPattern(name, channelA, channelB);
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] ResampleTimeline(JsonElement channelData)
|
||||||
|
{
|
||||||
|
// channelData is an array of {at, pos} objects (at in ms, pos 0-100)
|
||||||
|
var points = new List<(double at, double pos)>();
|
||||||
|
foreach (var pt in channelData.EnumerateArray())
|
||||||
|
{
|
||||||
|
double at = pt.GetProperty("at").GetDouble();
|
||||||
|
double pos = pt.GetProperty("pos").GetDouble();
|
||||||
|
points.Add((at, pos));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (points.Count == 0) return Array.Empty<byte>();
|
||||||
|
if (points.Count == 1) return new[] { (byte)Math.Clamp((int)Math.Round(points[0].pos), 0, 100) };
|
||||||
|
|
||||||
|
double durationMs = points[^1].at;
|
||||||
|
int tickCount = Math.Max(1, (int)Math.Ceiling(durationMs / 100.0));
|
||||||
|
|
||||||
|
var result = new byte[tickCount];
|
||||||
|
int ptIdx = 0;
|
||||||
|
|
||||||
|
for (int t = 0; t < tickCount; t++)
|
||||||
|
{
|
||||||
|
double tickMs = t * 100.0;
|
||||||
|
|
||||||
|
// Advance to the right pair of points
|
||||||
|
while (ptIdx < points.Count - 2 && points[ptIdx + 1].at < tickMs)
|
||||||
|
ptIdx++;
|
||||||
|
|
||||||
|
// Linear interpolation between points[ptIdx] and points[ptIdx + 1]
|
||||||
|
double pos;
|
||||||
|
if (points[ptIdx + 1].at == points[ptIdx].at)
|
||||||
|
pos = points[ptIdx].pos;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
double frac = (tickMs - points[ptIdx].at) / (points[ptIdx + 1].at - points[ptIdx].at);
|
||||||
|
frac = Math.Clamp(frac, 0, 1);
|
||||||
|
pos = points[ptIdx].pos + (points[ptIdx + 1].pos - points[ptIdx].pos) * frac;
|
||||||
|
}
|
||||||
|
|
||||||
|
result[t] = (byte)Math.Clamp((int)Math.Round(pos), 0, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── audio: flat float array, resample to 100ms ticks ──
|
||||||
|
|
||||||
|
static XToysPattern ParseAudio(string name, JsonElement pattern, JsonElement patternData)
|
||||||
|
{
|
||||||
|
byte freqByte = Freq.Compress(150);
|
||||||
|
|
||||||
|
// Length is in seconds (from pattern.length or patternData.length)
|
||||||
|
double lengthSec = 0;
|
||||||
|
if (pattern.TryGetProperty("length", out var lenEl) && lenEl.ValueKind == JsonValueKind.Number)
|
||||||
|
lengthSec = lenEl.GetDouble();
|
||||||
|
else if (pattern.TryGetProperty("length", out var lenStr) && lenStr.ValueKind == JsonValueKind.String)
|
||||||
|
double.TryParse(lenStr.GetString(), out lengthSec);
|
||||||
|
|
||||||
|
double scale = patternData.TryGetProperty("scale", out var scaleEl) ? scaleEl.GetDouble() : 100;
|
||||||
|
|
||||||
|
var intA = Array.Empty<byte>();
|
||||||
|
var intB = Array.Empty<byte>();
|
||||||
|
|
||||||
|
if (patternData.TryGetProperty("channelData", out var cd))
|
||||||
|
{
|
||||||
|
if (cd.TryGetProperty("1", out var ch1))
|
||||||
|
intA = ResampleAudio(ch1, scale, lengthSec);
|
||||||
|
if (cd.TryGetProperty("2", out var ch2))
|
||||||
|
intB = ResampleAudio(ch2, scale, lengthSec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single channel → duplicate
|
||||||
|
if (intB.Length == 0 && intA.Length > 0) intB = intA;
|
||||||
|
if (intA.Length == 0 && intB.Length > 0) intA = intB;
|
||||||
|
|
||||||
|
int tickCount = Math.Max(intA.Length, intB.Length);
|
||||||
|
if (tickCount == 0) tickCount = 1;
|
||||||
|
|
||||||
|
var channelA = new List<WaveFrame>(tickCount);
|
||||||
|
var channelB = new List<WaveFrame>(tickCount);
|
||||||
|
|
||||||
|
for (int i = 0; i < tickCount; i++)
|
||||||
|
{
|
||||||
|
byte ia = i < intA.Length ? intA[i] : (byte)0;
|
||||||
|
byte ib = i < intB.Length ? intB[i] : (byte)0;
|
||||||
|
|
||||||
|
channelA.Add(new WaveFrame(new[] { freqByte, freqByte, freqByte, freqByte }, new[] { ia, ia, ia, ia }));
|
||||||
|
channelB.Add(new WaveFrame(new[] { freqByte, freqByte, freqByte, freqByte }, new[] { ib, ib, ib, ib }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new XToysPattern(name, channelA, channelB);
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] ResampleAudio(JsonElement channelData, double scale, double lengthSec)
|
||||||
|
{
|
||||||
|
// channelData is a flat array of float values (raw, need to divide by scale)
|
||||||
|
var samples = new List<double>();
|
||||||
|
foreach (var s in channelData.EnumerateArray())
|
||||||
|
samples.Add(s.GetDouble() / scale * 100.0);
|
||||||
|
|
||||||
|
if (samples.Count == 0) return Array.Empty<byte>();
|
||||||
|
|
||||||
|
// Determine tick count from length (seconds → 100ms ticks)
|
||||||
|
int tickCount;
|
||||||
|
if (lengthSec > 0)
|
||||||
|
tickCount = Math.Max(1, (int)Math.Ceiling(lengthSec * 10));
|
||||||
|
else
|
||||||
|
tickCount = samples.Count; // fallback: 1 sample per tick
|
||||||
|
|
||||||
|
var result = new byte[tickCount];
|
||||||
|
|
||||||
|
for (int t = 0; t < tickCount; t++)
|
||||||
|
{
|
||||||
|
// Map tick to sample index
|
||||||
|
int idx = (int)Math.Round((double)t * samples.Count / tickCount);
|
||||||
|
if (idx >= samples.Count) idx = samples.Count - 1;
|
||||||
|
result[t] = (byte)Math.Clamp((int)Math.Round(samples[idx]), 0, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Shared helpers ──
|
||||||
|
|
||||||
|
static Dictionary<string, double> ResolveSliders(JsonElement patternData)
|
||||||
|
{
|
||||||
var sliders = new Dictionary<string, double>();
|
var sliders = new Dictionary<string, double>();
|
||||||
if (patternData.TryGetProperty("ma", out var ma))
|
if (patternData.TryGetProperty("ma", out var ma))
|
||||||
{
|
{
|
||||||
@@ -58,61 +382,25 @@ public static class XToysPatternImporter
|
|||||||
sliders[key] = value;
|
sliders[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return sliders;
|
||||||
// Evaluate patterns to per-tick value arrays
|
|
||||||
var int1 = EvaluatePattern(patternData, "Int1", sliders);
|
|
||||||
var int2 = EvaluatePattern(patternData, "Int2", sliders);
|
|
||||||
var freq1 = EvaluatePattern(patternData, "Freq1", sliders);
|
|
||||||
var freq2 = EvaluatePattern(patternData, "Freq2", sliders);
|
|
||||||
|
|
||||||
// All patterns in a group should have the same total duration.
|
|
||||||
// Use the max length to be safe.
|
|
||||||
int tickCount = Math.Max(int1.Length, Math.Max(int2.Length, Math.Max(freq1.Length, freq2.Length)));
|
|
||||||
|
|
||||||
var channelA = new List<WaveFrame>(tickCount);
|
|
||||||
var channelB = new List<WaveFrame>(tickCount);
|
|
||||||
|
|
||||||
for (int i = 0; i < tickCount; i++)
|
|
||||||
{
|
|
||||||
byte intA = i < int1.Length ? int1[i] : (byte)0;
|
|
||||||
byte intB = i < int2.Length ? int2[i] : (byte)0;
|
|
||||||
// Freq patterns output 0-100 (percentage). Map: 0% = 1000ms deep, 100% = 10ms buzzy.
|
|
||||||
byte freqA = i < freq1.Length ? Freq.Compress(FreqPctToMs(freq1[i])) : Freq.Compress(10);
|
|
||||||
byte freqB = i < freq2.Length ? Freq.Compress(FreqPctToMs(freq2[i])) : Freq.Compress(10);
|
|
||||||
|
|
||||||
channelA.Add(new WaveFrame(
|
|
||||||
new[] { freqA, freqA, freqA, freqA },
|
|
||||||
new[] { intA, intA, intA, intA }));
|
|
||||||
|
|
||||||
channelB.Add(new WaveFrame(
|
|
||||||
new[] { freqB, freqB, freqB, freqB },
|
|
||||||
new[] { intB, intB, intB, intB }));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new XToysPattern(name, channelA, channelB);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static byte[] EvaluatePattern(JsonElement patternData, string patternName, Dictionary<string, double> sliders)
|
static byte[] EvaluateLoops(JsonElement loopsElement, Dictionary<string, double> sliders)
|
||||||
{
|
{
|
||||||
if (!patternData.TryGetProperty("patterns", out var patterns))
|
|
||||||
return Array.Empty<byte>();
|
|
||||||
if (!patterns.TryGetProperty(patternName, out var patternArr))
|
|
||||||
return Array.Empty<byte>();
|
|
||||||
|
|
||||||
var values = new List<byte>();
|
var values = new List<byte>();
|
||||||
double lastEnd = 0;
|
double lastEnd = 0;
|
||||||
|
|
||||||
foreach (var loop in patternArr.EnumerateArray())
|
foreach (var loop in loopsElement.EnumerateArray())
|
||||||
{
|
{
|
||||||
var steps = loop.GetProperty("steps");
|
var steps = loop.GetProperty("steps");
|
||||||
foreach (var step in steps.EnumerateArray())
|
foreach (var step in steps.EnumerateArray())
|
||||||
{
|
{
|
||||||
var type = step.GetProperty("type").GetString() ?? "straight";
|
var type = step.GetProperty("type").GetString() ?? "straight";
|
||||||
var time = ResolveExpr(GetStepValue(step, "time"), sliders);
|
var time = EvaluateExpression(GetStepValue(step, "time"), sliders);
|
||||||
var startVal = step.TryGetProperty("start", out var s)
|
var startVal = step.TryGetProperty("start", out var s)
|
||||||
? (s.ValueKind == JsonValueKind.Null ? lastEnd : ResolveExpr(GetStepValue(step, "start"), sliders))
|
? (s.ValueKind == JsonValueKind.Null ? lastEnd : EvaluateExpression(GetStepValue(step, "start"), sliders))
|
||||||
: lastEnd;
|
: lastEnd;
|
||||||
var endVal = ResolveExpr(GetStepValue(step, "end"), sliders);
|
var endVal = EvaluateExpression(GetStepValue(step, "end"), sliders);
|
||||||
|
|
||||||
int ticks = Math.Max(1, (int)Math.Round(time / 0.1));
|
int ticks = Math.Max(1, (int)Math.Round(time / 0.1));
|
||||||
|
|
||||||
@@ -142,21 +430,41 @@ public static class XToysPatternImporter
|
|||||||
return el.ValueKind switch
|
return el.ValueKind switch
|
||||||
{
|
{
|
||||||
JsonValueKind.String => el.GetString() ?? "0",
|
JsonValueKind.String => el.GetString() ?? "0",
|
||||||
JsonValueKind.Number => el.GetDouble().ToString(),
|
JsonValueKind.Number => el.GetDouble().ToString(CultureInfo.InvariantCulture),
|
||||||
_ => "0"
|
_ => "0"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static double ResolveExpr(string expr, Dictionary<string, double> sliders)
|
static double EvaluateExpression(string expr, Dictionary<string, double> sliders)
|
||||||
{
|
{
|
||||||
// Replace {var} references, then evaluate simple expressions like "ramp/2"
|
// Replace {var} references with slider values
|
||||||
var resolved = Regex.Replace(expr, @"\{(\w+)\}", m =>
|
var resolved = Regex.Replace(expr, @"\{(\w+)\}", m =>
|
||||||
sliders.TryGetValue(m.Groups[1].Value, out var val) ? val.ToString() : "0");
|
sliders.TryGetValue(m.Groups[1].Value, out var val)
|
||||||
|
? val.ToString(CultureInfo.InvariantCulture)
|
||||||
|
: "0");
|
||||||
|
|
||||||
// Parse simple arithmetic: number, or number/number
|
// Try plain number first
|
||||||
var parts = resolved.Split('/');
|
if (double.TryParse(resolved, CultureInfo.InvariantCulture, out var simple))
|
||||||
if (parts.Length == 2 && double.TryParse(parts[0], out var a) && double.TryParse(parts[1], out var b))
|
return simple;
|
||||||
return a / b;
|
|
||||||
return double.TryParse(resolved, out var result) ? result : 0;
|
// Pre-process pow(a, b) calls — DataTable.Compute doesn't support pow
|
||||||
|
resolved = Regex.Replace(resolved, @"pow\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)", m =>
|
||||||
|
{
|
||||||
|
double a = double.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture);
|
||||||
|
double b = double.Parse(m.Groups[2].Value, CultureInfo.InvariantCulture);
|
||||||
|
return Math.Pow(a, b).ToString("G15", CultureInfo.InvariantCulture);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Evaluate with DataTable.Compute (handles +, -, *, /, parentheses)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = new DataTable().Compute(resolved, null);
|
||||||
|
return Convert.ToDouble(result, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Fallback: try simple number
|
||||||
|
return double.TryParse(resolved, CultureInfo.InvariantCulture, out var fallback) ? fallback : 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user