载入中...
搜索中...
未找到
SplinePathDocument.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <cmath>
5#include <set>
6#include <utility>
7
8namespace eve::procgen_editing {
9namespace {
10
11const EditorValue* field(const EditorValue& value, const char* key) {
12 const auto* object = value.getIf<EditorValue::Object>();
13 if (!object) return nullptr;
14 const auto found = object->find(key);
15 return found == object->end() ? nullptr : &found->second;
16}
17
18EditorValue pointValue(const SplinePathControlPoint& point) {
19 return EditorValue::Object{{"id", point.id.value()},
20 {"order", point.order},
21 {"x", point.x},
22 {"y", point.y},
23 {"z", point.z},
24 {"inX", point.inX},
25 {"inY", point.inY},
26 {"inZ", point.inZ},
27 {"outX", point.outX},
28 {"outY", point.outY},
29 {"outZ", point.outZ},
30 {"rollDegrees", point.rollDegrees},
31 {"scaleX", point.scaleX},
32 {"scaleY", point.scaleY},
33 {"breakBefore", point.breakBefore},
34 {"pitchDegrees", point.pitchDegrees},
35 {"yawDegrees", point.yawDegrees}};
36}
37
38EditorValue settingsValue(const SplinePathSettings& settings) {
39 return EditorValue::Object{{"kind", settings.kind}, {"closed", settings.closed}};
40}
41
42Result<SplinePathControlPoint> parsePoint(const EditorValue& value) {
43 const auto string = [&](const char* name) {
44 const auto* entry = field(value, name);
45 return entry ? entry->getIf<std::string>() : nullptr;
46 };
47 const auto integer = [&](const char* name) {
48 const auto* entry = field(value, name);
49 return entry ? entry->getIf<std::int64_t>() : nullptr;
50 };
51 const auto number = [&](const char* name) {
52 const auto* entry = field(value, name);
53 return entry ? entry->getIf<double>() : nullptr;
54 };
55 const auto* id = string("id");
56 const auto* order = integer("order");
57 const auto* x = number("x");
58 const auto* y = number("y");
59 const auto* z = number("z");
60 const auto* inX = number("inX");
61 const auto* inY = number("inY");
62 const auto* inZ = number("inZ");
63 const auto* outX = number("outX");
64 const auto* outY = number("outY");
65 const auto* outZ = number("outZ");
66 const auto* rollValue = field(value, "rollDegrees");
67 const auto* scaleXValue = field(value, "scaleX");
68 const auto* scaleYValue = field(value, "scaleY");
69 const auto* breakValue = field(value, "breakBefore");
70 const auto* pitchValue = field(value, "pitchDegrees");
71 const auto* yawValue = field(value, "yawDegrees");
72 const auto* roll = rollValue ? rollValue->getIf<double>() : nullptr;
73 const auto* scaleX = scaleXValue ? scaleXValue->getIf<double>() : nullptr;
74 const auto* scaleY = scaleYValue ? scaleYValue->getIf<double>() : nullptr;
75 const double resolvedRoll = roll ? *roll : 0.0;
76 const double resolvedScaleX = scaleX ? *scaleX : 1.0;
77 const double resolvedScaleY = scaleY ? *scaleY : 1.0;
78 const auto* breakBefore = breakValue ? breakValue->getIf<bool>() : nullptr;
79 const auto* pitch = pitchValue ? pitchValue->getIf<double>() : nullptr;
80 const auto* yaw = yawValue ? yawValue->getIf<double>() : nullptr;
81 const double resolvedPitch = pitch ? *pitch : 0.0;
82 const double resolvedYaw = yaw ? *yaw : 0.0;
83 if (!id || id->empty() || !order || !x || !y || !z || !inX || !inY || !inZ || !outX || !outY || !outZ ||
84 !std::isfinite(*x) || !std::isfinite(*y) || !std::isfinite(*z) || !std::isfinite(*inX) ||
85 !std::isfinite(*inY) || !std::isfinite(*inZ) || !std::isfinite(*outX) || !std::isfinite(*outY) ||
86 !std::isfinite(*outZ) || !std::isfinite(resolvedRoll) || !std::isfinite(resolvedScaleX) ||
87 !std::isfinite(resolvedScaleY) || !std::isfinite(resolvedPitch) || !std::isfinite(resolvedYaw) ||
88 resolvedScaleX <= 0.0 || resolvedScaleY <= 0.0)
89 return eve::editing::failed<SplinePathControlPoint>(
90 EditorStatus::Rejected, RuleId("editor.spline.invalid-point"),
91 "Spline point requires a stable id, order and finite coordinates");
92 return eve::editing::applied<SplinePathControlPoint>(
93 {StableId(*id), *order, *x, *y, *z, *inX, *inY, *inZ, *outX, *outY, *outZ, resolvedRoll, resolvedScaleX,
94 resolvedScaleY, breakBefore ? *breakBefore : false, resolvedPitch, resolvedYaw});
95}
96
97Result<SplinePathSettings> parseSettings(const EditorValue& value) {
98 const auto* kindValue = field(value, "kind");
99 const auto* closedValue = field(value, "closed");
100 const auto* kind = kindValue ? kindValue->getIf<std::string>() : nullptr;
101 const auto* closed = closedValue ? closedValue->getIf<bool>() : nullptr;
102 const std::set<std::string> kinds{"linear", "catmullRom", "bezier"};
103 if (!kind || !kinds.contains(*kind) || !closed)
104 return eve::editing::failed<SplinePathSettings>(EditorStatus::Rejected,
105 RuleId("editor.spline.invalid-settings"),
106 "Spline settings require a supported kind and closure flag");
107 return eve::editing::applied<SplinePathSettings>({*kind, *closed});
108}
109
110DomainOperation operation(const char* type, const char* inverseType, const std::string& target, EditorValue payload,
111 EditorValue inverse, const StableId& affected) {
112 DomainOperation result;
113 result.type = type;
114 result.inverseType = inverseType;
115 result.target = TargetId(target);
116 result.payload = std::move(payload);
117 result.inverse = std::move(inverse);
118 result.hasInverse = true;
119 result.affectedObjects.push_back({TargetId(target), affected.value(), 0});
120 return result;
121}
122
123} // namespace
124
125SplinePathDocument::SplinePathDocument(std::string id) : id_(std::move(id)) {}
126
128 return {TargetId(id_),
129 "spline-path-document",
131 false,
133}
134
137 ? static_cast<ISplinePathDocumentEditTarget*>(this)
138 : nullptr;
139}
140
142 if (value.target != TargetId(id_))
143 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.spline.target-mismatch"),
144 "Spline operation targets another document");
145 if (value.type == "spline.snapshot.replace.v1") {
146 SplinePathDocument candidate(id_);
147 auto loaded = candidate.loadSnapshot(value.payload);
148 if (!loaded.ok())
149 return eve::editing::failed<void>(loaded.code(), RuleId("editor.spline.invalid-replacement"),
150 loaded.status().describe());
151 candidate.setRevision(revisionValue() + 1);
152 candidate.widenDirty(0, 0);
153 *this = std::move(candidate);
154 return eve::editing::applied<void>();
155 }
156 if (value.type == "spline.point.set.v1") {
157 auto parsed = parsePoint(value.payload);
158 if (!parsed.ok())
159 return eve::editing::failed<void>(parsed.code(), RuleId("editor.spline.invalid-point"),
160 "Invalid spline point");
161 points_[parsed.value().id] = std::move(parsed.value());
162 } else if (value.type == "spline.point.delete.v1") {
163 auto parsed = parsePoint(value.payload);
164 if (!parsed.ok() || !points_.erase(parsed.value().id))
165 return eve::editing::failed<void>(EditorStatus::NotFound, RuleId("editor.spline.point-not-found"),
166 "Spline point was not found");
167 } else if (value.type == "spline.settings.set.v1") {
168 auto parsed = parseSettings(value.payload);
169 if (!parsed.ok())
170 return eve::editing::failed<void>(parsed.code(), RuleId("editor.spline.invalid-settings"),
171 "Invalid spline settings");
172 settings_ = std::move(parsed.value());
173 } else {
174 return eve::editing::failed<void>(EditorStatus::Unsupported, RuleId("editor.spline.operation-unsupported"),
175 "Spline operation is unsupported");
176 }
177 bumpRevision();
178 widenDirty(0, 0);
179 return eve::editing::applied<void>();
180}
181
183 const StableId& point) const {
184 const auto ordered = points();
185 std::vector<std::pair<int, int>> segments;
186 for (int i = 0; i + 1 < static_cast<int>(ordered.size()); ++i)
187 if (!ordered[static_cast<std::size_t>(i + 1)].breakBefore) segments.emplace_back(i, i + 1);
188 if (settings_.closed && ordered.size() > 1u) segments.emplace_back(static_cast<int>(ordered.size()) - 1, 0);
189 const int segmentCount = static_cast<int>(segments.size());
190 if (point.empty() || points_.contains(point) || segment < 0 || segment >= segmentCount || !std::isfinite(t) ||
191 t <= 0.0 || t >= 1.0)
192 return eve::editing::failed<DomainOperation>(
193 EditorStatus::Rejected, RuleId("editor.spline.insert-invalid"),
194 "Spline insertion requires a unique id, valid segment and t in (0,1)");
195 auto runtime = compilePath();
196 if (!runtime.ok())
197 return eve::editing::failed<DomainOperation>(runtime.code(), RuleId("editor.spline.insert-incomplete"),
198 runtime.status().describe());
199 auto sampled = runtime.value().evaluateResult((static_cast<float>(segment) + static_cast<float>(t)) /
200 static_cast<float>(segmentCount));
201 if (!sampled.ok())
202 return eve::editing::failed<DomainOperation>(
203 EditorStatus::Rejected, RuleId("editor.spline.insert-sample-failed"), sampled.status().describe());
204 SplinePathDocument candidate(*this);
205 auto candidatePoints = ordered;
206 const int firstIndex = segments[static_cast<std::size_t>(segment)].first;
207 const int nextIndex = segments[static_cast<std::size_t>(segment)].second;
208 SplinePathControlPoint inserted;
209 inserted.id = point;
210 inserted.x = sampled.value().x;
211 inserted.y = sampled.value().y;
212 inserted.z = sampled.value().z;
213 inserted.rollDegrees = sampled.value().rollDegrees;
214 inserted.scaleX = sampled.value().scaleX;
215 inserted.scaleY = sampled.value().scaleY;
216 if (settings_.kind == "bezier") {
217 auto& first = candidatePoints[static_cast<std::size_t>(firstIndex)];
218 auto& second = candidatePoints[static_cast<std::size_t>(nextIndex)];
219 const auto lerp = [t](double a, double b) { return a + (b - a) * t; };
220 const double q0x = lerp(first.x, first.x + first.outX), q0y = lerp(first.y, first.y + first.outY),
221 q0z = lerp(first.z, first.z + first.outZ);
222 const double q1x = lerp(first.x + first.outX, second.x + second.inX),
223 q1y = lerp(first.y + first.outY, second.y + second.inY),
224 q1z = lerp(first.z + first.outZ, second.z + second.inZ);
225 const double q2x = lerp(second.x + second.inX, second.x), q2y = lerp(second.y + second.inY, second.y),
226 q2z = lerp(second.z + second.inZ, second.z);
227 const double r0x = lerp(q0x, q1x), r0y = lerp(q0y, q1y), r0z = lerp(q0z, q1z);
228 const double r1x = lerp(q1x, q2x), r1y = lerp(q1y, q2y), r1z = lerp(q1z, q2z);
229 inserted.x = lerp(r0x, r1x);
230 inserted.y = lerp(r0y, r1y);
231 inserted.z = lerp(r0z, r1z);
232 first.outX = q0x - first.x;
233 first.outY = q0y - first.y;
234 first.outZ = q0z - first.z;
235 inserted.inX = r0x - inserted.x;
236 inserted.inY = r0y - inserted.y;
237 inserted.inZ = r0z - inserted.z;
238 inserted.outX = r1x - inserted.x;
239 inserted.outY = r1y - inserted.y;
240 inserted.outZ = r1z - inserted.z;
241 second.inX = q2x - second.x;
242 second.inY = q2y - second.y;
243 second.inZ = q2z - second.z;
244 }
245 const int insertionIndex = nextIndex == 0 ? static_cast<int>(candidatePoints.size()) : nextIndex;
246 candidatePoints.insert(candidatePoints.begin() + insertionIndex, inserted);
247 candidate.points_.clear();
248 for (std::size_t i = 0; i < candidatePoints.size(); ++i) {
249 candidatePoints[i].order = static_cast<std::int64_t>(i);
250 candidate.points_[candidatePoints[i].id] = candidatePoints[i];
251 }
252 return eve::editing::applied<DomainOperation>(operation("spline.snapshot.replace.v1", "spline.snapshot.replace.v1",
253 id_, candidate.snapshotValue(), snapshotValue(), point));
254}
255
257 const auto found = points_.find(point);
258 if (found == points_.end())
259 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.spline.snap-point-missing"),
260 "Spline point was not found");
261 if (!std::isfinite(gridSize) || gridSize <= 0.0)
262 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.spline.snap-grid-invalid"),
263 "Spline snap grid size must be finite and positive");
264 auto snapped = found->second;
265 snapped.x = std::round(snapped.x / gridSize) * gridSize;
266 snapped.y = std::round(snapped.y / gridSize) * gridSize;
267 snapped.z = std::round(snapped.z / gridSize) * gridSize;
268 return makeSetPoint(snapped);
269}
270
272 if (!std::isfinite(gridSize) || gridSize <= 0.0)
273 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.spline.snap-grid-invalid"),
274 "Spline snap grid size must be finite and positive");
275 SplinePathDocument candidate(*this);
276 for (auto& [unused, point] : candidate.points_) {
277 (void)unused;
278 point.x = std::round(point.x / gridSize) * gridSize;
279 point.y = std::round(point.y / gridSize) * gridSize;
280 point.z = std::round(point.z / gridSize) * gridSize;
281 }
282 return eve::editing::applied<DomainOperation>(operation("spline.snapshot.replace.v1", "spline.snapshot.replace.v1",
283 id_, candidate.snapshotValue(), snapshotValue(),
284 StableId("snap-all")));
285}
286
288 if (points_.size() < 2u)
289 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.spline.flip-incomplete"),
290 "Spline direction requires at least two points");
291 std::vector<std::vector<SplinePathControlPoint>> chunks(1);
292 for (const auto& point : points()) {
293 if (point.breakBefore) chunks.emplace_back();
294 chunks.back().push_back(point);
295 }
296 std::reverse(chunks.begin(), chunks.end());
297 std::vector<SplinePathControlPoint> reversed;
298 for (auto& chunk : chunks) {
299 std::reverse(chunk.begin(), chunk.end());
300 for (std::size_t i = 0; i < chunk.size(); ++i) {
301 chunk[i].breakBefore = !reversed.empty() && i == 0;
302 reversed.push_back(chunk[i]);
303 }
304 }
305 SplinePathDocument candidate(*this);
306 candidate.points_.clear();
307 for (std::size_t i = 0; i < reversed.size(); ++i) {
308 auto& point = reversed[i];
309 point.order = static_cast<std::int64_t>(i);
310 std::swap(point.inX, point.outX);
311 std::swap(point.inY, point.outY);
312 std::swap(point.inZ, point.outZ);
313 candidate.points_[point.id] = point;
314 }
315 return eve::editing::applied<DomainOperation>(operation("spline.snapshot.replace.v1", "spline.snapshot.replace.v1",
316 id_, candidate.snapshotValue(), snapshotValue(),
317 reversed.front().id));
318}
319
321 if (settings_.closed && disconnected)
322 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.spline.chunk-break-closed"),
323 "Closed splines cannot contain disconnected chunks");
324 auto ordered = points();
325 const auto found =
326 std::find_if(ordered.begin(), ordered.end(), [&](const auto& value) { return value.id == point; });
327 if (found == ordered.end() || found == ordered.begin())
328 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound,
329 RuleId("editor.spline.chunk-break-point-invalid"),
330 "Chunk boundary requires an existing non-first point");
331 const auto index = static_cast<std::size_t>(found - ordered.begin());
332 if (disconnected && !found->breakBefore) {
333 std::size_t chunkStart = index;
334 while (chunkStart > 0u && !ordered[chunkStart].breakBefore) --chunkStart;
335 std::size_t chunkEnd = index;
336 while (chunkEnd + 1u < ordered.size() && !ordered[chunkEnd + 1u].breakBefore) ++chunkEnd;
337 if (index - chunkStart < 2u || chunkEnd - index + 1u < 2u)
338 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
339 RuleId("editor.spline.chunk-break-singleton"),
340 "Splitting must leave at least two points in each chunk");
341 }
342 auto updated = *found;
343 updated.breakBefore = disconnected;
344 return makeSetPoint(updated);
345}
346
348 const SplinePathControlPoint& second) const {
349 if (settings_.closed)
350 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
351 RuleId("editor.spline.append-chunk-closed"),
352 "Closed splines cannot append disconnected chunks");
353 if (first.id.empty() || second.id.empty() || first.id == second.id || points_.contains(first.id) ||
354 points_.contains(second.id))
355 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
356 RuleId("editor.spline.append-chunk-id-invalid"),
357 "Appended chunk points require two new distinct stable ids");
358 auto parsedFirst = parsePoint(pointValue(first));
359 auto parsedSecond = parsePoint(pointValue(second));
360 if (!parsedFirst.ok() || !parsedSecond.ok())
361 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
362 RuleId("editor.spline.append-chunk-point-invalid"),
363 "Appended chunk points must be finite and valid");
364 SplinePathDocument candidate(*this);
365 auto ordered = candidate.points();
366 auto appendedFirst = parsedFirst.value();
367 auto appendedSecond = parsedSecond.value();
368 appendedFirst.order = static_cast<std::int64_t>(ordered.size());
369 appendedFirst.breakBefore = !ordered.empty();
370 appendedSecond.order = appendedFirst.order + 1;
371 appendedSecond.breakBefore = false;
372 candidate.points_[appendedFirst.id] = appendedFirst;
373 candidate.points_[appendedSecond.id] = appendedSecond;
374 return eve::editing::applied<DomainOperation>(operation("spline.snapshot.replace.v1", "spline.snapshot.replace.v1",
375 id_, candidate.snapshotValue(), snapshotValue(),
376 appendedFirst.id));
377}
378
380 if (chunk < 0)
381 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.spline.chunk-not-found"),
382 "Spline chunk was not found");
383 auto ordered = points();
384 std::vector<std::vector<SplinePathControlPoint>> chunks;
385 for (const auto& point : ordered) {
386 if (chunks.empty() || point.breakBefore) chunks.emplace_back();
387 chunks.back().push_back(point);
388 }
389 if (static_cast<std::size_t>(chunk) >= chunks.size())
390 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.spline.chunk-not-found"),
391 "Spline chunk was not found");
392 chunks.erase(chunks.begin() + chunk);
393 SplinePathDocument candidate(*this);
394 candidate.points_.clear();
395 std::int64_t order = 0;
396 for (std::size_t chunkIndex = 0; chunkIndex < chunks.size(); ++chunkIndex) {
397 for (std::size_t pointIndex = 0; pointIndex < chunks[chunkIndex].size(); ++pointIndex) {
398 auto point = chunks[chunkIndex][pointIndex];
399 point.order = order++;
400 point.breakBefore = chunkIndex > 0u && pointIndex == 0u;
401 candidate.points_[point.id] = point;
402 }
403 }
404 return eve::editing::applied<DomainOperation>(operation("spline.snapshot.replace.v1", "spline.snapshot.replace.v1",
405 id_, candidate.snapshotValue(), snapshotValue(),
406 StableId("delete-chunk")));
407}
408
410 double yawDegrees, double rollDegrees) const {
411 const auto found = points_.find(point);
412 if (found == points_.end())
413 return eve::editing::failed<DomainOperation>(
414 EditorStatus::NotFound, RuleId("editor.spline.rotation-point-missing"), "Spline point was not found");
415 if (!std::isfinite(pitchDegrees) || !std::isfinite(yawDegrees) || !std::isfinite(rollDegrees))
416 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.spline.rotation-invalid"),
417 "Spline point rotation must be finite");
418 auto updated = found->second;
419 updated.pitchDegrees = pitchDegrees;
420 updated.yawDegrees = yawDegrees;
421 updated.rollDegrees = rollDegrees;
422 return makeSetPoint(updated);
423}
424
428
430 const auto ordered = points();
431 const auto found =
432 std::find_if(ordered.begin(), ordered.end(), [&](const auto& value) { return value.id == point; });
433 if (found == ordered.end())
434 return eve::editing::failed<DomainOperation>(
435 EditorStatus::NotFound, RuleId("editor.spline.center-point-missing"), "Spline point was not found");
436 const auto index = static_cast<std::size_t>(found - ordered.begin());
437 if (index == 0u || index + 1u >= ordered.size() || found->breakBefore || ordered[index + 1u].breakBefore)
438 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
439 RuleId("editor.spline.center-point-endpoint"),
440 "Only an interior point of one connected chunk can be centered");
441 auto updated = *found;
442 updated.x = (ordered[index - 1u].x + ordered[index + 1u].x) * 0.5;
443 updated.y = (ordered[index - 1u].y + ordered[index + 1u].y) * 0.5;
444 updated.z = (ordered[index - 1u].z + ordered[index + 1u].z) * 0.5;
445 return makeSetPoint(updated);
446}
447
449 if (axis != "x" && axis != "y" && axis != "z")
450 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
451 RuleId("editor.spline.mirror-axis-invalid"),
452 "Spline mirror axis must be x, y, or z");
453 SplinePathDocument candidate(*this);
454 for (auto& [unused, point] : candidate.points_) {
455 (void)unused;
456 double* position = axis == "x" ? &point.x : axis == "y" ? &point.y : &point.z;
457 double* incoming = axis == "x" ? &point.inX : axis == "y" ? &point.inY : &point.inZ;
458 double* outgoing = axis == "x" ? &point.outX : axis == "y" ? &point.outY : &point.outZ;
459 *position = -*position;
460 *incoming = -*incoming;
461 *outgoing = -*outgoing;
462 }
463 return eve::editing::applied<DomainOperation>(operation("spline.snapshot.replace.v1", "spline.snapshot.replace.v1",
464 id_, candidate.snapshotValue(), snapshotValue(),
465 StableId(std::string("mirror-") + std::string(axis))));
466}
467
468std::unique_ptr<IDomainOperationTarget> SplinePathDocument::cloneDomainState() const {
469 return std::make_unique<SplinePathDocument>(*this);
470}
471
472Result<void> SplinePathDocument::commitDomainState(std::unique_ptr<IDomainOperationTarget> candidate) {
473 auto* spline = dynamic_cast<SplinePathDocument*>(candidate.get());
474 if (!spline || spline->id_ != id_)
475 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.spline.invalid-candidate"),
476 "Spline candidate does not match this document");
477 *this = std::move(*spline);
478 return eve::editing::applied<void>();
479}
480
482 auto parsed = parsePoint(pointValue(point));
483 if (!parsed.ok())
484 return eve::editing::failed<DomainOperation>(parsed.code(), RuleId("editor.spline.invalid-point"),
485 "Invalid spline point");
486 const auto found = points_.find(point.id);
487 const bool exists = found != points_.end();
488 return eve::editing::applied<DomainOperation>(operation(
489 "spline.point.set.v1", exists ? "spline.point.set.v1" : "spline.point.delete.v1", id_,
490 pointValue(parsed.value()), exists ? pointValue(found->second) : pointValue(parsed.value()), point.id));
491}
492
494 const auto found = points_.find(point);
495 if (found == points_.end())
496 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.spline.point-not-found"),
497 "Spline point was not found");
498 return eve::editing::applied<DomainOperation>(operation("spline.point.delete.v1", "spline.point.set.v1", id_,
499 pointValue(found->second), pointValue(found->second),
500 point));
501}
502
504 auto parsed = parseSettings(settingsValue(settings));
505 if (!parsed.ok())
506 return eve::editing::failed<DomainOperation>(parsed.code(), RuleId("editor.spline.invalid-settings"),
507 "Invalid spline settings");
508 if (settings.closed &&
509 std::any_of(points_.begin(), points_.end(), [](const auto& entry) { return entry.second.breakBefore; }))
510 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.spline.closed-has-breaks"),
511 "Reconnect all spline chunks before closing the path");
512 return eve::editing::applied<DomainOperation>(operation("spline.settings.set.v1", "spline.settings.set.v1", id_,
513 settingsValue(parsed.value()), settingsValue(settings_),
514 StableId("settings")));
515}
516
517std::vector<SplinePathControlPoint> SplinePathDocument::points() const {
518 std::vector<SplinePathControlPoint> result;
519 result.reserve(points_.size());
520 for (const auto& [id, point] : points_) {
521 static_cast<void>(id);
522 result.push_back(point);
523 }
524 std::sort(result.begin(), result.end(), [](const auto& left, const auto& right) {
525 return left.order == right.order ? left.id < right.id : left.order < right.order;
526 });
527 return result;
528}
529
532 auto kind = path.setKindResult(settings_.kind);
533 if (!kind.ok())
534 return eve::editing::failed<procgen::SplinePath>(EditorStatus::Rejected, RuleId("editor.spline.compile-kind"),
535 kind.status().describe());
536 for (const auto& point : points()) {
537 auto added = path.addPointResult(
538 {static_cast<float>(point.x), static_cast<float>(point.y), static_cast<float>(point.z),
539 static_cast<float>(point.inX), static_cast<float>(point.inY), static_cast<float>(point.inZ),
540 static_cast<float>(point.outX), static_cast<float>(point.outY), static_cast<float>(point.outZ),
541 static_cast<float>(point.rollDegrees), static_cast<float>(point.scaleX), static_cast<float>(point.scaleY),
542 point.breakBefore, static_cast<float>(point.pitchDegrees), static_cast<float>(point.yawDegrees)});
543 if (!added.ok())
544 return eve::editing::failed<procgen::SplinePath>(
545 EditorStatus::Rejected, RuleId("editor.spline.compile-point"), added.status().describe());
546 }
547 path.setClosed(settings_.closed);
548 auto ready = path.evaluateResult(0.f);
549 if (!ready.ok())
550 return eve::editing::failed<procgen::SplinePath>(
551 EditorStatus::Rejected, RuleId("editor.spline.compile-incomplete"), ready.status().describe());
552 return eve::editing::applied<procgen::SplinePath>(std::move(path));
553}
554
557 for (const auto& point : this->points()) points.push_back(pointValue(point));
558 return EditorValue::Object{{"schema", "eve.procgen.splinePath"},
559 {"schemaVersion", std::int64_t{1}},
560 {"kind", settings_.kind},
561 {"closed", settings_.closed},
562 {"points", std::move(points)}};
563}
564
566 const auto* schemaValue = field(snapshot, "schema");
567 const auto* versionValue = field(snapshot, "schemaVersion");
568 const auto* kindValue = field(snapshot, "kind");
569 const auto* closedValue = field(snapshot, "closed");
570 const auto* pointsValue = field(snapshot, "points");
571 const auto* schema = schemaValue ? schemaValue->getIf<std::string>() : nullptr;
572 const auto* version = versionValue ? versionValue->getIf<std::int64_t>() : nullptr;
573 const auto* points = pointsValue ? pointsValue->getIf<EditorValue::Array>() : nullptr;
574 if (!schema || *schema != "eve.procgen.splinePath" || !version || *version != 1 || !kindValue || !closedValue ||
575 !points)
576 return eve::editing::failed<void>(EditorStatus::Unsupported, RuleId("editor.spline.invalid-snapshot"),
577 "Spline snapshot schema is unsupported");
578 auto settings = parseSettings(EditorValue::Object{{"kind", *kindValue}, {"closed", *closedValue}});
579 if (!settings.ok())
580 return eve::editing::failed<void>(settings.code(), RuleId("editor.spline.invalid-snapshot-settings"),
581 "Spline snapshot settings are invalid");
582 SplinePathDocument candidate(id_);
583 candidate.settings_ = std::move(settings.value());
584 for (const auto& value : *points) {
585 auto parsed = parsePoint(value);
586 if (!parsed.ok() || candidate.points_.contains(parsed.value().id))
587 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.spline.invalid-snapshot-point"),
588 "Spline snapshot contains invalid or duplicate points");
589 candidate.points_[parsed.value().id] = std::move(parsed.value());
590 }
591 if (candidate.settings_.closed && std::any_of(candidate.points_.begin(), candidate.points_.end(),
592 [](const auto& entry) { return entry.second.breakBefore; }))
593 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.spline.invalid-snapshot-breaks"),
594 "Closed spline snapshot cannot contain disconnected chunks");
595 candidate.setRevision(revisionValue() + 1);
596 candidate.clearDirtyRegion();
597 *this = std::move(candidate);
598 return eve::editing::applied<void>();
599}
600
602 SplinePathDocument document("spline-preview");
603 auto loaded = document.loadSnapshot(snapshot);
604 if (!loaded.ok())
605 return eve::editing::failed<procgen::SplinePath>(loaded.code(), RuleId("editor.spline.snapshot-compile"),
606 loaded.status().describe());
607 return document.compilePath();
608}
609
610} // namespace eve::procgen_editing
LogicalId target
ActionParameterOperation operation
double value
Value::Object payload
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
std::uint32_t key
float u
Definition Grass.cpp:233
HexVec3 left
HexVec3 right
std::int32_t second
std::int32_t first
TokenKind kind
std::array< float, 3 > position
bool exists
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::vector< std::int32_t > order
std::array< std::uint64_t, kPixelChunkSize *kPixelChunkSize > updated
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
float t
SplinePath spline
double number
bool found
std::string string
TerrainThermalSettings settings
uint32_t index
glm::vec3 point
Move-only operation result carrying either a value or Status.
Definition Result.h:155
UUID-backed identifier adapter for legacy textual boundaries.
Definition Identity.h:314
const std::string & value() const noexcept
Returns the exact legacy or canonical compatibility spelling.
Definition Identity.h:362
void setRevision(Revision revision)
Adopt a revision observed elsewhere, without advancing it.
void bumpRevision()
Advance the revision without touching the dirty region.
void clearDirtyRegion() override
Discard the dirty region; the revision does not change.
Revision revisionValue() const noexcept
Return the revision as the shared protocol revision type.
void widenDirty(int x, int y)
Widen the dirty region without advancing the revision.
void widenDirty()
Mark an unknown area dirty without advancing the revision.
eve::OptionalRef< C > capability()
Query a capability through its interface-owned stable identity.
Deterministic owning value tree shared by authoring hosts.
std::vector< Value > Array
std::map< std::string, Value > Object
const T * getIf() const
Return an immediately borrowed typed value.
Owning multi-segment 3D spline with deterministic bounded sampling.
Definition SplinePath.h:91
Result< void > setKindResult(std::string_view kind)
Select linear, catmullRom, quadraticBezier, or cubic bezier interpolation.
UI-neutral reversible spline editing capability.
static CapabilityId editingCapabilityId()
Return the stable capability id.
Owning spline authoring document with transactional operations and schema-versioned snapshots.
Result< DomainOperation > makeSetPoint(const SplinePathControlPoint &point) const override
Make set point.
std::unique_ptr< IDomainOperationTarget > cloneDomainState() const override
Clones domain state.
Result< DomainOperation > makeDeletePoint(const StableId &point) const override
Make delete point.
Result< DomainOperation > makeSetPointRotation(const StableId &point, double pitchDegrees, double yawDegrees, double rollDegrees) const override
Make set point rotation.
Result< DomainOperation > makeCenterPoint(const StableId &point) const override
Make center point.
Result< void > commitDomainState(std::unique_ptr< IDomainOperationTarget > candidate) override
Commits domain state.
Result< DomainOperation > makeAppendChunk(const SplinePathControlPoint &first, const SplinePathControlPoint &second) const override
Make append chunk.
std::vector< SplinePathControlPoint > points() const
Return points in deterministic order and stable-id tie order.
Result< DomainOperation > makeMirrorAxis(std::string_view axis) const override
Make mirror axis.
Result< DomainOperation > makeResetPointRotation(const StableId &point) const override
Make reset point rotation.
Result< DomainOperation > makeDeleteChunk(int chunk) const override
Make delete chunk.
Result< DomainOperation > makeFlipDirection() const override
Make flip direction.
Result< DomainOperation > makeInsertPointAt(int segment, double t, const StableId &point) const override
Make insert point at.
SplinePathDocument(std::string id)
Construct an empty document with a stable target id.
void * queryCapability(const CapabilityId &capability) override
Query the spline editing capability.
EditorValue snapshotValue() const
Capture schema eve.procgen.splinePath version one.
Result< DomainOperation > makeSetChunkBreak(const StableId &point, bool disconnected) const override
Make set chunk break.
Result< void > applyDomainOperation(const DomainOperation &operation) override
Applies domain operation.
TargetDescriptor describe() const override
Describe.
Result< DomainOperation > makeSnapPoint(const StableId &point, double gridSize) const override
Make snap point.
Result< void > loadSnapshot(const EditorValue &snapshot)
Atomically load a validated version-one snapshot.
Result< DomainOperation > makeSetSettings(const SplinePathSettings &settings) const override
Make set settings.
Result< procgen::SplinePath > compilePath() const
Compile an owning runtime spline without retaining document references.
Result< DomainOperation > makeSnapAll(double gridSize) const override
Make snap all.
const SplinePathSettings & settings() const noexcept
Return current interpolation and closure settings.
editing::DomainOperation DomainOperation
StrongId< TargetIdTag > TargetId
Definition EditingIds.h:57
StrongId< StableIdTag > StableId
Definition EditingIds.h:64
StrongId< RuleIdTag > RuleId
Definition EditingIds.h:60
const EditorValue * field(const EditorValue &value, const char *name)
Result< procgen::SplinePath > compileSplinePathSnapshot(const EditorValue &snapshot)
Compile a version-one spline snapshot for isolated graph preview.
int64_t integer(const RuntimeTensor &v, size_t i=0)
Integer.
DomainOperation public API.
Stable discovery metadata exposed by every editable target.
Stable authoring control point for a 3D spline path.
Spline authoring settings stored independently from runtime caches.