载入中...
搜索中...
未找到
SplinePath.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <array>
5#include <cmath>
6#include <string>
7
8namespace eve::procgen {
9namespace {
10
11bool finite(const SplinePoint& p) {
12 return std::isfinite(p.x) && std::isfinite(p.y) && std::isfinite(p.z) && std::isfinite(p.inX) &&
13 std::isfinite(p.inY) && std::isfinite(p.inZ) && std::isfinite(p.outX) && std::isfinite(p.outY) &&
14 std::isfinite(p.outZ) && std::isfinite(p.rollDegrees) && std::isfinite(p.scaleX) &&
15 std::isfinite(p.scaleY) && std::isfinite(p.pitchDegrees) && std::isfinite(p.yawDegrees) && p.scaleX > 0.f &&
16 p.scaleY > 0.f;
17}
18
19struct V3 {
20 float x, y, z;
21};
22
23V3 add(V3 a, V3 b) { return {a.x + b.x, a.y + b.y, a.z + b.z}; }
24V3 sub(V3 a, V3 b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; }
25V3 mul(V3 a, float s) { return {a.x * s, a.y * s, a.z * s}; }
26float dot(V3 a, V3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
27V3 cross(V3 a, V3 b) { return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x}; }
28float length(V3 value) { return std::sqrt(value.x * value.x + value.y * value.y + value.z * value.z); }
29V3 normalized(V3 value) {
30 const float magnitude = length(value);
31 return magnitude > 1e-7f ? mul(value, 1.f / magnitude) : V3{1.f, 0.f, 0.f};
32}
33V3 rotateAround(V3 value, V3 axis, float angle) {
34 const float c = std::cos(angle), s = std::sin(angle);
35 return add(add(mul(value, c), mul(cross(axis, value), s)), mul(axis, dot(axis, value) * (1.f - c)));
36}
37V3 position(const SplinePoint& p) { return {p.x, p.y, p.z}; }
38
39} // namespace
40
42 if (kind != "linear" && kind != "catmullRom" && kind != "quadraticBezier" && kind != "bezier")
44 DiagnosticCode::InvalidArgument, "spline kind must be linear, catmullRom, quadraticBezier, or bezier",
45 "kind", {}, "procgen.splinePath"));
46 kind_ = kind;
47 invalidate();
48 return Result<void>::success();
49}
50
51void SplinePath::setClosed(bool closed) {
52 bool changed = closed_ != closed;
53 if (closed)
54 for (std::size_t i = 1; i < points_.size(); ++i) {
55 changed = changed || points_[i].breakBefore;
56 points_[i].breakBefore = false;
57 }
58 if (!changed) return;
59 closed_ = closed;
60 invalidate();
61}
62
64 if (!finite(point))
66 "point", {}, "procgen.splinePath"));
67 points_.push_back(point);
68 invalidate();
69 return Result<void>::success();
70}
71
73 if (index < 0 || index >= pointCount())
74 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "spline point index is out of range",
75 "point.index", {}, "procgen.splinePath"));
76 if (!finite(point))
78 "point", {}, "procgen.splinePath"));
79 points_[static_cast<std::size_t>(index)] = point;
80 invalidate();
81 return Result<void>::success();
82}
83
84Result<void> SplinePath::setPointProfileResult(int index, float rollDegrees, float scaleX, float scaleY) {
85 if (index < 0 || index >= pointCount())
86 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "spline point index is out of range",
87 "point.index", {}, "procgen.splinePath"));
88 if (!std::isfinite(rollDegrees) || !std::isfinite(scaleX) || !std::isfinite(scaleY) || scaleX <= 0.f ||
89 scaleY <= 0.f)
91 "spline point profile requires finite roll and positive scales",
92 "point.profile", {}, "procgen.splinePath"));
93 auto candidate = points_[static_cast<std::size_t>(index)];
94 candidate.rollDegrees = rollDegrees;
95 candidate.scaleX = scaleX;
96 candidate.scaleY = scaleY;
97 points_[static_cast<std::size_t>(index)] = candidate;
98 invalidate();
99 return Result<void>::success();
100}
101
102Result<void> SplinePath::setPointRotationResult(int index, float pitchDegrees, float yawDegrees, float rollDegrees) {
103 if (index < 0 || index >= pointCount())
104 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "spline point index is out of range",
105 "point.index", {}, "procgen.splinePath"));
106 if (!std::isfinite(pitchDegrees) || !std::isfinite(yawDegrees) || !std::isfinite(rollDegrees))
108 "spline point rotation must be finite", "point.rotation", {},
109 "procgen.splinePath"));
110 auto candidate = points_[static_cast<std::size_t>(index)];
111 candidate.pitchDegrees = pitchDegrees;
112 candidate.yawDegrees = yawDegrees;
113 candidate.rollDegrees = rollDegrees;
114 points_[static_cast<std::size_t>(index)] = candidate;
115 invalidate();
116 return Result<void>::success();
117}
118
120 if (index <= 0 || index >= pointCount())
122 "chunk breaks require a non-first existing spline point",
123 "point.index", {}, "procgen.splinePath"));
124 if (closed_ && disconnected)
126 "closed splines cannot contain disconnected chunks", "closed",
127 {}, "procgen.splinePath"));
128 if (points_[static_cast<std::size_t>(index)].breakBefore == disconnected) return Result<void>::success();
129 points_[static_cast<std::size_t>(index)].breakBefore = disconnected;
130 invalidate();
131 return Result<void>::success();
132}
133
135 if (index < 0 || index >= pointCount())
136 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "spline point index is out of range",
137 "point.index", {}, "procgen.splinePath"));
138 points_.erase(points_.begin() + index);
139 invalidate();
140 return Result<void>::success();
141}
142
144 if (points_.empty()) return;
145 points_.clear();
146 invalidate();
147}
148
149int SplinePath::segmentCount() const noexcept {
150 if (points_.size() < 2u) return 0;
151 int result = closed_ ? 1 : 0;
152 for (std::size_t i = 1; i < points_.size(); ++i)
153 if (!points_[i].breakBefore) ++result;
154 return result;
155}
156
157int SplinePath::chunkCount() const noexcept {
158 if (points_.empty()) return 0;
159 int result = 1;
160 for (std::size_t i = 1; i < points_.size(); ++i)
161 if (points_[i].breakBefore) ++result;
162 return result;
163}
164
166 if (chunk < 0 || chunk >= chunkCount())
168 DiagnosticCode::NotFound, "spline chunk index is out of range", "chunk.index", {}, "procgen.splinePath"));
169 SplinePath result;
170 result.kind_ = kind_;
171 int current = 0;
172 for (std::size_t i = 0; i < points_.size(); ++i) {
173 if (i > 0u && points_[i].breakBefore) ++current;
174 if (current != chunk) continue;
175 auto point = points_[i];
176 point.breakBefore = false;
177 result.points_.push_back(point);
178 }
179 result.closed_ = closed_ && chunkCount() == 1;
180 result.revision_ = revision_;
181 if (result.segmentCount() == 0)
183 "each spline chunk requires at least two points",
184 "chunk.points", {}, "procgen.splinePath"));
185 return Result<SplinePath>::success(std::move(result));
186}
187
188std::vector<std::pair<int, int>> SplinePath::segmentEndpoints() const {
189 std::vector<std::pair<int, int>> result;
190 for (int i = 0; i + 1 < pointCount(); ++i)
191 if (!points_[static_cast<std::size_t>(i + 1)].breakBefore) result.emplace_back(i, i + 1);
192 if (closed_ && pointCount() > 1) result.emplace_back(pointCount() - 1, 0);
193 return result;
194}
195
196Result<void> SplinePath::validateReady() const {
197 if (segmentCount() == 0)
199 "spline requires at least two points", "points", {},
200 "procgen.splinePath"));
201 return Result<void>::success();
202}
203
204SplineSample SplinePath::evaluateUnchecked(float t) const {
205 t = std::clamp(t, 0.f, 1.f);
206 const int segments = segmentCount();
207 const float scaled = t * static_cast<float>(segments);
208 const int segment = std::min(static_cast<int>(scaled), segments - 1);
209 const float u = segment == segments - 1 && t == 1.f ? 1.f : scaled - static_cast<float>(segment);
210 return evaluateSegmentUnchecked(segment, u, t);
211}
212
213SplineSample SplinePath::evaluateSegmentUnchecked(int segment, float u, float normalizedDistance) const {
214 const auto endpoints = segmentEndpoints();
215 const int aIndex = endpoints[static_cast<std::size_t>(segment)].first;
216 const int bIndex = endpoints[static_cast<std::size_t>(segment)].second;
217 const auto& a = points_[static_cast<std::size_t>(aIndex)];
218 const auto& b = points_[static_cast<std::size_t>(bIndex)];
219 int chunkIndex = 0;
220 for (int i = 1; i <= aIndex; ++i)
221 if (points_[static_cast<std::size_t>(i)].breakBefore) ++chunkIndex;
222 V3 value{}, tangent{};
223 if (kind_ == "linear") {
224 value = add(position(a), mul(sub(position(b), position(a)), u));
225 tangent = sub(position(b), position(a));
226 } else if (kind_ == "quadraticBezier") {
227 const V3 p0 = position(a), p2 = position(b);
228 const V3 outgoing = add(p0, {a.outX, a.outY, a.outZ});
229 const V3 incoming = add(p2, {b.inX, b.inY, b.inZ});
230 const V3 p1 = mul(add(outgoing, incoming), 0.5f);
231 const float v = 1.f - u;
232 value = add(add(mul(p0, v * v), mul(p1, 2.f * v * u)), mul(p2, u * u));
233 tangent = add(mul(sub(p1, p0), 2.f * v), mul(sub(p2, p1), 2.f * u));
234 } else if (kind_ == "bezier") {
235 const V3 p0 = position(a), p1 = add(p0, {a.outX, a.outY, a.outZ});
236 const V3 p3 = position(b), p2 = add(p3, {b.inX, b.inY, b.inZ});
237 const float v = 1.f - u;
238 value =
239 add(add(mul(p0, v * v * v), mul(p1, 3.f * v * v * u)), add(mul(p2, 3.f * v * u * u), mul(p3, u * u * u)));
240 tangent = add(add(mul(sub(p1, p0), 3.f * v * v), mul(sub(p2, p1), 6.f * v * u)), mul(sub(p3, p2), 3.f * u * u));
241 } else {
242 int previous = aIndex;
243 if (aIndex > 0 && !points_[static_cast<std::size_t>(aIndex)].breakBefore)
244 previous = aIndex - 1;
245 else if (closed_ && aIndex == 0)
246 previous = pointCount() - 1;
247 int next = bIndex;
248 if (bIndex + 1 < pointCount() && !points_[static_cast<std::size_t>(bIndex + 1)].breakBefore)
249 next = bIndex + 1;
250 else if (closed_ && bIndex == pointCount() - 1)
251 next = 0;
252 const V3 p0 = position(points_[static_cast<std::size_t>(previous)]);
253 const V3 p1 = position(a), p2 = position(b);
254 const V3 p3 = position(points_[static_cast<std::size_t>(next)]);
255 const float u2 = u * u, u3 = u2 * u;
256 value = mul(add(add(mul(p1, 2.f), mul(sub(p2, p0), u)),
257 add(mul(add(add(mul(p0, 2.f), mul(p1, -5.f)), add(mul(p2, 4.f), mul(p3, -1.f))), u2),
258 mul(add(add(mul(p0, -1.f), mul(p1, 3.f)), add(mul(p2, -3.f), p3)), u3))),
259 0.5f);
260 tangent = mul(
261 add(sub(p2, p0), add(mul(add(add(mul(p0, 2.f), mul(p1, -5.f)), add(mul(p2, 4.f), mul(p3, -1.f))), 2.f * u),
262 mul(add(add(mul(p0, -1.f), mul(p1, 3.f)), add(mul(p2, -3.f), p3)), 3.f * u2))),
263 0.5f);
264 }
265 const float tangentLength = length(tangent);
266 if (tangentLength > 1e-7f)
267 tangent = mul(tangent, 1.f / tangentLength);
268 else
269 tangent = {1.f, 0.f, 0.f};
270 return {value.x,
271 value.y,
272 value.z,
273 tangent.x,
274 tangent.y,
275 tangent.z,
276 normalizedDistance,
277 std::lerp(a.rollDegrees, b.rollDegrees, u),
278 std::lerp(a.scaleX, b.scaleX, u),
279 std::lerp(a.scaleY, b.scaleY, u),
280 chunkIndex,
281 std::lerp(a.pitchDegrees, b.pitchDegrees, u),
282 std::lerp(a.yawDegrees, b.yawDegrees, u)};
283}
284
286 auto valid = validateReady();
287 if (!valid.ok()) return Result<SplineSample>::failure(valid.status());
288 if (!std::isfinite(t))
290 DiagnosticCode::InvalidArgument, "spline parameter must be finite", "t", {}, "procgen.splinePath"));
291 return Result<SplineSample>::success(evaluateUnchecked(t));
292}
293
294Result<void> SplinePath::ensureArcTable(int samplesPerSegment) const {
295 auto valid = validateReady();
296 if (!valid.ok()) return valid;
297 if (samplesPerSegment < 2 || samplesPerSegment > 1024)
299 "arc samples per segment must be in [2, 1024]",
300 "samplesPerSegment", {}, "procgen.splinePath"));
301 if (arcSamplesPerSegment_ == samplesPerSegment && !arcTable_.empty()) return Result<void>::success();
302 arcTable_.clear();
303 arcTable_.reserve(static_cast<std::size_t>(segmentCount() * samplesPerSegment + chunkCount()));
304 float distance = 0.f;
305 const auto endpoints = segmentEndpoints();
306 for (int segment = 0; segment < segmentCount(); ++segment) {
307 const bool startsChunk = segment == 0 || endpoints[static_cast<std::size_t>(segment - 1)].second !=
308 endpoints[static_cast<std::size_t>(segment)].first;
309 auto previous =
310 evaluateSegmentUnchecked(segment, 0.f, static_cast<float>(segment) / static_cast<float>(segmentCount()));
311 if (startsChunk) arcTable_.push_back({segment, 0.f, distance});
312 for (int sample = 1; sample <= samplesPerSegment; ++sample) {
313 const float u = static_cast<float>(sample) / static_cast<float>(samplesPerSegment);
314 const auto current = evaluateSegmentUnchecked(
315 segment, u, (static_cast<float>(segment) + u) / static_cast<float>(segmentCount()));
317 arcTable_.push_back({segment, u, distance});
319 }
320 }
321 arcSamplesPerSegment_ = samplesPerSegment;
322 return Result<void>::success();
323}
324
325Result<float> SplinePath::lengthResult(int samplesPerSegment) const {
326 auto ready = ensureArcTable(samplesPerSegment);
327 if (!ready.ok()) return Result<float>::failure(ready.status());
328 return Result<float>::success(arcTable_.back().distance);
329}
330
332 if (!std::isfinite(distance))
334 DiagnosticCode::InvalidArgument, "spline distance must be finite", "distance", {}, "procgen.splinePath"));
335 auto ready = ensureArcTable(samplesPerSegment);
336 if (!ready.ok()) return Result<SplineSample>::failure(ready.status());
337 const float total = arcTable_.back().distance;
338 distance = std::clamp(distance, 0.f, total);
339 const auto upper = std::lower_bound(arcTable_.begin(), arcTable_.end(), distance,
340 [](const ArcEntry& entry, float value) { return entry.distance < value; });
341 int segment = 0;
342 float u = 0.f;
343 if (upper == arcTable_.begin()) {
344 segment = upper->segment;
345 u = upper->u;
346 } else if (upper == arcTable_.end()) {
347 segment = arcTable_.back().segment;
348 u = arcTable_.back().u;
349 } else {
350 const auto& before = *(upper - 1);
351 const float span = upper->distance - before.distance;
352 if (before.segment != upper->segment || span <= 1e-7f) {
353 segment = upper->segment;
354 u = upper->u;
355 } else {
356 segment = upper->segment;
357 u = std::lerp(before.u, upper->u, (distance - before.distance) / span);
358 }
359 }
360 auto sample =
361 evaluateSegmentUnchecked(segment, u, (static_cast<float>(segment) + u) / static_cast<float>(segmentCount()));
362 sample.normalizedDistance = total > 1e-7f ? distance / total : 0.f;
364}
365
366Result<SplineSample> SplinePath::closestPointResult(float x, float y, float z, int samplesPerSegment) const {
367 if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z))
369 DiagnosticCode::InvalidArgument, "closest point query must be finite", "point", {}, "procgen.splinePath"));
370 auto ready = ensureArcTable(samplesPerSegment);
371 if (!ready.ok()) return Result<SplineSample>::failure(ready.status());
372 float bestSquared = 0.f;
374 bool assigned = false;
375 for (const auto& entry : arcTable_) {
376 auto sample = evaluateSegmentUnchecked(
377 entry.segment, entry.u, (static_cast<float>(entry.segment) + entry.u) / static_cast<float>(segmentCount()));
378 const float dx = sample.x - x, dy = sample.y - y, dz = sample.z - z;
379 const float squared = dx * dx + dy * dy + dz * dz;
380 if (!assigned || squared < bestSquared) {
381 assigned = true;
382 bestSquared = squared;
383 best = sample;
384 best.normalizedDistance =
385 arcTable_.back().distance > 1e-7f ? entry.distance / arcTable_.back().distance : 0.f;
386 }
387 }
389}
390
392 float rollDegrees, int samplesPerSegment) const {
393 if (sampleCount < 1 || sampleCount > 4096)
395 Diagnostic::error(DiagnosticCode::InvalidArgument, "frame sampleCount must be in [1, 4096]", "sampleCount",
396 {}, "procgen.splinePath"));
397 if (!std::isfinite(rollDegrees))
399 DiagnosticCode::InvalidArgument, "frame roll must be finite", "rollDegrees", {}, "procgen.splinePath"));
400 float totalLength = 0.f;
401 if (uniformByDistance) {
402 auto measured = lengthResult(samplesPerSegment);
403 if (!measured.ok()) return Result<std::vector<SplineFrameSample>>::failure(measured.status());
404 totalLength = measured.value();
405 } else {
406 auto ready = validateReady();
407 if (!ready.ok()) return Result<std::vector<SplineFrameSample>>::failure(ready.status());
408 }
409
410 std::vector<SplineFrameSample> frames;
411 frames.reserve(static_cast<std::size_t>(sampleCount + 1));
412 V3 previousTangent{}, previousSide{};
413 for (int index = 0; index <= sampleCount; ++index) {
414 const float ratio = static_cast<float>(index) / static_cast<float>(sampleCount);
415 auto sampled =
416 uniformByDistance ? evaluateDistanceResult(totalLength * ratio, samplesPerSegment) : evaluateResult(ratio);
417 if (!sampled.ok()) return Result<std::vector<SplineFrameSample>>::failure(sampled.status());
418 const auto sample = sampled.value();
419 const V3 tangent = normalized({sample.tangentX, sample.tangentY, sample.tangentZ});
420 V3 side;
421 if (index == 0 || sample.chunkIndex != frames.back().sample.chunkIndex) {
422 const V3 reference = std::abs(tangent.y) < 0.9f ? V3{0.f, 1.f, 0.f} : V3{1.f, 0.f, 0.f};
423 side = normalized(cross(reference, tangent));
424 } else {
425 const V3 axis = cross(previousTangent, tangent);
426 const float axisSize = length(axis);
427 if (axisSize > 1e-7f) {
428 const float angle = std::atan2(axisSize, std::clamp(dot(previousTangent, tangent), -1.f, 1.f));
429 side = rotateAround(previousSide, mul(axis, 1.f / axisSize), angle);
430 } else if (dot(previousTangent, tangent) < 0.f) {
431 side = mul(previousSide, -1.f);
432 } else {
433 side = previousSide;
434 }
435 side = normalized(sub(side, mul(tangent, dot(side, tangent))));
436 }
437 V3 up = normalized(cross(tangent, side));
438 side = normalized(cross(up, tangent));
439 frames.push_back({sample, side.x, side.y, side.z, up.x, up.y, up.z, tangent.x, tangent.y, tangent.z});
440 previousTangent = tangent;
441 previousSide = side;
442 }
443
444 if (closed_ && frames.size() > 1u) {
445 const V3 firstSide{frames.front().sideX, frames.front().sideY, frames.front().sideZ};
446 const V3 lastSide{frames.back().sideX, frames.back().sideY, frames.back().sideZ};
447 const V3 tangent{frames.front().sample.tangentX, frames.front().sample.tangentY,
448 frames.front().sample.tangentZ};
449 const float correction =
450 std::atan2(dot(tangent, cross(lastSide, firstSide)), std::clamp(dot(lastSide, firstSide), -1.f, 1.f));
451 for (int index = 1; index <= sampleCount; ++index) {
452 auto& frame = frames[static_cast<std::size_t>(index)];
453 const float ratio = static_cast<float>(index) / static_cast<float>(sampleCount);
454 const V3 currentTangent{frame.sample.tangentX, frame.sample.tangentY, frame.sample.tangentZ};
455 V3 side = rotateAround({frame.sideX, frame.sideY, frame.sideZ}, currentTangent, correction * ratio);
456 V3 up = normalized(cross(currentTangent, side));
457 side = normalized(cross(up, currentTangent));
458 frame.sideX = side.x;
459 frame.sideY = side.y;
460 frame.sideZ = side.z;
461 frame.upX = up.x;
462 frame.upY = up.y;
463 frame.upZ = up.z;
464 }
465 frames.back() = frames.front();
466 frames.back().sample.normalizedDistance = 1.f;
467 }
468
469 for (auto& frame : frames) {
470 V3 side{frame.sideX, frame.sideY, frame.sideZ};
471 V3 up{frame.upX, frame.upY, frame.upZ};
472 V3 forward{frame.sample.tangentX, frame.sample.tangentY, frame.sample.tangentZ};
473 const float yaw = frame.sample.yawDegrees * 0.01745329251994329577f;
474 if (std::abs(yaw) > 1e-7f) {
475 forward = rotateAround(forward, up, yaw);
476 side = rotateAround(side, up, yaw);
477 }
478 const float pitch = frame.sample.pitchDegrees * 0.01745329251994329577f;
479 if (std::abs(pitch) > 1e-7f) {
480 forward = rotateAround(forward, side, pitch);
481 up = rotateAround(up, side, pitch);
482 }
483 const float roll = (rollDegrees + frame.sample.rollDegrees) * 0.01745329251994329577f;
484 if (std::abs(roll) > 1e-7f) {
485 const float c = std::cos(roll), s = std::sin(roll);
486 const V3 rolledSide = add(mul(side, c), mul(up, s));
487 up = add(mul(up, c), mul(side, -s));
488 side = rolledSide;
489 }
490 frame.sideX = side.x;
491 frame.sideY = side.y;
492 frame.sideZ = side.z;
493 frame.upX = up.x;
494 frame.upY = up.y;
495 frame.upZ = up.z;
496 frame.forwardX = forward.x;
497 frame.forwardY = forward.y;
498 frame.forwardZ = forward.z;
499 }
500 return Result<std::vector<SplineFrameSample>>::success(std::move(frames));
501}
502
504 if (index < 0 || index >= count())
506 "spline distribution index is out of range",
507 "distribution.index", {}, "procgen.splinePath"));
508 return Result<SplineSample>::success(frames_[static_cast<std::size_t>(index)].sample);
509}
510
512 if (index < 0 || index >= count())
514 "spline distribution index is out of range",
515 "distribution.index", {}, "procgen.splinePath"));
516 return Result<SplineFrameSample>::success(frames_[static_cast<std::size_t>(index)]);
517}
518
519int SplinePolyline::count() const noexcept {
520 int result = 0;
521 for (const auto& chunk : chunks_) result += static_cast<int>(chunk.size());
522 return result;
523}
524
525int SplinePolyline::chunkPointCount(int chunk) const noexcept {
526 return chunk < 0 || chunk >= chunkCount() ? 0 : static_cast<int>(chunks_[static_cast<std::size_t>(chunk)].size());
527}
528
530 if (chunk < 0 || chunk >= chunkCount() || index < 0 || index >= chunkPointCount(chunk))
532 "spline polyline index is out of range",
533 "polyline.index", {}, "procgen.splinePath"));
534 return Result<SplineSample>::success(chunks_[static_cast<std::size_t>(chunk)][static_cast<std::size_t>(index)]);
535}
536
538 if (index >= 0)
539 for (const auto& chunk : chunks_) {
540 if (index < static_cast<int>(chunk.size()))
541 return Result<SplineSample>::success(chunk[static_cast<std::size_t>(index)]);
542 index -= static_cast<int>(chunk.size());
543 }
545 DiagnosticCode::NotFound, "spline polyline index is out of range", "polyline.index", {}, "procgen.splinePath"));
546}
547
548Result<SplineSample> SplinePath::travelResult(float distance, std::string_view wrapMode, int samplesPerSegment) const {
549 if (!std::isfinite(distance))
551 "spline travel distance must be finite", "distance", {},
552 "procgen.splinePath"));
553 if (wrapMode != "clamp" && wrapMode != "loop" && wrapMode != "pingPong")
555 DiagnosticCode::InvalidArgument, "spline travel wrap mode must be clamp, loop, or pingPong", "wrapMode", {},
556 "procgen.splinePath"));
557 auto measured = lengthResult(samplesPerSegment);
558 if (!measured.ok()) return Result<SplineSample>::failure(measured.status());
559 const float total = measured.value();
560 float resolved = distance;
561 if (wrapMode == "loop" && total > 1e-7f) {
562 resolved = std::fmod(distance, total);
563 if (resolved < 0.f) resolved += total;
564 } else if (wrapMode == "pingPong" && total > 1e-7f) {
565 const float period = total * 2.f;
566 resolved = std::fmod(distance, period);
567 if (resolved < 0.f) resolved += period;
568 if (resolved > total) resolved = period - resolved;
569 }
570 return evaluateDistanceResult(resolved, samplesPerSegment);
571}
572
574 int samplesPerSegment) const {
575 auto sample = travelResult(distance, wrapMode, samplesPerSegment);
576 if (!sample.ok()) return Result<SplineFrameSample>::failure(sample.status());
577 const int frameIntervals = std::clamp(segmentCount() * samplesPerSegment, 1, 4096);
578 auto frames = sampleFramesResult(frameIntervals, true, 0.f, samplesPerSegment);
579 if (!frames.ok()) return Result<SplineFrameSample>::failure(frames.status());
580 const float location = sample.value().normalizedDistance * static_cast<float>(frameIntervals);
581 const int lowerIndex = std::clamp(static_cast<int>(std::floor(location)), 0, frameIntervals);
582 const int upperIndex = std::min(lowerIndex + 1, frameIntervals);
583 const float blend = location - static_cast<float>(lowerIndex);
584 const auto& lower = frames.value()[static_cast<std::size_t>(lowerIndex)];
585 const auto& upper = frames.value()[static_cast<std::size_t>(upperIndex)];
586 V3 forward =
587 normalized({std::lerp(lower.forwardX, upper.forwardX, blend), std::lerp(lower.forwardY, upper.forwardY, blend),
588 std::lerp(lower.forwardZ, upper.forwardZ, blend)});
589 V3 side = normalized({std::lerp(lower.sideX, upper.sideX, blend), std::lerp(lower.sideY, upper.sideY, blend),
590 std::lerp(lower.sideZ, upper.sideZ, blend)});
591 side = normalized(sub(side, mul(forward, dot(side, forward))));
592 const V3 up = normalized(cross(forward, side));
593 side = normalized(cross(up, forward));
595 {sample.value(), side.x, side.y, side.z, up.x, up.y, up.z, forward.x, forward.y, forward.z});
596}
597
599 int samplesPerSegment) const {
600 if (instanceCount < 1 || instanceCount > 100000)
602 "spline instanceCount must be in [1, 100000]",
603 "instanceCount", {}, "procgen.splinePath"));
604 const bool duplicateEnd = includeEnd && !closed_ && instanceCount > 1;
605 const int intervals = duplicateEnd ? instanceCount - 1 : instanceCount;
606 auto frames = sampleFramesResult(std::max(1, intervals), true, 0.f, samplesPerSegment);
607 if (!frames.ok()) return Result<SplineDistribution>::failure(frames.status());
608 frames.value().resize(static_cast<std::size_t>(instanceCount));
609 return Result<SplineDistribution>::success(SplineDistribution(std::move(frames).takeValue()));
610}
611
612Result<SplinePolyline> SplinePath::polylineResult(int sampleCount, bool uniformByDistance,
613 int samplesPerSegment) const {
614 if (sampleCount < 2 || sampleCount > 65536)
616 "spline polyline sampleCount must be in [2, 65536]",
617 "sampleCount", {}, "procgen.splinePath"));
618 if (sampleCount < chunkCount() * 2)
620 DiagnosticCode::InvalidArgument, "spline polyline requires at least two samples per chunk", "sampleCount",
621 {}, "procgen.splinePath"));
622 std::vector<std::vector<SplineSample>> chunks;
623 chunks.reserve(static_cast<std::size_t>(chunkCount()));
624 int remaining = sampleCount;
625 for (int chunk = 0; chunk < chunkCount(); ++chunk) {
626 auto path = chunkPathResult(chunk);
627 if (!path.ok()) return Result<SplinePolyline>::failure(path.status());
628 const int chunksLeft = chunkCount() - chunk;
629 const int maximum = remaining - (chunksLeft - 1) * 2;
630 const int proposed = sampleCount * path.value().segmentCount() / segmentCount();
631 const int chunkSamples = chunksLeft == 1 ? remaining : std::clamp(proposed, 2, maximum);
632 remaining -= chunkSamples;
633 auto measured = uniformByDistance ? path.value().lengthResult(samplesPerSegment) : Result<float>::success(0.f);
634 if (!measured.ok()) return Result<SplinePolyline>::failure(measured.status());
635 std::vector<SplineSample> points;
636 points.reserve(static_cast<std::size_t>(chunkSamples));
637 const int denominator = path.value().isClosed() ? chunkSamples : chunkSamples - 1;
638 for (int index = 0; index < chunkSamples; ++index) {
639 const float ratio = static_cast<float>(index) / static_cast<float>(denominator);
640 auto sample = uniformByDistance
641 ? path.value().evaluateDistanceResult(measured.value() * ratio, samplesPerSegment)
642 : path.value().evaluateResult(ratio);
643 if (!sample.ok()) return Result<SplinePolyline>::failure(sample.status());
644 points.push_back(sample.value());
645 }
646 chunks.push_back(std::move(points));
647 }
648 return Result<SplinePolyline>::success(SplinePolyline(std::move(chunks), closed_));
649}
650
651Result<void> SplinePath::applyShapePresetResult(std::string_view preset, int pointCount, float radius, float height,
652 float turns) {
653 if (preset != "line" && preset != "circle" && preset != "arc" && preset != "spiral" && preset != "wave")
655 "spline preset must be line, circle, arc, spiral, or wave",
656 "preset", {}, "procgen.splinePath"));
657 if (pointCount < 2 || pointCount > 4096 || (preset == "circle" && pointCount < 3))
659 "spline preset pointCount is invalid", "pointCount", {},
660 "procgen.splinePath"));
661 if (!std::isfinite(radius) || !std::isfinite(height) || !std::isfinite(turns) || radius <= 0.f || turns <= 0.f)
663 "spline preset requires finite positive radius and turns",
664 "preset.parameters", {}, "procgen.splinePath"));
665 constexpr float tau = 6.28318530717958647692f;
666 std::vector<SplinePoint> candidate;
667 candidate.reserve(static_cast<std::size_t>(pointCount));
668 for (int index = 0; index < pointCount; ++index) {
669 const float t = pointCount > 1 ? static_cast<float>(index) / static_cast<float>(pointCount - 1) : 0.f;
671 if (preset == "line") {
672 point = {-radius + 2.f * radius * t, height * t, 0.f};
673 } else if (preset == "circle") {
674 const float angle = tau * static_cast<float>(index) / static_cast<float>(pointCount);
675 point = {std::cos(angle) * radius, 0.f, std::sin(angle) * radius};
676 } else if (preset == "arc") {
677 const float angle = (-0.25f + 0.5f * t) * tau * turns;
678 point = {std::cos(angle) * radius, height * t, std::sin(angle) * radius};
679 } else if (preset == "spiral") {
680 const float angle = tau * turns * t;
681 const float ring = radius * t;
682 point = {std::cos(angle) * ring, height * t, std::sin(angle) * ring};
683 } else {
684 point = {-radius + 2.f * radius * t, std::sin(tau * turns * t) * height, 0.f};
685 }
686 candidate.push_back(point);
687 }
688 points_ = std::move(candidate);
689 kind_ = preset == "line" ? "linear" : "catmullRom";
690 closed_ = preset == "circle";
691 invalidate();
692 return Result<void>::success();
693}
694
695void SplinePath::invalidate() {
696 ++revision_;
697 arcTable_.clear();
698 arcSamplesPerSegment_ = 0;
699}
700
701} // namespace eve::procgen
double value
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
AuthorityStoreHandleRef reference
Definition Authority.cpp:24
const std::string & s
Vec3 tangent
Definition CaveMesh.cpp:80
float length
Definition CaveMesh.cpp:94
glm::vec4 p[6]
float maximum[3]
std::uint32_t instanceCount
float u
Definition Grass.cpp:233
float v
HexVec3 up
std::int32_t second
std::int32_t c
std::int32_t first
float blend
std::uint32_t height
TokenKind kind
std::array< float, 3 > position
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
float distance
graphics::Canvas * previous
bool finite
float radius
std::string path
Definition PlayHost.cpp:110
std::shared_ptr< const std::vector< glm::vec2 > > points
float t
double current
float dz
float dy
float dx
std::map< Cell, int > best
ecs::EntityHandle side
float size
Definition TreeMesh.cpp:156
uint32_t index
int turns
glm::vec3 point
float angle
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
Owning uniformly distributed spline samples for instancing.
Definition SplinePath.h:45
Result< SplineSample > sampleResult(int index) const
Return one sample value, or a structured out-of-range diagnostic.
Result< SplineFrameSample > frameResult(int index) const
Return one sample with its transported orientation frame.
int count() const noexcept
Return the number of independent sample values.
Definition SplinePath.h:50
Owning multi-segment 3D spline with deterministic bounded sampling.
Definition SplinePath.h:91
Result< void > setPointRotationResult(int index, float pitchDegrees, float yawDegrees, float rollDegrees)
Atomically replace point-local pitch, yaw, and roll orientation.
Result< void > setPointResult(int index, const SplinePoint &point)
Replace a control point atomically.
Result< void > applyShapePresetResult(std::string_view preset, int pointCount, float radius, float height, float turns=1.f)
Atomically replace this path with line, circle, arc, spiral, or wave preset points.
Result< void > removePointResult(int index)
Remove a control point by stable current index.
Result< SplineSample > evaluateResult(float t) const
Evaluate by normalized segment parameter, not arc length.
void clear()
Remove every point.
void setClosed(bool closed)
Configure whether the final point connects back to the first.
int chunkCount() const noexcept
Return the number of independently connected chunks.
Result< std::vector< SplineFrameSample > > sampleFramesResult(int sampleCount, bool uniformByDistance, float rollDegrees=0.f, int samplesPerSegment=24) const
Sample stable parallel-transport frames for extrusion and placement.
Result< void > setKindResult(std::string_view kind)
Select linear, catmullRom, quadraticBezier, or cubic bezier interpolation.
std::string_view kind() const noexcept
Return interpolation kind.
Definition SplinePath.h:165
int pointCount() const noexcept
Return point count.
Definition SplinePath.h:157
Result< SplinePolyline > polylineResult(int sampleCount=64, bool uniformByDistance=true, int samplesPerSegment=24) const
Build an owning sampled polyline without depending on a graphics module.
Result< void > setPointChunkBreakResult(int index, bool disconnected)
Start or reconnect an open-path chunk before one non-first point.
Result< SplinePath > chunkPathResult(int chunk) const
Copy one independently connected chunk as an owning path.
Result< SplineSample > evaluateDistanceResult(float distance, int samplesPerSegment=24) const
Evaluate by world-space distance using a bounded arc-length table.
Result< void > addPointResult(const SplinePoint &point)
Append an owning control point after validating finite coordinates.
Result< void > setPointProfileResult(int index, float rollDegrees, float scaleX, float scaleY)
Atomically replace per-point roll and cross-section scale after finite positive validation.
Result< SplineSample > travelResult(float distance, std::string_view wrapMode="clamp", int samplesPerSegment=24) const
Evaluate distance-driven travel using clamp, loop, or pingPong wrapping without reading wall time.
Result< SplineDistribution > distributeResult(int instanceCount, bool includeEnd=true, int samplesPerSegment=24) const
Produce owning arc-length-uniform samples for instance distribution.
Result< SplineSample > closestPointResult(float x, float y, float z, int samplesPerSegment=32) const
Return the sampled closest path location to a point.
Result< float > lengthResult(int samplesPerSegment=24) const
Return approximate total world-space length.
int segmentCount() const noexcept
Return segment count.
Result< SplineFrameSample > travelFrameResult(float distance, std::string_view wrapMode="clamp", int samplesPerSegment=24) const
Evaluate distance-driven travel with a transported orientation frame.
Owning sampled polyline for renderer-neutral runtime visualization adapters.
Definition SplinePath.h:61
Result< SplineSample > chunkPointResult(int chunk, int index) const
Return one point inside one disconnected chunk.
int chunkCount() const noexcept
Return disconnected polyline chunk count.
Definition SplinePath.h:72
int count() const noexcept
Return sampled point count.
Result< SplineSample > pointResult(int index) const
Return one point sample or a structured range error.
int chunkPointCount(int chunk) const noexcept
Return point count for one chunk, or zero for an invalid index.
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Definition HexMetrics.h:76
double sample(const Heightmap &map, double u, double v)
Sample.
double dot(const Vec2 &a, const Vec2 &b)
Dot.
Definition UrbanTypes.h:38
double cross(const Vec2 &a, const Vec2 &b)
Cross.
Definition UrbanTypes.h:36
One spline anchor with incoming and outgoing handle offsets.
Definition SplinePath.h:15
Evaluated spline position and normalized tangent.
Definition SplinePath.h:26