9#include <simplesquirrel/simplesquirrel.hpp>
13const editing::Value*
field(
const editing::Value&
value,
const char*
key) {
15 if (!
object)
return nullptr;
16 const auto found =
object->find(
key);
17 return found ==
object->end() ? nullptr : &
found->second;
21 if (
const auto* real =
value.getIf<
double>())
23 else if (
const auto* integer =
value.getIf<std::int64_t>())
24 output =
static_cast<double>(*integer);
30editing::Result<PhysicsPlacementCollider> placementCollider(
const editing::Value&
value) {
31 PhysicsPlacementCollider result;
32 const auto* shapeValue =
field(
value,
"shape");
33 const auto*
shape = shapeValue ? shapeValue->getIf<std::string>() : nullptr;
35 return editing::failed<PhysicsPlacementCollider>(editing::Status::Rejected,
37 "Collider part shape is unsupported");
38 if (*
shape ==
"sphere")
40 else if (*
shape ==
"capsule")
42 else if (*
shape ==
"convex")
44 if (
const auto* sourceValue =
field(
value,
"source")) {
45 const auto*
source = sourceValue->getIf<std::string>();
47 return editing::failed<PhysicsPlacementCollider>(
48 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-part-source"),
49 "Collider part source must be existing or generated");
53 const auto parseVector = [&](
const char*
key,
double&
x,
double&
y,
double&
z,
bool required) {
57 return array && array->size() == 3 &&
number((*array)[0],
x) &&
number((*array)[1],
y) &&
60 if (!parseVector(
"halfExtents", result.halfExtentX, result.halfExtentY, result.halfExtentZ,
true) ||
61 !parseVector(
"localPosition", result.localX, result.localY, result.localZ,
false) ||
62 !parseVector(
"localRotation", result.localRotationX, result.localRotationY, result.localRotationZ,
false))
63 return editing::failed<PhysicsPlacementCollider>(editing::Status::Rejected,
65 "Collider part vectors require three numbers");
66 if (
const auto* verticesValue =
field(
value,
"vertices")) {
69 return editing::failed<PhysicsPlacementCollider>(
70 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-part-vertices"),
71 "Collider part vertices must be an array");
72 for (
const auto& component : *
vertices) {
74 if (!
number(component, numeric))
75 return editing::failed<PhysicsPlacementCollider>(
76 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-part-vertex"),
77 "Collider part vertices must be numeric");
78 result.convexVertices.push_back(
static_cast<float>(numeric));
81 if (
const auto* maximumValue =
field(
value,
"maxHullVertices")) {
82 const auto*
maximum = maximumValue->getIf<std::int64_t>();
84 return editing::failed<PhysicsPlacementCollider>(
85 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-part-budget"),
86 "Collider part maxHullVertices must be an integer");
87 result.convexMaxVertices =
static_cast<int>(*maximum);
89 return editing::applied<PhysicsPlacementCollider>(std::move(result));
92editing::Result<PhysicsPlacementRequest> placementRequest(
const editing::Value&
payload) {
96 return editing::failed<PhysicsPlacementRequest>(editing::Status::Rejected,
98 "Physics placement requires an objects array");
99 PhysicsPlacementRequest
request;
100 if (
const auto* primaryValue =
field(
payload,
"primary")) {
101 const auto* primary = primaryValue->getIf<std::string>();
103 return editing::failed<PhysicsPlacementRequest>(editing::Status::Rejected,
105 "Physics placement primary must be an object id");
109 const auto* mode = modeValue->getIf<std::string>();
110 if (!mode || (*mode !=
"drag" && *mode !=
"place" && *mode !=
"drop" && *mode !=
"fall" && *mode !=
"rotate" &&
111 *mode !=
"align" && *mode !=
"point"))
112 return editing::failed<PhysicsPlacementRequest>(editing::Status::Rejected,
114 "Physics placement mode is unsupported");
115 if (*mode ==
"drop" || *mode ==
"fall")
117 else if (*mode ==
"rotate")
119 else if (*mode ==
"align")
121 else if (*mode ==
"point")
126 if (
const auto* settingsValue =
field(
payload,
"settings")) {
128 return editing::failed<PhysicsPlacementRequest>(editing::Status::Rejected,
130 "Physics placement settings must be an object");
131 const auto assign = [&](
const char*
key,
double& destination) {
135 if (!assign(
"gravityX",
request.settings.gravityX) || !assign(
"gravityY",
request.settings.gravityY) ||
136 !assign(
"gravityZ",
request.settings.gravityZ) ||
137 !assign(
"maximumLinearSpeed",
request.settings.maximumLinearSpeed) ||
138 !assign(
"linearDamping",
request.settings.linearDamping) ||
139 !assign(
"angularDamping",
request.settings.angularDamping) ||
140 !assign(
"contactHertz",
request.settings.contactHertz) ||
141 !assign(
"contactDampingRatio",
request.settings.contactDampingRatio) ||
142 !assign(
"maximumPushOutSpeed",
request.settings.maximumPushOutSpeed) ||
143 !assign(
"maximumAngularSpeed",
request.settings.maximumAngularSpeed) ||
144 !assign(
"minimumBoundsSpeedFactor",
request.settings.minimumBoundsSpeedFactor) ||
145 !assign(
"maximumBoundsSpeedFactor",
request.settings.maximumBoundsSpeedFactor) ||
146 !assign(
"teleportDistance",
request.settings.teleportDistance))
147 return editing::failed<PhysicsPlacementRequest>(
148 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-setting-number"),
149 "Physics placement numeric settings must contain numbers");
150 if (
const auto* subStepsValue =
field(*settingsValue,
"subStepCount")) {
151 const auto* subSteps = subStepsValue->getIf<std::int64_t>();
153 return editing::failed<PhysicsPlacementRequest>(
154 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-substeps"),
155 "Physics placement subStepCount must be an integer");
156 request.settings.subStepCount =
static_cast<int>(*subSteps);
158 if (
const auto* preserveValue =
field(*settingsValue,
"preserveSelectionLayout")) {
159 const auto* preserve = preserveValue->getIf<
bool>();
161 return editing::failed<PhysicsPlacementRequest>(
162 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-layout"),
163 "Physics placement preserveSelectionLayout must be boolean");
164 request.settings.preserveSelectionLayout = *preserve;
166 const auto assignBool = [&](
const char*
key,
bool& destination) {
168 if (!
value)
return true;
169 const auto*
boolean =
value->getIf<
bool>();
170 if (!
boolean)
return false;
174 if (!assignBool(
"freezePositionX",
request.settings.freezePositionX) ||
175 !assignBool(
"freezePositionY",
request.settings.freezePositionY) ||
176 !assignBool(
"freezePositionZ",
request.settings.freezePositionZ) ||
177 !assignBool(
"freezeRotationX",
request.settings.freezeRotationX) ||
178 !assignBool(
"freezeRotationY",
request.settings.freezeRotationY) ||
179 !assignBool(
"freezeRotationZ",
request.settings.freezeRotationZ) ||
180 !assignBool(
"scaleSpeedByBounds",
request.settings.scaleSpeedByBounds) ||
181 !assignBool(
"softCollision",
request.settings.softCollision))
182 return editing::failed<PhysicsPlacementRequest>(
183 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-setting-bool"),
184 "Physics placement boolean settings must be boolean");
185 if (
const auto* policyValue =
field(*settingsValue,
"colliderPolicy")) {
186 const auto* policy = policyValue->getIf<std::string>();
187 if (!policy || (*policy !=
"existingOnly" && *policy !=
"generatedWhenMissing" &&
188 *policy !=
"generatedOnly" && *policy !=
"existingAndGenerated"))
189 return editing::failed<PhysicsPlacementRequest>(
190 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-collider-policy"),
191 "Physics placement colliderPolicy is unsupported");
192 if (*policy ==
"existingOnly")
194 else if (*policy ==
"generatedOnly")
196 else if (*policy ==
"existingAndGenerated")
202 if (
const auto* admissionValue =
field(
payload,
"admission")) {
204 return editing::failed<PhysicsPlacementRequest>(editing::Status::Rejected,
206 "Physics placement admission must be an object");
207 for (
const auto& [
key, destination] :
std::initializer_list<
std::pair<const char*,
std::uint64_t*>>{
208 {
"includedLayerBits", &
request.admission.includedLayerBits},
209 {
"excludedLayerBits", &
request.admission.excludedLayerBits}}) {
210 if (
const auto* candidate =
field(*admissionValue,
key)) {
211 const auto*
integer = candidate->getIf<std::int64_t>();
212 if (!integer || *integer < 0)
213 return editing::failed<PhysicsPlacementRequest>(
214 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-admission-layer"),
215 "Admission layer masks must be non-negative integers");
216 *destination =
static_cast<std::uint64_t
>(*integer);
219 for (
const auto& [
key, destination] :
std::initializer_list<
std::pair<const char*, bool*>>{
220 {
"excludeDisabled", &
request.admission.excludeDisabled},
221 {
"excludeLocked", &
request.admission.excludeLocked}}) {
222 if (
const auto* candidate =
field(*admissionValue,
key)) {
223 const auto*
boolean = candidate->getIf<
bool>();
225 return editing::failed<PhysicsPlacementRequest>(
226 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-admission-flag"),
227 "Admission flags must be boolean");
231 if (
const auto* tagsValue =
field(*admissionValue,
"requiredTags")) {
234 return editing::failed<PhysicsPlacementRequest>(
235 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-admission-tags"),
236 "Admission requiredTags must be an array");
237 for (
const auto& candidate : *tags) {
238 const auto*
tag = candidate.getIf<std::string>();
240 return editing::failed<PhysicsPlacementRequest>(
241 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-admission-tag"),
242 "Admission requiredTags must contain strings");
243 request.admission.requiredTags.push_back(*
tag);
246 const auto* minimumValue =
field(*admissionValue,
"minimum");
247 const auto* maximumValue =
field(*admissionValue,
"maximum");
248 if (minimumValue || maximumValue) {
258 return editing::failed<PhysicsPlacementRequest>(
259 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-admission-bounds"),
260 "Admission bounds require minimum and maximum numeric vectors");
261 request.admission.useBounds =
true;
264 for (
const auto&
value : *objects) {
266 const auto*
id = idValue ? idValue->getIf<std::string>() : nullptr;
267 const auto* selectedValue =
field(
value,
"selected");
268 const auto* selected = selectedValue ? selectedValue->getIf<
bool>() : nullptr;
271 PhysicsPlacementObject entry;
274 return editing::failed<PhysicsPlacementRequest>(
275 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-object"),
276 "Placement entries require object, selected and three half extents");
278 entry.selected = *selected;
279 if (
const auto* shapeValue =
field(
value,
"shape")) {
280 const auto*
shape = shapeValue->getIf<std::string>();
283 return editing::failed<PhysicsPlacementRequest>(
284 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-shape"),
285 "Placement shape must be box, sphere, capsule, convex or auto");
291 if (
const auto* verticesValue =
field(
value,
"vertices")) {
294 return editing::failed<PhysicsPlacementRequest>(
295 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-vertices"),
296 "Convex placement vertices must be a numeric array");
297 entry.convexVertices.reserve(
vertices->size());
298 for (
const auto& vertex : *
vertices) {
299 double component = 0.0;
300 if (!
number(vertex, component))
301 return editing::failed<PhysicsPlacementRequest>(
302 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-vertex"),
303 "Convex placement vertices must be numeric");
304 entry.convexVertices.push_back(
static_cast<float>(component));
307 if (
const auto* maximumValue =
field(
value,
"maxHullVertices")) {
308 const auto*
maximum = maximumValue->getIf<std::int64_t>();
310 return editing::failed<PhysicsPlacementRequest>(
311 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-hull-budget"),
312 "Convex hull vertex budget must be an integer");
313 entry.convexMaxVertices =
static_cast<int>(*maximum);
315 if (
const auto* keyValue =
field(
value,
"colliderResourceKey")) {
316 const auto*
key = keyValue->getIf<std::string>();
318 return editing::failed<PhysicsPlacementRequest>(
319 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-resource-key"),
320 "colliderResourceKey must be a string");
321 entry.colliderResourceKey = *
key;
323 if (
const auto* layerValue =
field(
value,
"layerBits")) {
324 const auto*
layer = layerValue->getIf<std::int64_t>();
326 return editing::failed<PhysicsPlacementRequest>(editing::Status::Rejected,
328 "layerBits must be a non-negative integer");
329 entry.layerBits =
static_cast<std::uint64_t
>(*layer);
331 for (
const auto& [
key, destination] :
std::initializer_list<
std::pair<const char*, bool*>>{
332 {
"enabled", &entry.enabled}, {
"locked", &entry.locked}}) {
334 const auto*
boolean = candidate->getIf<
bool>();
336 return editing::failed<PhysicsPlacementRequest>(
337 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-object-flag"),
338 "Placement object flags must be boolean");
342 if (
const auto* tagsValue =
field(
value,
"tags")) {
345 return editing::failed<PhysicsPlacementRequest>(editing::Status::Rejected,
347 "Placement tags must be an array");
348 for (
const auto& candidate : *tags) {
349 const auto*
tag = candidate.getIf<std::string>();
351 return editing::failed<PhysicsPlacementRequest>(
352 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-tag"),
353 "Placement tags must contain strings");
354 entry.tags.push_back(*
tag);
357 if (
const auto* collidersValue =
field(
value,
"colliders")) {
360 return editing::failed<PhysicsPlacementRequest>(
361 editing::Status::Rejected,
editing::RuleId(
"scene.physics-placement.script-colliders"),
362 "Placement colliders must be an array");
363 for (
const auto& colliderValue : *colliders) {
364 auto collider = placementCollider(colliderValue);
366 entry.colliders.push_back(std::move(collider.value()));
369 request.objects.push_back(std::move(entry));
371 return editing::applied<PhysicsPlacementRequest>(std::move(
request));
374editing::Value placementValue(
const PhysicsPlacementFrame& frame) {
376 for (
const auto& entry : frame.objects) {
377 const auto&
value = entry.transform;
379 {
"object", entry.object.value()},
380 {
"selected", entry.selected},
387 {
"tick",
static_cast<std::int64_t
>(frame.tick)},
388 {
"colliding", frame.colliding},
389 {
"settled", frame.settled},
390 {
"surfaceAligned", frame.surfaceAligned},
391 {
"surfaceNormal",
editing::Value::Array{frame.surfaceNormalX, frame.surfaceNormalY, frame.surfaceNormalZ}},
392 {
"objects", std::move(objects)}};
397 const auto vm = table.getHandle();
399 "SceneEditorSession", std::function<SceneEditorSession*()>([]() ->
SceneEditorSession* {
return nullptr; }),
411 std::vector<std::string>
ids;
412 for (std::size_t i = 0; i <
commands.size(); ++i)
ids.push_back(
commands.get<std::string>(i));
427 ssq::Array
vertices,
int maxVertices) {
428 std::vector<float> copied;
435 const std::string& resourceKey, ssq::Array
parts) {
440 std::vector<PhysicsPlacementCollider> colliders;
442 colliders.reserve(
values->size());
443 for (
const auto& partValue : *
values) {
444 auto part = placementCollider(partValue);
446 colliders.push_back(std::move(part.value()));
453 cls.addFunc(
"restorePhysicsPlacementColliderCacheJson", [
vm](
SceneEditorSession* self,
const std::string& json) {
476 float rotationY,
float rotationZ,
float dt) {
482 cls.addFunc(
"updatePhysicsPlacementTransform",
487 "Placement transform arrays require three values")));
491 scale.get<
float>(2), dt);
496 cls.addFunc(
"alignPhysicsPlacementToSurface",
498 if (
from.size() != 3 ||
to.size() != 3)
501 "Surface alignment endpoints require three values")));
503 from.get<
float>(2),
to.get<
float>(0),
504 to.get<
float>(1),
to.get<
float>(2),
offset, dt);
509 cls.addFunc(
"commitPhysicsPlacement",
516 module.addFunc("createSession", [vm](SceneEditorModule*, const std::string& id) {
518 return script::projectStatusResult(
520 Diagnostic::error(DiagnosticCode::InvalidArgument, "Scene target id must not be empty")));
522 script::makeOwnedSquirrelInstance<SceneEditorSession>(
vm, std::make_unique<SceneEditorSession>(
id));
525 result.set(
"value", std::move(
object).takeValue());
526 result.set(
"ownership", std::string(
"owned"));
529 module.addFunc("createLiveSession", [vm](SceneEditorModule*, const std::string& id, const std::string& hostName) {
530 auto* scene = ModuleManager::getInstance<scene::Scene>("Scene");
531 if (!scene ||
id.empty())
535 "Live scene editing requires Scene module and a nonempty target id")));
538 auto object = script::makeOwnedSquirrelInstance<SceneEditorSession>(
539 vm, std::make_unique<SceneEditorSession>(
540 std::make_unique<scene_editing::SceneHostEditorTarget>(
id,
host.value())));
543 result.set(
"value", std::move(
object).takeValue());
544 result.set(
"ownership", std::string(
"owned"));
std::vector< eve::artifact::PartView > parts
std::map< std::string, Var > values
const GltfImportRequest & request
wgpu::PopErrorScopeStatus status
HexCoordinates to
Cell the unit walks towards on this segment.
std::array< float, 4 > rotation
std::array< float, 3 > position
std::array< float, 3 > scale
std::vector< Point > vertices
std::weak_ptr< PrimitiveScene > scene
The single Squirrel projection for common Result, Status and Value.
Explicit Value/Owned/Borrowed semantics at the Squirrel boundary.
const UnitySourceAsset & source
std::vector< VegetationPresetCommand > commands
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.
Move-only operation result carrying either a value or Status.
static Result failure(Status status)
Construct a failed result from a structured status.
static Status failure(StatusCode code, Diagnostic diagnostic)
Construct a failed status with one diagnostic.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
UUID-backed identifier adapter for legacy textual boundaries.
const T * getIf() const noexcept
Return a typed pointer, or nullptr when the kind differs.
std::vector< Value > Array
std::map< std::string, Value > Object
const T * getIf() const
Return an immediately borrowed typed value.
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.
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.
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.
eve::Value toPresentationValue(const Value &value)
To presentation value.
StrongId< ObjectIdTag > ObjectId
StrongId< RuleIdTag > RuleId
Value toEditingValue(const eve::Value &value)
To editing value.
const EditorValue * field(const EditorValue &value, const char *name)
void exposeSceneEditorSessions(ssq::Table &table, ssq::Class &module)
Register owned, UI-neutral scene editing sessions in the supplied VM; owner-thread only.
eve::Value boundsValue(const eve::artifact::Bounds &bounds)
ssq::Table projectStatusResult(HSQUIRRELVM vm, const Status &status)
Project a checked native status that carries no payload.
Result< Value > valueFromSquirrel(HSQUIRRELVM vm, SQInteger index, const SquirrelValueOptions &options)
Convert one Squirrel value into the canonical owning Value tree.
int64_t integer(const RuntimeTensor &v, size_t i=0)
Integer.