载入中...
搜索中...
未找到
SceneEditorSession.cpp
浏览该文件的文档.
2
7
8#include <algorithm>
9#include <array>
10#include <cmath>
11#include <optional>
12#include <stdexcept>
13#include <unordered_map>
14#include <unordered_set>
15
16#include <glm/gtc/matrix_transform.hpp>
17#include <glm/gtc/quaternion.hpp>
18#include <glm/gtx/matrix_decompose.hpp>
19#include <glm/gtx/quaternion.hpp>
20
21namespace eve::scene_editor {
22namespace {
23struct SceneGraphNode {
24 std::string parent;
25 scene_editing::SceneTransformValue local;
26};
27
28using SceneGraph = std::unordered_map<std::string, SceneGraphNode>;
29
30const editing::Value* valueField(const editing::Value& value, const char* key) {
31 const auto* object = value.getIf<editing::Value::Object>();
32 if (!object) return nullptr;
33 const auto found = object->find(key);
34 return found == object->end() ? nullptr : &found->second;
35}
36
37editing::Result<SceneGraph> sceneGraphOf(const editing::Value& snapshot) {
38 const auto* root = snapshot.getIf<editing::Value::Object>();
39 if (!root)
40 return editing::failed<SceneGraph>(editing::Status::Failed, editing::RuleId("scene.physics-placement.snapshot"),
41 "Scene snapshot is not an object");
42 const auto found = root->find("objects");
43 const auto* objects = found != root->end() ? found->second.getIf<editing::Value::Array>() : nullptr;
44 if (!objects)
45 return editing::failed<SceneGraph>(editing::Status::Failed, editing::RuleId("scene.physics-placement.snapshot"),
46 "Scene snapshot has no objects array");
47 SceneGraph graph;
48 for (const auto& value : *objects) {
49 const auto* idValue = valueField(value, "id");
50 const auto* parentValue = valueField(value, "parent");
51 const auto* transformValue = valueField(value, "transform");
52 const auto* id = idValue ? idValue->getIf<std::string>() : nullptr;
53 const auto* parent = parentValue ? parentValue->getIf<std::string>() : nullptr;
54 const auto* transform = transformValue ? transformValue->getIf<editing::Value::Object>() : nullptr;
55 if (!id || !parent || !transform)
56 return editing::failed<SceneGraph>(editing::Status::Failed,
57 editing::RuleId("scene.physics-placement.snapshot-object"),
58 "Scene snapshot contains an incomplete object");
59 SceneGraphNode node;
60 node.parent = *parent;
61 const auto assign = [&](const char* key, double& destination) {
62 const auto entry = transform->find(key);
63 const auto* number = entry != transform->end() ? entry->second.getIf<double>() : nullptr;
64 if (!number) return false;
65 destination = *number;
66 return true;
67 };
68 if (!assign("x", node.local.x) || !assign("y", node.local.y) || !assign("z", node.local.z) ||
69 !assign("rotationX", node.local.rotationX) || !assign("rotationY", node.local.rotationY) ||
70 !assign("rotationZ", node.local.rotationZ) || !assign("scaleX", node.local.scaleX) ||
71 !assign("scaleY", node.local.scaleY) || !assign("scaleZ", node.local.scaleZ))
72 return editing::failed<SceneGraph>(editing::Status::Failed,
73 editing::RuleId("scene.physics-placement.snapshot-transform"),
74 "Scene snapshot contains an incomplete transform");
75 graph.emplace(*id, std::move(node));
76 }
77 return editing::applied<SceneGraph>(std::move(graph));
78}
79
80glm::dmat4 matrixOf(const scene_editing::SceneTransformValue& value) {
81 const auto rotation = glm::dquat(glm::dvec3(value.rotationX, value.rotationY, value.rotationZ));
82 return glm::translate(glm::dmat4(1.0), glm::dvec3(value.x, value.y, value.z)) * glm::mat4_cast(rotation) *
83 glm::scale(glm::dmat4(1.0), glm::dvec3(value.scaleX, value.scaleY, value.scaleZ));
84}
85
86editing::Result<glm::dmat4> worldMatrixOf(const std::string& id, const SceneGraph& graph,
87 std::unordered_map<std::string, glm::dmat4>& cache,
88 std::unordered_set<std::string>& visiting) {
89 if (const auto cached = cache.find(id); cached != cache.end()) return editing::applied<glm::dmat4>(cached->second);
90 const auto node = graph.find(id);
91 if (node == graph.end())
92 return editing::failed<glm::dmat4>(editing::Status::NotFound,
93 editing::RuleId("scene.physics-placement.object-parent"),
94 "Placement object is missing from the scene hierarchy");
95 if (!visiting.insert(id).second)
96 return editing::failed<glm::dmat4>(editing::Status::Failed,
97 editing::RuleId("scene.physics-placement.hierarchy-cycle"),
98 "Scene hierarchy contains a cycle");
99 glm::dmat4 world = matrixOf(node->second.local);
100 if (!node->second.parent.empty()) {
101 auto parent = worldMatrixOf(node->second.parent, graph, cache, visiting);
102 if (!parent.ok()) return editing::Result<glm::dmat4>::failure(parent.status());
103 world = parent.value() * world;
104 }
105 visiting.erase(id);
106 cache.insert_or_assign(id, world);
107 return editing::applied<glm::dmat4>(world);
108}
109
110editing::Result<scene_editing::SceneTransformValue> transformOf(const glm::dmat4& matrix) {
111 glm::dvec3 scale;
112 glm::dquat rotation;
113 glm::dvec3 translation;
114 glm::dvec3 skew;
115 glm::dvec4 perspective;
116 if (!glm::decompose(matrix, scale, rotation, translation, skew, perspective) || glm::length(skew) > 1.0e-7 ||
117 std::abs(perspective.x) > 1.0e-9 || std::abs(perspective.y) > 1.0e-9 || std::abs(perspective.z) > 1.0e-9 ||
118 std::abs(perspective.w - 1.0) > 1.0e-9) {
119 return editing::failed<scene_editing::SceneTransformValue>(
120 editing::Status::Rejected, editing::RuleId("scene.physics-placement.sheared-transform"),
121 "Physics placement cannot represent a sheared hierarchy transform as TRS");
122 }
123 const auto euler = glm::eulerAngles(glm::normalize(rotation));
124 scene_editing::SceneTransformValue value;
125 value.x = translation.x;
126 value.y = translation.y;
127 value.z = translation.z;
128 value.rotationX = euler.x;
129 value.rotationY = euler.y;
130 value.rotationZ = euler.z;
131 value.scaleX = scale.x;
132 value.scaleY = scale.y;
133 value.scaleZ = scale.z;
134 return editing::applied<scene_editing::SceneTransformValue>(value);
135}
136
137bool spansVolume(const std::vector<float>& vertices) {
138 constexpr double epsilonSquared = 1.0e-12;
139 const auto point = [&](std::size_t index) {
140 return std::array<double, 3>{vertices[index], vertices[index + 1], vertices[index + 2]};
141 };
142 const auto subtract = [](const auto& left, const auto& right) {
143 return std::array<double, 3>{left[0] - right[0], left[1] - right[1], left[2] - right[2]};
144 };
145 const auto cross = [](const auto& left, const auto& right) {
146 return std::array<double, 3>{left[1] * right[2] - left[2] * right[1], left[2] * right[0] - left[0] * right[2],
147 left[0] * right[1] - left[1] * right[0]};
148 };
149 const auto dot = [](const auto& left, const auto& right) {
150 return left[0] * right[0] + left[1] * right[1] + left[2] * right[2];
151 };
152 const auto origin = point(0);
153 std::array<double, 3> first{};
154 std::array<double, 3> normal{};
155 bool hasFirst = false;
156 bool hasNormal = false;
157 for (std::size_t index = 3; index < vertices.size(); index += 3) {
158 const auto edge = subtract(point(index), origin);
159 if (!hasFirst) {
160 if (dot(edge, edge) <= epsilonSquared) continue;
161 first = edge;
162 hasFirst = true;
163 continue;
164 }
165 if (!hasNormal) {
166 const auto candidate = cross(first, edge);
167 if (dot(candidate, candidate) <= epsilonSquared) continue;
168 normal = candidate;
169 hasNormal = true;
170 continue;
171 }
172 if (std::abs(dot(normal, edge)) > 1.0e-9) return true;
173 }
174 return false;
175}
176
177const char* shapeName(PhysicsPlacementShape shape) {
178 switch (shape) {
179 case PhysicsPlacementShape::Box: return "box";
180 case PhysicsPlacementShape::Sphere: return "sphere";
181 case PhysicsPlacementShape::Capsule: return "capsule";
182 case PhysicsPlacementShape::ConvexHull: return "convex";
183 case PhysicsPlacementShape::Auto: return "auto";
184 }
185 return "box";
186}
187
188editing::Value colliderValue(const PhysicsPlacementCollider& collider) {
190 vertices.reserve(collider.convexVertices.size());
191 for (const float value : collider.convexVertices) vertices.emplace_back(static_cast<double>(value));
193 {"shape", shapeName(collider.shape)},
194 {"source", collider.source == PhysicsPlacementColliderSource::Existing ? "existing" : "generated"},
195 {"halfExtents", editing::Value::Array{collider.halfExtentX, collider.halfExtentY, collider.halfExtentZ}},
196 {"localPosition", editing::Value::Array{collider.localX, collider.localY, collider.localZ}},
197 {"localRotation",
198 editing::Value::Array{collider.localRotationX, collider.localRotationY, collider.localRotationZ}},
199 {"vertices", std::move(vertices)},
200 {"maxVertices", static_cast<std::int64_t>(collider.convexMaxVertices)},
201 };
202}
203
204bool valueNumber(const editing::Value& value, double& output) {
205 if (const auto* real = value.getIf<double>())
206 output = *real;
207 else if (const auto* integer = value.getIf<std::int64_t>())
208 output = static_cast<double>(*integer);
209 else
210 return false;
211 return true;
212}
213
214editing::Result<PhysicsPlacementCollider> colliderOf(const editing::Value& value) {
215 const auto* shapeValue = valueField(value, "shape");
216 const auto* sourceValue = valueField(value, "source");
217 const auto* halfValue = valueField(value, "halfExtents");
218 const auto* localValue = valueField(value, "localPosition");
219 const auto* rotationValue = valueField(value, "localRotation");
220 const auto* verticesValue = valueField(value, "vertices");
221 const auto* maximumValue = valueField(value, "maxVertices");
222 const auto* shape = shapeValue ? shapeValue->getIf<std::string>() : nullptr;
223 const auto* source = sourceValue ? sourceValue->getIf<std::string>() : nullptr;
224 const auto* half = halfValue ? halfValue->getIf<editing::Value::Array>() : nullptr;
225 const auto* local = localValue ? localValue->getIf<editing::Value::Array>() : nullptr;
226 const auto* rotation = rotationValue ? rotationValue->getIf<editing::Value::Array>() : nullptr;
227 const auto* vertices = verticesValue ? verticesValue->getIf<editing::Value::Array>() : nullptr;
228 const auto* maximum = maximumValue ? maximumValue->getIf<std::int64_t>() : nullptr;
229 PhysicsPlacementCollider collider;
230 if (!shape || !source || !half || half->size() != 3 || !local || local->size() != 3 || !rotation ||
231 rotation->size() != 3 || !vertices || !maximum || !valueNumber((*half)[0], collider.halfExtentX) ||
232 !valueNumber((*half)[1], collider.halfExtentY) || !valueNumber((*half)[2], collider.halfExtentZ) ||
233 !valueNumber((*local)[0], collider.localX) || !valueNumber((*local)[1], collider.localY) ||
234 !valueNumber((*local)[2], collider.localZ) || !valueNumber((*rotation)[0], collider.localRotationX) ||
235 !valueNumber((*rotation)[1], collider.localRotationY) || !valueNumber((*rotation)[2], collider.localRotationZ))
236 return editing::failed<PhysicsPlacementCollider>(editing::Status::Rejected,
237 editing::RuleId("scene.physics-placement.cache-part"),
238 "Collider cache part has an invalid schema");
239 if (*shape == "sphere")
240 collider.shape = PhysicsPlacementShape::Sphere;
241 else if (*shape == "capsule")
242 collider.shape = PhysicsPlacementShape::Capsule;
243 else if (*shape == "convex")
244 collider.shape = PhysicsPlacementShape::ConvexHull;
245 else if (*shape != "box")
246 return editing::failed<PhysicsPlacementCollider>(editing::Status::Rejected,
247 editing::RuleId("scene.physics-placement.cache-shape"),
248 "Collider cache shape is unsupported");
249 if (*source == "existing")
251 else if (*source != "generated")
252 return editing::failed<PhysicsPlacementCollider>(editing::Status::Rejected,
253 editing::RuleId("scene.physics-placement.cache-source"),
254 "Collider cache source is unsupported");
255 collider.convexMaxVertices = static_cast<int>(*maximum);
256 collider.convexVertices.reserve(vertices->size());
257 for (const auto& component : *vertices) {
258 double numeric = 0.0;
259 if (!valueNumber(component, numeric))
260 return editing::failed<PhysicsPlacementCollider>(editing::Status::Rejected,
261 editing::RuleId("scene.physics-placement.cache-vertex"),
262 "Collider cache vertices must be numeric");
263 collider.convexVertices.push_back(static_cast<float>(numeric));
264 }
265 return editing::applied<PhysicsPlacementCollider>(std::move(collider));
266}
267} // namespace
268
270 struct CachedHull {
271 std::string resourceKey;
272 std::vector<PhysicsPlacementCollider> colliders;
273 };
274 explicit Impl(std::unique_ptr<scene_editing::SceneTargetBase> value)
275 : target(std::move(value)), coordinator(commands) {
276 if (!target || target->targetId().empty()) throw std::invalid_argument("Scene target must have an identity");
278 throw std::runtime_error("Could not register scene editing target");
279 }
280 std::unique_ptr<scene_editing::SceneTargetBase> target;
284 std::unique_ptr<ScenePhysicsPlacementBackend> placement;
285 std::unordered_map<std::string, CachedHull> placementHulls;
286 SceneGraph placementGraph;
287 std::unordered_map<std::string, glm::dmat4> placementInitialWorld;
288 std::uint64_t placementTick = 0;
289};
291 : SceneEditorSession(std::make_unique<scene_editing::SceneDocumentTarget>(std::move(id))) {}
292SceneEditorSession::SceneEditorSession(std::unique_ptr<scene_editing::SceneTargetBase> target)
293 : impl_(std::make_unique<Impl>(std::move(target))) {}
295
298 request.id = editing::CommandId(std::move(command));
299 request.payload = std::move(payload);
300 request.context.target = impl_->target->targetId();
301 request.context.targetRevision = impl_->target->revision();
302 const auto& profile = impl_->profile;
303 auto plan = impl_->commands.plan(request, profile);
304 if (!plan.ok()) return editing::Result<editing::TransactionReceipt>::failure(plan.status());
305 // Planning resolves the target generation; execution immediately validates it again.
306 request.context.targetGeneration = plan.value().targetGeneration;
307 return impl_->commands.executePlan(request, plan.value(), profile);
308}
311 for (const auto& command : commands) {
312 if (command.empty())
313 return editing::failed<void>(editing::Status::Rejected, editing::RuleId("scene.session.empty-command"),
314 "Allowed command id must not be empty");
315 profile.allowCommand(editing::CommandId(command));
316 }
317 impl_->profile = std::move(profile);
318 return editing::applied<void>();
319}
321 return impl_->coordinator.undo(impl_->target->targetId());
322}
324 return impl_->coordinator.redo(impl_->target->targetId());
325}
326editing::Value SceneEditorSession::snapshot() const { return impl_->target->snapshotValue(); }
328editing::Revision SceneEditorSession::revision() const { return impl_->target->revision(); }
329
331 std::vector<float> vertices, int maxVertices) {
332 if (object.empty() || vertices.size() < 12 || vertices.size() % 3 != 0 || vertices.size() / 3 > 100000 ||
333 maxVertices < 4 || maxVertices > 254 ||
334 !std::all_of(vertices.begin(), vertices.end(), [](float value) { return std::isfinite(value); }) ||
335 !spansVolume(vertices))
336 return editing::failed<void>(
337 editing::Status::Rejected, editing::RuleId("scene.physics-placement.hull-cache-input"),
338 "Cached hulls require an object id, finite non-coplanar XYZ points and budget [4, 254]");
341 collider.convexVertices = std::move(vertices);
342 collider.convexMaxVertices = maxVertices;
343 return cachePhysicsPlacementCompound(object, {}, {std::move(collider)});
344}
345
347 const editing::ObjectId& object, std::string resourceKey, std::vector<PhysicsPlacementCollider> colliders) {
348 if (object.empty() || colliders.empty() || colliders.size() > 64)
349 return editing::failed<void>(editing::Status::Rejected,
350 editing::RuleId("scene.physics-placement.compound-cache-input"),
351 "Compound collider cache requires an object and between 1 and 64 parts");
352 for (auto& collider : colliders) {
354 if (!std::isfinite(collider.halfExtentX) || !std::isfinite(collider.halfExtentY) ||
355 !std::isfinite(collider.halfExtentZ) || collider.halfExtentX <= 0.0 || collider.halfExtentY <= 0.0 ||
356 collider.halfExtentZ <= 0.0 || !std::isfinite(collider.localX) || !std::isfinite(collider.localY) ||
357 !std::isfinite(collider.localZ) || !std::isfinite(collider.localRotationX) ||
358 !std::isfinite(collider.localRotationY) || !std::isfinite(collider.localRotationZ) ||
359 collider.shape == PhysicsPlacementShape::Auto ||
360 (collider.shape == PhysicsPlacementShape::ConvexHull &&
361 (collider.convexVertices.size() < 12 || collider.convexVertices.size() % 3 != 0 ||
362 collider.convexMaxVertices < 4 || collider.convexMaxVertices > 254 ||
363 !std::all_of(collider.convexVertices.begin(), collider.convexVertices.end(),
364 [](float value) { return std::isfinite(value); }) ||
365 !spansVolume(collider.convexVertices))))
366 return editing::failed<void>(editing::Status::Rejected,
367 editing::RuleId("scene.physics-placement.compound-cache-part"),
368 "Compound collider cache contains malformed local geometry");
369 }
370 impl_->placementHulls.insert_or_assign(object.value(),
371 Impl::CachedHull{std::move(resourceKey), std::move(colliders)});
372 return editing::applied<void>();
373}
374
376 if (object.empty())
377 return editing::failed<void>(editing::Status::Rejected,
378 editing::RuleId("scene.physics-placement.hull-cache-object"),
379 "Cached hull removal requires an object id");
380 if (impl_->placementHulls.erase(object.value()) == 0)
381 return editing::failed<void>(editing::Status::NoOp,
382 editing::RuleId("scene.physics-placement.hull-cache-missing"),
383 "The object has no cached automatic placement hull");
384 return editing::applied<void>();
385}
386
389 std::vector<std::string> objects;
390 objects.reserve(impl_->placementHulls.size());
391 for (const auto& [object, cached] : impl_->placementHulls) {
392 (void)cached;
393 objects.push_back(object);
394 }
395 std::sort(objects.begin(), objects.end());
396 for (const auto& object : objects) {
397 const auto& cached = impl_->placementHulls.at(object);
398 editing::Value::Array colliders;
399 colliders.reserve(cached.colliders.size());
400 for (const auto& collider : cached.colliders) colliders.emplace_back(colliderValue(collider));
401 entries.emplace_back(editing::Value::Object{
402 {"object", object}, {"resourceKey", cached.resourceKey}, {"colliders", std::move(colliders)}});
403 }
405 {"schema", "eve.scene.physics-placement-collider-cache"},
406 {"version", std::int64_t{2}},
407 {"entries", std::move(entries)},
408 });
409}
410
412 auto decoded = editing::editorValueFromJson(json);
413 if (!decoded.ok()) return editing::Result<void>::failure(decoded.status());
414 const auto* schemaValue = valueField(decoded.value(), "schema");
415 const auto* versionValue = valueField(decoded.value(), "version");
416 const auto* entriesValue = valueField(decoded.value(), "entries");
417 const auto* schema = schemaValue ? schemaValue->getIf<std::string>() : nullptr;
418 const auto* version = versionValue ? versionValue->getIf<std::int64_t>() : nullptr;
419 const auto* entries = entriesValue ? entriesValue->getIf<editing::Value::Array>() : nullptr;
420 if (!schema || *schema != "eve.scene.physics-placement-collider-cache" || !version || *version != 2 || !entries)
421 return editing::failed<void>(editing::Status::Rejected, editing::RuleId("scene.physics-placement.cache-schema"),
422 "Collider cache requires schema eve.scene.physics-placement-collider-cache v2");
423 std::unordered_map<std::string, Impl::CachedHull> replacement;
424 for (const auto& entry : *entries) {
425 const auto* objectValue = valueField(entry, "object");
426 const auto* keyValue = valueField(entry, "resourceKey");
427 const auto* collidersValue = valueField(entry, "colliders");
428 const auto* object = objectValue ? objectValue->getIf<std::string>() : nullptr;
429 const auto* key = keyValue ? keyValue->getIf<std::string>() : nullptr;
430 const auto* colliders = collidersValue ? collidersValue->getIf<editing::Value::Array>() : nullptr;
431 if (!object || object->empty() || !key || !colliders || colliders->empty() || colliders->size() > 64 ||
432 replacement.contains(*object))
433 return editing::failed<void>(editing::Status::Rejected,
434 editing::RuleId("scene.physics-placement.cache-entry"),
435 "Collider cache entry is malformed or duplicated");
436 std::vector<PhysicsPlacementCollider> parsed;
437 parsed.reserve(colliders->size());
438 for (const auto& value : *colliders) {
439 auto collider = colliderOf(value);
440 if (!collider.ok()) return editing::Result<void>::failure(collider.status());
441 parsed.push_back(std::move(collider.value()));
442 }
443 replacement.emplace(*object, Impl::CachedHull{*key, std::move(parsed)});
444 }
445 impl_->placementHulls = std::move(replacement);
446 return editing::applied<void>();
447}
448
450 if (impl_->placement)
451 return editing::failed<void>(editing::Status::Conflict, editing::RuleId("scene.physics-placement.active"),
452 "A physics placement preview is already active");
453 auto graph = sceneGraphOf(impl_->target->snapshotValue());
454 if (!graph.ok()) return editing::Result<void>::failure(graph.status());
455 std::unordered_map<std::string, glm::dmat4> worldMatrices;
456 std::unordered_set<std::string> visiting;
457 for (auto& object : request.objects) {
458 if (object.shape == PhysicsPlacementShape::Auto) {
459 const auto found = impl_->placementHulls.find(object.object.value());
460 if (found == impl_->placementHulls.end())
461 return editing::failed<void>(editing::Status::NotFound,
462 editing::RuleId("scene.physics-placement.auto-shape-missing"),
463 "Automatic placement hull is not cached for the object");
464 if (!object.colliderResourceKey.empty() && object.colliderResourceKey != found->second.resourceKey)
465 return editing::failed<void>(editing::Status::Conflict,
466 editing::RuleId("scene.physics-placement.auto-shape-stale"),
467 "Automatic placement collider cache is stale for the resource revision");
468 object.shape = PhysicsPlacementShape::Box;
469 object.colliders = found->second.colliders;
470 }
471 auto world = worldMatrixOf(object.object.value(), graph.value(), worldMatrices, visiting);
472 if (!world.ok()) return editing::Result<void>::failure(world.status());
473 auto transform = transformOf(world.value());
474 if (!transform.ok()) return editing::Result<void>::failure(transform.status());
475 object.transform = transform.value();
476 }
477 request.sourceRevision = impl_->target->revision();
479 if (!created.ok()) return editing::Result<void>::failure(created.status());
480 impl_->placement = std::move(created.value());
481 impl_->placementGraph = std::move(graph.value());
482 impl_->placementInitialWorld = std::move(worldMatrices);
483 impl_->placementTick = 0;
484 return editing::applied<void>();
485}
486
488 double fixedDelta) {
489 if (!impl_->placement)
490 return editing::failed<PhysicsPlacementFrame>(editing::Status::Rejected,
491 editing::RuleId("scene.physics-placement.inactive"),
492 "No physics placement preview is active");
493 auto moved = impl_->placement->setHandlePosition(x, y, z);
494 if (!moved.ok()) return editing::Result<PhysicsPlacementFrame>::failure(moved.status());
495 const auto nextTick = impl_->placementTick + 1;
496 auto stepped = impl_->placement->step(nextTick, fixedDelta);
497 if (!stepped.ok()) return editing::Result<PhysicsPlacementFrame>::failure(stepped.status());
498 impl_->placementTick = nextTick;
499 return impl_->placement->placementFrame();
500}
501
503 double x, double y, double z, double rotationX, double rotationY, double rotationZ, double fixedDelta) {
504 if (!impl_->placement)
505 return editing::failed<PhysicsPlacementFrame>(editing::Status::Rejected,
506 editing::RuleId("scene.physics-placement.inactive"),
507 "No physics placement preview is active");
508 auto moved = impl_->placement->setHandlePosition(x, y, z);
509 if (!moved.ok()) return editing::Result<PhysicsPlacementFrame>::failure(moved.status());
510 auto rotated = impl_->placement->setHandleRotation(rotationX, rotationY, rotationZ);
511 if (!rotated.ok()) return editing::Result<PhysicsPlacementFrame>::failure(rotated.status());
512 const auto nextTick = impl_->placementTick + 1;
513 auto stepped = impl_->placement->step(nextTick, fixedDelta);
514 if (!stepped.ok()) return editing::Result<PhysicsPlacementFrame>::failure(stepped.status());
515 impl_->placementTick = nextTick;
516 return impl_->placement->placementFrame();
517}
518
520 double x, double y, double z, double rotationX, double rotationY, double rotationZ, double scaleX, double scaleY,
521 double scaleZ, double fixedDelta) {
522 if (!impl_->placement)
523 return editing::failed<PhysicsPlacementFrame>(editing::Status::Rejected,
524 editing::RuleId("scene.physics-placement.inactive"),
525 "No physics placement preview is active");
526 auto moved = impl_->placement->setHandlePosition(x, y, z);
527 if (!moved.ok()) return editing::Result<PhysicsPlacementFrame>::failure(moved.status());
528 auto rotated = impl_->placement->setHandleRotation(rotationX, rotationY, rotationZ);
529 if (!rotated.ok()) return editing::Result<PhysicsPlacementFrame>::failure(rotated.status());
530 auto scaled = impl_->placement->setHandleScale(scaleX, scaleY, scaleZ);
531 if (!scaled.ok()) return editing::Result<PhysicsPlacementFrame>::failure(scaled.status());
532 const auto nextTick = impl_->placementTick + 1;
533 auto stepped = impl_->placement->step(nextTick, fixedDelta);
534 if (!stepped.ok()) return editing::Result<PhysicsPlacementFrame>::failure(stepped.status());
535 impl_->placementTick = nextTick;
536 return impl_->placement->placementFrame();
537}
538
540 double fromX, double fromY, double fromZ, double toX, double toY, double toZ, double offset, double fixedDelta) {
541 if (!impl_->placement)
542 return editing::failed<PhysicsPlacementFrame>(editing::Status::Rejected,
543 editing::RuleId("scene.physics-placement.inactive"),
544 "No physics placement preview is active");
545 auto aligned = impl_->placement->alignHandleToSurface(fromX, fromY, fromZ, toX, toY, toZ, offset);
546 if (!aligned.ok()) return editing::Result<PhysicsPlacementFrame>::failure(aligned.status());
547 const auto nextTick = impl_->placementTick + 1;
548 auto stepped = impl_->placement->step(nextTick, fixedDelta);
549 if (!stepped.ok()) return editing::Result<PhysicsPlacementFrame>::failure(stepped.status());
550 impl_->placementTick = nextTick;
551 return impl_->placement->placementFrame();
552}
553
555 if (!impl_->placement)
556 return editing::failed<editing::TransactionReceipt>(editing::Status::Rejected,
557 editing::RuleId("scene.physics-placement.inactive"),
558 "No physics placement preview is active");
559 if (impl_->target->revision() != impl_->placement->sourceRevision())
560 return editing::failed<editing::TransactionReceipt>(editing::Status::Conflict,
561 editing::RuleId("scene.physics-placement.stale"),
562 "Scene changed while physics placement was active");
563 auto captured = impl_->placement->placementFrame();
564 if (!captured.ok()) return editing::Result<editing::TransactionReceipt>::failure(captured.status());
565 std::unordered_map<std::string, glm::dmat4> desiredWorld;
566 for (const auto& object : captured.value().objects)
567 if (object.selected) desiredWorld.insert_or_assign(object.object.value(), matrixOf(object.transform));
568 editing::Value::Array transforms;
569 for (const auto& object : captured.value().objects) {
570 if (!object.selected) continue;
571 const auto graphNode = impl_->placementGraph.find(object.object.value());
572 if (graphNode == impl_->placementGraph.end())
573 return editing::failed<editing::TransactionReceipt>(editing::Status::Conflict,
574 editing::RuleId("scene.physics-placement.graph-stale"),
575 "Placement hierarchy state is no longer available");
576 glm::dmat4 localMatrix = desiredWorld.at(object.object.value());
577 if (!graphNode->second.parent.empty()) {
578 const auto desiredParent = desiredWorld.find(graphNode->second.parent);
579 if (desiredParent != desiredWorld.end())
580 localMatrix = glm::inverse(desiredParent->second) * localMatrix;
581 else {
582 const auto initialParent = impl_->placementInitialWorld.find(graphNode->second.parent);
583 if (initialParent == impl_->placementInitialWorld.end())
584 return editing::failed<editing::TransactionReceipt>(
585 editing::Status::Conflict, editing::RuleId("scene.physics-placement.parent-stale"),
586 "Placement parent world transform is no longer available");
587 localMatrix = glm::inverse(initialParent->second) * localMatrix;
588 }
589 }
590 auto local = transformOf(localMatrix);
592 const auto& value = local.value();
593 transforms.emplace_back(editing::Value::Object{
594 {"object", object.object.value()},
595 {"position", editing::Value::Array{value.x, value.y, value.z}},
596 {"rotation", editing::Value::Array{value.rotationX, value.rotationY, value.rotationZ}},
597 {"scale", editing::Value::Array{value.scaleX, value.scaleY, value.scaleZ}},
598 });
599 }
600 auto committed =
601 execute("scene.transform.batch.set.v1", editing::Value::Object{{"transforms", std::move(transforms)}});
602 if (committed.ok()) {
603 impl_->placement.reset();
604 impl_->placementGraph.clear();
605 impl_->placementInitialWorld.clear();
606 }
607 return committed;
608}
609
611 if (!impl_->placement)
612 return editing::failed<void>(editing::Status::NoOp, editing::RuleId("scene.physics-placement.inactive"),
613 "No physics placement preview is active");
614 impl_->placement.reset();
615 impl_->placementGraph.clear();
616 impl_->placementInitialWorld.clear();
617 impl_->placementTick = 0;
618 return editing::applied<void>();
619}
620
621bool SceneEditorSession::physicsPlacementActive() const noexcept { return impl_->placement != nullptr; }
625 return execute("scene.snapshot.restore.v1", std::move(value.value()));
626}
627} // namespace eve::scene_editor
LogicalId target
double value
Value::Object payload
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
int root
Definition AnimSmr.cpp:119
std::string output
std::unordered_map< std::string, QuestRuntime > entries
float maximum[3]
const GltfImportRequest & request
std::uint32_t key
ShaderImageInput shape
HexVec3 left
HexVec3 right
std::int32_t first
std::string local
size_t offset
std::array< float, 4 > rotation
std::array< float, 3 > scale
std::int32_t parent
Texture * normal
std::map< std::string, std::vector< std::string > > graph
Definition Package.cpp:59
World3D * world
std::vector< Point > vertices
std::string id
Definition PlayHost.cpp:108
V3 origin
Definition RoadBake.cpp:138
const RoadNode * node
const RoadEdge * edge
double number
bool found
int created
Json object
std::set< std::string > visiting
uint32_t index
const UnitySourceAsset & source
std::vector< VegetationPresetCommand > commands
glm::vec3 point
bool ok() const noexcept
Whether this result represents a non-failure outcome.
Definition Result.h:480
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
UUID-backed identifier adapter for legacy textual boundaries.
Definition Identity.h:314
Deterministic owning value tree shared by authoring hosts.
std::vector< Value > Array
std::map< std::string, Value > Object
Registry and execution gate for UI-, script- and automation-neutral commands.
Coordinates editable targets, per-target histories and domain commands.
Result< void > registerTarget(IEditableTarget &target)
Registers target.
Upper capability boundary for one editor host.
static HostProfile runtimeBuilder()
Safe runtime profile that denies commands unless allow-listed.
static HostProfile developer()
Unrestricted developer profile used by legacy sessions.
Composable scene command/history host with no UI or renderer dependency. Owns its target,...
editing::Result< editing::TransactionReceipt > redo()
Reapply the last undone scene transaction.
editing::Result< void > cachePhysicsPlacementCompound(const editing::ObjectId &object, std::string resourceKey, std::vector< PhysicsPlacementCollider > colliders)
Replace one object's generated compound collider cache entry.
editing::Result< PhysicsPlacementFrame > alignPhysicsPlacementToSurface(double fromX, double fromY, double fromZ, double toX, double toY, double toZ, double offset=0.0, double fixedDelta=1.0/60.0)
Align the active selection to a non-selected preview surface and advance one step.
editing::Result< void > removePhysicsPlacementHull(const editing::ObjectId &object)
Remove one cached automatic placement hull; missing entries return NoOp.
editing::Result< PhysicsPlacementFrame > updatePhysicsPlacementPose(double x, double y, double z, double rotationX, double rotationY, double rotationZ, double fixedDelta=1.0/60.0)
Move and rotate the preview handle, then advance one fixed simulation step.
editing::Result< void > restrictCommands(const std::vector< std::string > &commands)
Restrict future commands to a host allow-list; empty denies all. Existing history remains undoable.
editing::Result< editing::TransactionReceipt > undo()
Undo the last scene transaction; failures preserve history.
std::string savePhysicsPlacementColliderCacheJson() const
Encode schema-2 generated collider cache; host owns file I/O and the returned string.
editing::Result< editing::TransactionReceipt > commitPhysicsPlacement()
Atomically publish selected preview transforms as one undoable transaction.
SceneEditorSession(std::string targetId)
Own a document target with a nonempty host-local identity.
editing::Result< editing::TransactionReceipt > execute(std::string command, editing::Value payload)
Execute a registered scene command through revision checks and one reversible transaction.
editing::Result< editing::TransactionReceipt > restoreJson(const std::string &json)
Parse and restore schema-1 JSON as an undoable transaction; invalid input changes nothing.
editing::Value snapshot() const
Copy the versioned hierarchy/TRS snapshot; caller owns the returned tree.
editing::Result< PhysicsPlacementFrame > updatePhysicsPlacement(double x, double y, double z, double fixedDelta=1.0/60.0)
Move the preview handle and advance one fixed simulation step.
editing::Revision revision() const
Return the current monotonic target revision.
std::string saveJson() const
Encode snapshot deterministically; file I/O and paths belong to the host.
editing::Result< void > beginPhysicsPlacement(PhysicsPlacementRequest request)
Begin an isolated Box3D placement preview using current scene transforms.
editing::Result< void > restorePhysicsPlacementColliderCacheJson(const std::string &json)
Atomically replace generated collider cache from schema-2 JSON.
bool physicsPlacementActive() const noexcept
Report whether this session owns an active placement preview.
editing::Result< void > cancelPhysicsPlacement()
Discard the isolated preview without changing scene state.
~SceneEditorSession()
Scene editor session.
editing::Result< PhysicsPlacementFrame > updatePhysicsPlacementTransform(double x, double y, double z, double rotationX, double rotationY, double rotationZ, double scaleX, double scaleY, double scaleZ, double fixedDelta=1.0/60.0)
Move, rotate and scale the preview handle, then advance one fixed simulation step.
editing::Result< void > cachePhysicsPlacementHull(const editing::ObjectId &object, std::vector< float > vertices, int maxVertices=64)
Cache backend-neutral local-space mesh points for later automatic convex placement.
static editing::Result< std::unique_ptr< ScenePhysicsPlacementBackend > > create(PhysicsPlacementRequest request)
Validate and build an isolated placement world.
std::uint64_t Revision
std::string editorValueToJson(const EditorValue &value)
Serialize an EditorValue to deterministic compact JSON.
Result< EditorValue > editorValueFromJson(const std::string &json)
Parse JSON into the pointer-free EditorValue protocol tree.
StrongId< RuleIdTag > RuleId
Definition EditingIds.h:60
StrongId< CommandIdTag > CommandId
Definition EditingIds.h:58
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
editing::Result< void > registerEditingCommands(editing::IEditingCommandRegistry &registry)
Register Scene-owned commands with a generic editing host.
PhysicsPlacementShape
Collision approximation admitted into the private preview world.
CommandRequest public API.
One local-space shape in a possibly compound placement collider.
Owning request used to construct one isolated placement candidate.
std::unique_ptr< ScenePhysicsPlacementBackend > placement
std::unordered_map< std::string, CachedHull > placementHulls
std::unique_ptr< scene_editing::SceneTargetBase > target
Impl(std::unique_ptr< scene_editing::SceneTargetBase > value)
std::unordered_map< std::string, glm::dmat4 > placementInitialWorld