29#include <SimoxUtility/algorithm/string/string_tools.h>
30#include <SimoxUtility/json.h>
31#include <VirtualRobot/BoundingBox.h>
32#include <VirtualRobot/CollisionDetection/CollisionChecker.h>
33#include <VirtualRobot/CollisionDetection/CollisionModel.h>
34#include <VirtualRobot/Obstacle.h>
47 const std::vector<std::string> contextMenuEntries = {
57 const std::string noneOption =
"<none>";
59 constexpr float rad2deg = 180.0f /
M_PI;
60 constexpr float deg2rad =
M_PI / 180.0f;
68 "Package containing the object models.");
72 "Package to whose data/<package> directory scenes are saved to and "
73 "loaded from (if SceneStorageDirectory is empty and the scene file "
74 "is not an absolute path).");
76 "SceneStorageDirectory",
78 "Directory that directly holds the scene files (new scenes are "
79 "created here and loaded from here). Grounding info is written to a "
80 "parallel 'groundings' directory (the scene path with its last "
81 "'scenes' component replaced by 'groundings'). If empty, the "
82 "ScenesPackage's <data>/<package>/scenes directory is used. "
83 "Use an absolute path for reliable resolution (a relative path is "
84 "resolved against the process working directory, not the package).");
88 "Optional class ID (Dataset/ClassName) of an object that is spawned "
89 "at the origin and designated as the ground object.");
91 "GroundZ", 0.0f,
"Initial height (z, in mm) of the ground plane objects are locked to.");
95 "Initial scene file (name in the scenes directory or absolute path).");
101 return "SceneEditor";
127 std::scoped_lock lock(mutex_);
131 for (
const auto& [dataset, infos] : objectFinder_.findAllObjectsByDataset(
true))
133 std::vector<std::string>& classNames = objectsByDataset_[dataset];
136 classNames.push_back(info.id().className());
138 std::sort(classNames.begin(), classNames.end());
141 catch (
const std::exception& e)
143 ARMARX_WARNING <<
"Failed to enumerate objects of package '" << objectsPackage_
144 <<
"': " << e.what();
146 if (!objectsByDataset_.empty())
148 currentDataset_ = objectsByDataset_.begin()->first;
150 ARMARX_INFO <<
"Found " << objectsByDataset_.size() <<
" datasets in package '"
151 << objectsPackage_ <<
"'.";
153 if (!initialGroundClass_.empty())
155 if (objectFinder_.findObject(initialGroundClass_))
157 Entry& ground = addObject(initialGroundClass_);
159 ground.manualPose =
true;
160 ground.groundRef.clear();
164 ARMARX_WARNING <<
"Ground object '" << initialGroundClass_ <<
"' not found.";
169 createRemoteGuiTab();
181 const bool join =
true;
193 SceneEditor::createRemoteGuiTab()
197 std::vector<std::string> datasets;
198 for (
const auto& datasetEntry : objectsByDataset_)
200 datasets.push_back(datasetEntry.first);
202 if (datasets.empty())
204 datasets.push_back(noneOption);
206 tab_.dataset.setOptions(datasets);
207 if (!currentDataset_.empty())
209 tab_.dataset.setValue(currentDataset_);
212 std::vector<std::string> classNames;
213 if (
auto it = objectsByDataset_.find(currentDataset_); it != objectsByDataset_.end())
215 classNames = it->second;
217 if (classNames.empty())
219 classNames.push_back(noneOption);
221 tab_.objectClass.setOptions(classNames);
223 std::vector<std::string> instanceOptions = {noneOption};
224 for (
const Entry& entry : entries_)
226 instanceOptions.push_back(entry.data.instanceName);
228 tab_.groundObject.setOptions(instanceOptions);
230 const Entry* selected = findEntry(selected_);
231 tab_.groundObject.setValue(
232 (selected && !selected->groundRef.empty()) ? selected->groundRef : noneOption);
236 const std::string previousAlignTarget =
237 guiInitialized_ ? tab_.alignTarget.getValue() : std::string();
238 tab_.alignTarget.setOptions(instanceOptions);
239 if (std::find(instanceOptions.begin(), instanceOptions.end(), previousAlignTarget) !=
240 instanceOptions.end())
242 tab_.alignTarget.setValue(previousAlignTarget);
245 if (!guiInitialized_)
247 guiInitialized_ =
true;
249 tab_.addObject.setLabel(
"Add object");
251 tab_.groundZ.setRange(-100000.0f, 100000.0f);
252 tab_.groundZ.setDecimals(1);
253 tab_.groundZ.setSteps(2000);
254 tab_.groundZ.setValue(groundZ_);
256 tab_.selectedInfo.setText(
"<none>");
257 tab_.instanceName.setValue(
"");
258 tab_.renameObject.setLabel(
"Rename");
259 for (FloatSpinBox* spin : {&tab_.posX, &tab_.posY, &tab_.posZ})
261 spin->setRange(-100000.0f, 100000.0f);
262 spin->setDecimals(1);
263 spin->setSteps(2000);
264 spin->setValue(0.0f);
266 tab_.yaw.setRange(-180.0f, 180.0f);
267 tab_.yaw.setDecimals(1);
268 tab_.yaw.setSteps(720);
269 tab_.yaw.setValue(0.0f);
271 tab_.alignOutside.setValue(
false);
272 tab_.frontXZ.setLabel(
"Front XZ plane (y min)");
273 tab_.backXZ.setLabel(
"Back XZ plane (y max)");
274 tab_.frontYZ.setLabel(
"Front YZ plane (x min)");
275 tab_.backYZ.setLabel(
"Back YZ plane (x max)");
277 tab_.isStatic.setValue(
true);
278 tab_.liveApply.setValue(
false);
279 tab_.applyPose.setLabel(
"Apply pose (manual)");
280 tab_.lockToGround.setLabel(
"Lock to ground");
281 tab_.flipX.setLabel(
"Flip X (+90°)");
282 tab_.flipY.setLabel(
"Flip Y (+90°)");
283 tab_.flipZ.setLabel(
"Flip Z (+90°)");
284 tab_.deleteObject.setLabel(
"Delete object");
286 tab_.sceneFile.setValue(initialSceneFile_);
287 tab_.saveScene.setLabel(
"Save scene");
288 tab_.loadScene.setLabel(
"Load scene");
289 tab_.clearScene.setLabel(
"Clear scene");
290 tab_.status.setText(
"");
293 GroupBox importGroup;
294 importGroup.setLabel(
"Import object");
297 grid.add(
Label(
"Dataset"), {0, 0}).add(tab_.dataset, {0, 1});
298 grid.add(
Label(
"Object"), {1, 0}).add(tab_.objectClass, {1, 1});
299 grid.add(tab_.addObject, {2, 0}, {1, 2});
300 importGroup.addChild(grid);
303 GroupBox groundGroup;
304 groundGroup.setLabel(
"Default ground");
307 grid.add(
Label(
"Default ground z (mm)"), {0, 0}).add(tab_.groundZ, {0, 1});
308 groundGroup.addChild(grid);
311 GroupBox selectedGroup;
312 selectedGroup.setLabel(
"Selected object (click an object in ArViz to select)");
316 grid.add(tab_.selectedInfo, {row++, 0}, {1, 4});
317 grid.add(
Label(
"Instance name"), {row, 0})
318 .add(tab_.instanceName, {row, 1}, {1, 2})
319 .add(tab_.renameObject, {row, 3});
321 grid.add(
Label(
"Grounded to"), {row, 0}).add(tab_.groundObject, {row, 1}, {1, 3});
323 grid.add(
Label(
"Static"), {row, 0}).add(tab_.isStatic, {row, 1});
325 grid.add(
Label(
"x (mm)"), {row, 0})
326 .add(tab_.posX, {row, 1})
327 .add(
Label(
"y (mm)"), {row, 2})
328 .add(tab_.posY, {row, 3});
330 grid.add(
Label(
"z (mm)"), {row, 0})
331 .add(tab_.posZ, {row, 1})
332 .add(
Label(
"yaw (°)"), {row, 2})
333 .add(tab_.yaw, {row, 3});
335 grid.add(
Label(
"Apply immediately"), {row, 0}).add(tab_.liveApply, {row, 1});
337 grid.add(tab_.applyPose, {row, 0}, {1, 2}).add(tab_.lockToGround, {row, 2}, {1, 2});
339 grid.add(tab_.flipX, {row, 0}).add(tab_.flipY, {row, 1}).add(tab_.flipZ, {row, 2});
340 grid.add(tab_.deleteObject, {row, 3});
341 selectedGroup.addChild(grid);
346 "Align the selected object's bounding box face with a reference object");
350 grid.add(
Label(
"Reference object"), {row, 0}).add(tab_.alignTarget, {row, 1});
352 grid.add(
Label(
"Align from outside (touching)"), {row, 0})
353 .add(tab_.alignOutside, {row, 1});
355 grid.add(tab_.frontYZ, {row, 0}).add(tab_.backYZ, {row, 1});
357 grid.add(tab_.frontXZ, {row, 0}).add(tab_.backXZ, {row, 1});
358 alignGroup.addChild(grid);
362 fileGroup.setLabel(
"Scene file");
365 grid.add(
Label(
"File"), {0, 0}).add(tab_.sceneFile, {0, 1}, {1, 3});
366 grid.add(tab_.saveScene, {1, 0})
367 .add(tab_.loadScene, {1, 1})
368 .add(tab_.clearScene, {1, 2});
369 fileGroup.addChild(grid);
372 VBoxLayout root = {importGroup, groundGroup, selectedGroup, alignGroup,
373 fileGroup, tab_.status, VSpacer()};
386 catch (
const std::exception& e)
395 std::scoped_lock lock(mutex_);
397 if (tab_.dataset.hasValueChanged())
399 const std::string dataset = tab_.dataset.getValue();
400 if (!dataset.empty() && dataset != currentDataset_)
402 currentDataset_ = dataset;
403 tabRebuildNeeded_ =
true;
408 if (tab_.groundObject.hasValueChanged())
410 const std::string value = tab_.groundObject.getValue();
411 const std::string ref = (value == noneOption) ?
"" : value;
412 if (Entry* entry = findEntry(selected_); entry && ref != entry->groundRef)
414 if (ref == entry->data.instanceName)
416 status_ =
"An object cannot be grounded to itself.";
421 entry->groundRef = ref;
422 if (!entry->manualPose)
424 entry->data.position.z() = groundSnappedZ(*entry, poseOf(entry->data));
425 sceneLayerDirty_ =
true;
428 status_ =
"Grounded '" + selected_ +
"' to " +
429 (ref.empty() ? std::string(
"the default ground.")
435 if (tab_.addObject.wasClicked())
437 const std::string className = tab_.objectClass.getValue();
438 if (className.empty() || className == noneOption || currentDataset_.empty() ||
439 currentDataset_ == noneOption)
441 status_ =
"No object class selected.";
445 Entry& entry = addObject(currentDataset_ +
"/" + className);
451 if (tab_.groundZ.hasValueChanged())
453 const float value = tab_.groundZ.getValue();
454 if (std::abs(value - groundZ_) > 0.01f)
460 Entry* selected = findEntry(selected_);
462 if (tab_.isStatic.hasValueChanged())
464 const bool isStatic = tab_.isStatic.getValue();
468 status_ =
"'" + selected_ +
"' is now " +
469 (isStatic ?
"static." :
"dynamic.");
473 const bool applyClicked = tab_.applyPose.wasClicked();
474 bool poseEdited =
false;
476 poseEdited |= tab_.posX.hasValueChanged();
477 poseEdited |= tab_.posY.hasValueChanged();
478 poseEdited |= tab_.posZ.hasValueChanged();
479 poseEdited |= tab_.yaw.hasValueChanged();
480 const bool liveApply = tab_.liveApply.getValue() && poseEdited;
482 if (applyClicked || liveApply)
486 const Eigen::Vector3f position(
487 tab_.posX.getValue(), tab_.posY.getValue(), tab_.posZ.getValue());
488 const float yawDeg = tab_.yaw.getValue();
492 const float yawDiff = std::remainder(
494 const bool differs = (position - selected->data.
position).
norm() > 0.01f ||
495 std::abs(yawDiff) > 1e-3f;
496 if (applyClicked || differs)
498 if (applyManualPose(*selected, position, yawDeg))
501 status_ =
"Applied manual pose to '" + selected_ +
502 "' (unlocked from ground).";
506 else if (applyClicked)
508 status_ =
"No object selected.";
512 if (tab_.renameObject.wasClicked())
516 renameEntry(*selected, tab_.instanceName.getValue());
520 status_ =
"No object selected.";
524 if (tab_.lockToGround.wasClicked())
528 status_ =
"No object selected.";
532 selected->manualPose =
false;
533 selected->data.
position.z() = groundSnappedZ(*selected, poseOf(selected->data));
534 sceneLayerDirty_ =
true;
536 status_ =
"Locked '" + selected_ +
"' to " +
537 (selected->groundRef.empty() ? std::string(
"the default ground.")
538 :
"'" + selected->groundRef +
"'.");
542 const Eigen::Vector3f flipAxes[] = {
543 Eigen::Vector3f::UnitX(), Eigen::Vector3f::UnitY(), Eigen::Vector3f::UnitZ()};
545 for (
int axis = 0; axis < 3; ++axis)
547 if (flipButtons[axis]->wasClicked())
551 flipEntry(*selected, flipAxes[axis]);
555 status_ =
"No object selected.";
560 if (tab_.deleteObject.wasClicked())
564 status_ =
"Deleted '" + selected_ +
"'.";
565 deleteEntry(selected_);
570 status_ =
"No object selected.";
574 if (tab_.saveScene.wasClicked())
576 saveScene(tab_.sceneFile.getValue());
579 if (tab_.loadScene.wasClicked())
581 loadScene(tab_.sceneFile.getValue());
584 if (tab_.frontYZ.wasClicked())
586 alignToPlane(0,
true,
"front yz-plane");
588 if (tab_.backYZ.wasClicked())
590 alignToPlane(0,
false,
"back yz-plane");
592 if (tab_.frontXZ.wasClicked())
594 alignToPlane(1,
true,
"front xz-plane");
596 if (tab_.backXZ.wasClicked())
598 alignToPlane(1,
false,
"back xz-plane");
601 if (tab_.clearScene.wasClicked())
604 pendingTransforms_.clear();
605 collisionModels_.clear();
607 sceneLayerDirty_ =
true;
608 groundLayerDirty_ =
true;
610 tabRebuildNeeded_ =
true;
611 status_ =
"Cleared scene.";
617 if (Entry* entry = findEntry(selected_))
619 const std::string grounding =
621 ? std::string(
", manual pose)")
622 : (entry->groundRef.empty()
623 ? std::string(
", locked to default ground)")
624 :
", locked to '" + entry->groundRef +
"')");
625 tab_.selectedInfo.setText(entry->data.instanceName +
" (" +
626 entry->data.className + grounding);
627 tab_.instanceName.setValue(entry->data.instanceName);
628 tab_.isStatic.setValue(entry->data.isStatic.value_or(
true));
629 tab_.posX.setValue(entry->data.position.x());
630 tab_.posY.setValue(entry->data.position.y());
631 tab_.posZ.setValue(entry->data.position.z());
632 tab_.yaw.setValue(yawOf(entry->data.orientation) * rad2deg);
633 tab_.groundObject.setValue(entry->groundRef.empty() ? noneOption
638 tab_.selectedInfo.setText(
"<none>");
639 tab_.instanceName.setValue(
"");
640 tab_.groundObject.setValue(noneOption);
642 tab_.groundZ.setValue(groundZ_);
645 tab_.status.setText(status_);
647 if (tabRebuildNeeded_)
649 tabRebuildNeeded_ =
false;
650 createRemoteGuiTab();
662 std::scoped_lock lock(mutex_);
665 stage.
add(sceneLayer_);
666 stage.
add(groundLayer_);
673 std::scoped_lock lock(mutex_);
675 rebuildGroundLayer();
676 stage.
add(sceneLayer_);
677 stage.
add(groundLayer_);
678 sceneLayerDirty_ =
false;
679 groundLayerDirty_ =
false;
687 CycleUtil cycle(10.0f);
688 while (!task_->isStopped())
690 result =
arviz.commit(stage);
695 std::scoped_lock lock(mutex_);
699 for (
const viz::InteractionFeedback& interaction : result.
interactions())
701 handleInteraction(interaction);
704 if (sceneLayerDirty_)
707 stage.
add(sceneLayer_);
708 sceneLayerDirty_ =
false;
710 if (groundLayerDirty_)
712 rebuildGroundLayer();
713 stage.
add(groundLayer_);
714 groundLayerDirty_ =
false;
718 cycle.waitForCycleDuration();
759 flipEntry(*entry, Eigen::Vector3f::UnitX());
762 flipEntry(*entry, Eigen::Vector3f::UnitY());
765 flipEntry(*entry, Eigen::Vector3f::UnitZ());
768 entry->manualPose = !entry->manualPose;
769 if (!entry->manualPose)
771 entry->data.position.z() =
772 groundSnappedZ(*entry, poseOf(entry->data));
774 sceneLayerDirty_ =
true;
792 SceneEditor::applyPendingTransform(
const std::string& instanceName)
794 auto it = pendingTransforms_.find(instanceName);
795 if (it == pendingTransforms_.end())
799 const Eigen::Matrix4f
transform = it->second;
800 pendingTransforms_.erase(it);
802 Entry* entry = findEntry(instanceName);
810 const Eigen::Matrix4f candidate =
transform * poseOf(entry->data);
812 Eigen::Vector3f newPosition = candidate.block<3, 1>(0, 3);
814 if (entry->manualPose)
823 newOrientation = (Eigen::AngleAxisf(deltaYaw, Eigen::Vector3f::UnitZ()) *
824 entry->data.orientation)
828 Eigen::Matrix4f newPose = Eigen::Matrix4f::Identity();
829 newPose.block<3, 3>(0, 0) = newOrientation.toRotationMatrix();
830 newPose.block<3, 1>(0, 3) = newPosition;
832 if (!entry->manualPose)
835 newPosition.z() = groundSnappedZ(*entry, newPose);
836 newPose(2, 3) = newPosition.z();
841 sceneLayerDirty_ =
true;
844 if (!isMoveAllowed(*entry, newPose))
849 entry->data.position = newPosition;
850 entry->data.orientation = newOrientation;
851 afterEntryPoseChanged(*entry);
855 SceneEditor::applyManualPose(Entry& entry,
const Eigen::Vector3f& position,
float yawDeg)
857 const float deltaYaw =
858 std::remainder(yawDeg * deg2rad - yawOf(entry.data.orientation), 2.0 *
M_PI);
860 (Eigen::AngleAxisf(deltaYaw, Eigen::Vector3f::UnitZ()) * entry.data.orientation)
863 Eigen::Matrix4f newPose = Eigen::Matrix4f::Identity();
864 newPose.block<3, 3>(0, 0) = newOrientation.toRotationMatrix();
865 newPose.block<3, 1>(0, 3) = position;
867 if (!isMoveAllowed(entry, newPose))
874 entry.manualPose =
true;
875 entry.data.position = position;
876 entry.data.orientation = newOrientation;
877 sceneLayerDirty_ =
true;
879 afterEntryPoseChanged(entry);
884 SceneEditor::collidesWith(
const Entry& entry,
const Eigen::Matrix4f& candidatePose)
886 VirtualRobot::ObstaclePtr model = collisionModelOf(entry);
887 if (!model || !model->getCollisionModel())
891 model->setGlobalPose(candidatePose);
893 auto checker = VirtualRobot::CollisionChecker::getGlobalCollisionChecker();
894 for (
const Entry& other : entries_)
896 if (&other == &entry)
901 if (other.data.instanceName == entry.groundRef ||
902 other.groundRef == entry.data.instanceName)
906 VirtualRobot::ObstaclePtr otherModel = collisionModelOf(other);
907 if (!otherModel || !otherModel->getCollisionModel())
911 otherModel->setGlobalPose(poseOf(other.data));
912 if (checker->checkCollision(model->getCollisionModel(),
913 otherModel->getCollisionModel()))
915 return other.data.instanceName;
922 SceneEditor::isMoveAllowed(Entry& entry,
const Eigen::Matrix4f& candidatePose)
926 const bool wasColliding = !collidesWith(entry, poseOf(entry.data)).empty();
931 const std::string hit = collidesWith(entry, candidatePose);
934 status_ =
"Move of '" + entry.data.instanceName +
"' rejected: collides with '" +
941 const std::optional<simox::AxisAlignedBoundingBox>&
942 SceneEditor::localAabbOf(
const Entry& entry)
944 auto it = localAabbs_.find(entry.data.className);
945 if (it == localAabbs_.end())
947 std::optional<simox::AxisAlignedBoundingBox> aabb;
950 if (std::optional<ObjectInfo> info =
951 objectFinder_.findObject(entry.data.className))
953 info->setLogError(
false);
954 aabb = info->loadAABB();
957 catch (
const std::exception& e)
959 ARMARX_WARNING <<
"Failed to load AABB of '" << entry.data.className
960 <<
"': " << e.what();
964 ARMARX_INFO <<
"No AABB (aabb.json) for '" << entry.data.className
965 <<
"', falling back to the collision model's bounding box.";
967 it = localAabbs_.emplace(entry.data.className, aabb).first;
972 std::optional<simox::AxisAlignedBoundingBox>
973 SceneEditor::globalAabb(
const Entry& entry,
const Eigen::Matrix4f& pose)
979 if (
const std::optional<simox::AxisAlignedBoundingBox>& aabb = localAabbOf(entry))
984 else if (VirtualRobot::ObstaclePtr model = collisionModelOf(entry);
985 model && model->getCollisionModel())
987 const VirtualRobot::BoundingBox bbox =
988 model->getCollisionModel()->getBoundingBox(
false);
998 const Eigen::Matrix3f rotation = pose.block<3, 3>(0, 0);
999 const Eigen::Vector3f translation = pose.block<3, 1>(0, 3);
1000 Eigen::Vector3f globalMin =
1001 Eigen::Vector3f::Constant(std::numeric_limits<float>::max());
1002 Eigen::Vector3f globalMax =
1003 Eigen::Vector3f::Constant(std::numeric_limits<float>::lowest());
1004 for (
int i = 0; i < 8; ++i)
1006 const Eigen::Vector3f corner((i & 1) ?
max.x() :
min.x(),
1007 (i & 2) ?
max.y() :
min.y(),
1008 (i & 4) ?
max.z() :
min.z());
1009 const Eigen::Vector3f global = rotation * corner + translation;
1010 globalMin = globalMin.cwiseMin(global);
1011 globalMax = globalMax.cwiseMax(global);
1013 return simox::AxisAlignedBoundingBox(globalMin, globalMax);
1017 SceneEditor::groundHeightOf(
const std::string& ref)
1023 Entry* ground = findEntry(ref);
1028 if (
const auto aabb = globalAabb(*ground, poseOf(ground->data)))
1030 return aabb->max().z();
1036 SceneEditor::groundHeightFor(
const Entry& entry)
1038 return groundHeightOf(entry.groundRef);
1042 SceneEditor::reseatEntriesGroundedTo(
const std::string& ref)
1044 for (Entry& entry : entries_)
1046 if (!entry.manualPose && entry.groundRef == ref &&
1047 entry.data.instanceName != ref)
1049 entry.data.position.z() = groundSnappedZ(entry, poseOf(entry.data));
1050 sceneLayerDirty_ =
true;
1056 SceneEditor::afterEntryPoseChanged(
const Entry& entry)
1058 reseatEntriesGroundedTo(entry.data.instanceName);
1062 SceneEditor::groundSnappedZ(
const Entry& entry,
const Eigen::Matrix4f& candidatePose)
1064 const float ground = groundHeightFor(entry);
1065 const auto aabb = globalAabb(entry, candidatePose);
1070 return candidatePose(2, 3) + (ground - aabb->min().z());
1073 VirtualRobot::ObstaclePtr
1074 SceneEditor::collisionModelOf(
const Entry& entry)
1076 auto it = collisionModels_.find(entry.data.instanceName);
1077 if (it != collisionModels_.end())
1082 VirtualRobot::ObstaclePtr model;
1087 catch (
const std::exception& e)
1089 ARMARX_WARNING <<
"Failed to load collision model of '" << entry.data.className
1090 <<
"': " << e.what();
1094 ARMARX_WARNING <<
"No collision model for '" << entry.data.className
1095 <<
"', collision checks are skipped for '" << entry.data.instanceName
1098 collisionModels_[entry.data.instanceName] = model;
1103 SceneEditor::findEntry(
const std::string& instanceName)
1105 for (Entry& entry : entries_)
1107 if (entry.data.instanceName == instanceName)
1116 SceneEditor::addObject(
const std::string& classId)
1118 Entry& entry = entries_.emplace_back();
1119 entry.data.className = classId;
1120 entry.data.instanceName = makeUniqueInstanceName(classId);
1121 entry.groundRef.clear();
1122 entry.data.position = Eigen::Vector3f(0.0f, 0.0f, groundZ_);
1123 entry.data.orientation = Eigen::Quaternionf::Identity();
1124 entry.data.isStatic =
true;
1125 collisionModelOf(entry);
1126 entry.data.position.z() = groundSnappedZ(entry, poseOf(entry.data));
1127 sceneLayerDirty_ =
true;
1128 tabRebuildNeeded_ =
true;
1133 SceneEditor::renameEntry(Entry& entry,
const std::string& newName)
1135 const std::string oldName = entry.data.instanceName;
1136 if (newName.empty() || newName == noneOption)
1138 status_ =
"Cannot rename '" + oldName +
"': invalid name '" + newName +
"'.";
1142 if (newName == oldName)
1146 if (findEntry(newName) !=
nullptr)
1148 status_ =
"Cannot rename '" + oldName +
"': an object named '" + newName +
1149 "' already exists.";
1154 entry.data.instanceName = newName;
1157 for (Entry& other : entries_)
1159 if (other.groundRef == oldName)
1161 other.groundRef = newName;
1164 if (
auto node = collisionModels_.extract(oldName); !node.empty())
1166 node.key() = newName;
1167 collisionModels_.insert(std::move(node));
1169 if (
auto it = pendingTransforms_.find(oldName); it != pendingTransforms_.end())
1171 pendingTransforms_[newName] = it->second;
1172 pendingTransforms_.erase(it);
1174 if (selected_ == oldName)
1176 selected_ = newName;
1179 sceneLayerDirty_ =
true;
1181 tabRebuildNeeded_ =
true;
1182 status_ =
"Renamed '" + oldName +
"' to '" + newName +
"'.";
1186 SceneEditor::deleteEntry(
const std::string& instanceName)
1198 return e.data.instanceName == instanceName;
1203 pendingTransforms_.erase(instanceName);
1204 collisionModels_.erase(instanceName);
1206 for (Entry& entry : entries_)
1208 if (entry.groundRef == instanceName)
1210 entry.groundRef.clear();
1211 if (!entry.manualPose)
1213 entry.data.position.z() = groundSnappedZ(entry, poseOf(entry.data));
1217 if (selected_ == instanceName)
1221 sceneLayerDirty_ =
true;
1223 tabRebuildNeeded_ =
true;
1227 SceneEditor::flipEntry(Entry& entry,
const Eigen::Vector3f& axis)
1233 Eigen::Matrix4f newPose = Eigen::Matrix4f::Identity();
1234 newPose.block<3, 3>(0, 0) = newOrientation.toRotationMatrix();
1235 newPose.block<3, 1>(0, 3) = entry.data.position;
1238 if (!entry.manualPose)
1240 newPose(2, 3) = groundSnappedZ(entry, newPose);
1243 if (!isMoveAllowed(entry, newPose))
1248 entry.data.orientation = newOrientation;
1249 entry.data.position.z() = newPose(2, 3);
1250 sceneLayerDirty_ =
true;
1252 status_ =
"Flipped '" + entry.data.instanceName +
"'.";
1253 afterEntryPoseChanged(entry);
1257 SceneEditor::selectEntry(
const std::string& instanceName)
1259 if (findEntry(instanceName))
1261 selected_ = instanceName;
1267 SceneEditor::setGroundZ(
float groundZ)
1270 reseatEntriesGroundedTo(
"");
1271 groundLayerDirty_ =
true;
1276 SceneEditor::alignToPlane(
int axis,
bool minSide,
const std::string& planeName)
1278 Entry* selected = findEntry(selected_);
1281 status_ =
"No object selected.";
1284 const std::string targetName = tab_.alignTarget.getValue();
1285 Entry*
target = findEntry(targetName);
1288 status_ =
"No reference object selected.";
1291 if (target == selected)
1293 status_ =
"Cannot align an object with itself.";
1297 const auto selectedBox = globalAabb(*selected, poseOf(selected->data));
1298 const auto targetBox = globalAabb(*target, poseOf(
target->data));
1299 if (!selectedBox || !targetBox)
1301 status_ =
"Cannot align: missing bounding box for '" +
1302 (selectedBox ? targetName : selected_) +
"'.";
1310 const bool outside = tab_.alignOutside.getValue();
1311 constexpr float clearance = 0.5f;
1316 ? (targetBox->min()(axis) - clearance) - selectedBox->max()(axis)
1317 : (targetBox->
max()(axis) + clearance) - selectedBox->
min()(axis);
1321 delta = minSide ? targetBox->min()(axis) - selectedBox->min()(axis)
1322 : targetBox->
max()(axis) - selectedBox->
max()(axis);
1325 Eigen::Vector3f newPosition = selected->data.position;
1326 newPosition(axis) += delta;
1328 Eigen::Matrix4f newPose = poseOf(selected->data);
1329 newPose.block<3, 1>(0, 3) = newPosition;
1331 if (!isMoveAllowed(*selected, newPose))
1337 selected->data.position = newPosition;
1338 sceneLayerDirty_ =
true;
1340 status_ =
"Aligned '" + selected_ +
"' with the " + planeName +
" of '" + targetName +
1341 (outside ?
"' (outside)." :
"' (inside).");
1342 afterEntryPoseChanged(*selected);
1346 SceneEditor::makeUniqueInstanceName(
const std::string& classId)
1348 const std::string className =
ObjectID(classId).className();
1352 name = className +
"_" + std::to_string(nextId_++);
1353 }
while (findEntry(name) !=
nullptr);
1358 SceneEditor::rebuildSceneLayer()
1360 sceneLayer_ =
arviz.layer(
"Scene");
1361 for (
const Entry& entry : entries_)
1363 viz::Object object(entry.data.instanceName);
1364 object.fileByObjectFinder(entry.data.className, objectsPackage_);
1365 object.position(entry.data.position).orientation(entry.data.orientation);
1371 object.enable(interaction);
1373 sceneLayer_.add(
object);
1378 SceneEditor::rebuildGroundLayer()
1380 groundLayer_ =
arviz.layer(
"Ground");
1383 viz::Box plane(
"GroundPlane");
1384 plane.position(Eigen::Vector3f(0.0f, 0.0f, groundZ_ - 5.0f))
1385 .size(Eigen::Vector3f(10000.0f, 10000.0f, 10.0f))
1386 .color(viz::Color(128, 128, 128, 64));
1387 groundLayer_.add(plane);
1390 std::filesystem::path
1391 SceneEditor::resolveScenePath(std::string name)
const
1397 if (not simox::alg::ends_with(name,
".json"))
1401 std::filesystem::path path(name);
1402 if (!path.is_absolute())
1404 if (!sceneStorageDirectory_.empty())
1407 path = std::filesystem::path(sceneStorageDirectory_) / path;
1412 CMakePackageFinder packageFinder(scenesPackage_);
1413 const std::string dataDir = packageFinder.getDataDir();
1414 if (!packageFinder.packageFound() || dataDir.empty())
1416 throw LocalException(
1417 "Could not locate the scenes package '" + scenesPackage_ +
1418 "'. Set the SceneStorageDirectory property to an absolute path.");
1420 path = std::filesystem::path(dataDir) / scenesPackage_ /
"scenes" / path;
1426 if (!path.is_absolute())
1428 path = std::filesystem::absolute(path);
1430 return path.lexically_normal();
1433 std::filesystem::path
1434 SceneEditor::groundingPathFor(
const std::filesystem::path& scenePath)
const
1436 std::vector<std::filesystem::path> parts(scenePath.begin(), scenePath.end());
1437 int lastScenes = -1;
1438 for (std::size_t i = 0; i < parts.size(); ++i)
1440 if (parts[i].
string() ==
"scenes")
1442 lastScenes =
static_cast<int>(i);
1447 return scenePath.parent_path() / (scenePath.stem().
string() +
".groundings.json");
1449 std::filesystem::path result;
1450 for (std::size_t i = 0; i < parts.size(); ++i)
1452 result /= (
static_cast<int>(i) == lastScenes ? std::filesystem::path(
"groundings")
1459 SceneEditor::saveScene(
const std::string& fileArg)
1463 const std::filesystem::path path = resolveScenePath(fileArg);
1465 objects::Scene scene;
1468 std::set<std::string> takenNames;
1469 for (
const Entry& entry : entries_)
1471 takenNames.insert(entry.data.instanceName);
1473 std::map<std::string, int> perClassCount;
1474 for (
const Entry& entry : entries_)
1476 objects::SceneObject& obj = scene.objects.emplace_back(entry.data);
1477 if (!obj.instanceName.empty())
1481 const std::string className =
ObjectID(entry.data.className).className();
1485 className +
"_" + std::to_string(perClassCount[className]++);
1486 }
while (!takenNames.insert(obj.instanceName).second);
1489 std::filesystem::create_directories(path.parent_path());
1490 const simox::json::json j = scene;
1491 simox::json::write(path.string(), j, 2);
1495 const std::filesystem::path groundingPath = groundingPathFor(path);
1497 const auto indexOf = [
this](
const std::string& instanceName) ->
int
1499 for (std::size_t i = 0; i < entries_.size(); ++i)
1501 if (entries_[i].
data.instanceName == instanceName)
1503 return static_cast<int>(i);
1508 simox::json::json grounding;
1509 grounding[
"groundings"] = simox::json::json::array();
1510 for (std::size_t i = 0; i < entries_.size(); ++i)
1512 const Entry& entry = entries_[i];
1513 grounding[
"groundings"].push_back({
1514 {
"index",
static_cast<int>(i)},
1515 {
"instanceName", scene.objects[i].instanceName},
1516 {
"ground", entry.groundRef.empty() ? -1 : indexOf(entry.groundRef)},
1517 {
"locked", !entry.manualPose},
1520 std::filesystem::create_directories(groundingPath.parent_path());
1521 simox::json::write(groundingPath.string(), grounding, 2);
1523 status_ =
"Saved " + std::to_string(scene.objects.size()) +
" objects to " +
1524 path.string() +
" (grounding info: " + groundingPath.string() +
").";
1527 catch (
const std::exception& e)
1529 status_ = std::string(
"Saving scene failed: ") + e.what();
1535 SceneEditor::loadScene(
const std::string& fileArg)
1539 const std::filesystem::path path = resolveScenePath(fileArg);
1540 ARMARX_INFO <<
"Loading scene from " << path <<
".";
1541 if (!std::filesystem::exists(path))
1543 status_ =
"Scene file does not exist: " + path.string();
1548 const simox::json::json j = simox::json::read(path.string());
1549 const auto scene = j.get<objects::Scene>();
1552 pendingTransforms_.clear();
1553 collisionModels_.clear();
1556 for (
const objects::SceneObject& obj : scene.objects)
1558 Entry& entry = entries_.emplace_back();
1560 if (entry.data.instanceName.empty() ||
1561 findEntry(entry.data.instanceName) != &entry)
1563 entry.data.instanceName = makeUniqueInstanceName(entry.data.className);
1568 bool groundingLoaded =
false;
1569 const std::filesystem::path groundingPath = groundingPathFor(path);
1570 if (std::filesystem::exists(groundingPath))
1574 const simox::json::json grounding =
1575 simox::json::read(groundingPath.string());
1576 for (
const auto& item : grounding.at(
"groundings"))
1580 Entry* entry =
nullptr;
1581 const int index = item.value(
"index", -1);
1582 if (
index >= 0 &&
index <
static_cast<int>(entries_.size()))
1584 entry = &entries_[
index];
1586 else if (item.contains(
"instanceName"))
1588 entry = findEntry(item.at(
"instanceName").get<std::string>());
1595 entry->groundRef.clear();
1596 if (item.contains(
"ground"))
1598 const auto& ground = item.at(
"ground");
1599 if (ground.is_number_integer())
1601 const int groundIndex = ground.get<
int>();
1602 if (groundIndex >= 0 &&
1603 groundIndex <
static_cast<int>(entries_.size()))
1606 entries_[groundIndex].data.instanceName;
1609 else if (ground.is_string())
1611 entry->groundRef = ground.get<std::string>();
1614 entry->manualPose = !item.value(
"locked",
true);
1616 groundingLoaded =
true;
1618 catch (
const std::exception& e)
1620 ARMARX_WARNING <<
"Failed to load grounding info from " << groundingPath
1621 <<
": " << e.what();
1625 for (Entry& entry : entries_)
1627 if (!entry.groundRef.empty() &&
1628 (entry.groundRef == entry.data.instanceName ||
1629 findEntry(entry.groundRef) ==
nullptr))
1631 entry.groundRef.clear();
1633 if (groundingLoaded)
1635 if (!entry.manualPose)
1637 entry.data.position.z() = groundSnappedZ(entry, poseOf(entry.data));
1645 std::abs(entry.data.position.z() -
1646 groundSnappedZ(entry, poseOf(entry.data))) > 0.5f;
1650 sceneLayerDirty_ =
true;
1651 groundLayerDirty_ =
true;
1653 tabRebuildNeeded_ =
true;
1654 status_ =
"Loaded " + std::to_string(entries_.size()) +
" objects from " +
1655 path.string() +
".";
1658 catch (
const std::exception& e)
1660 status_ = std::string(
"Loading scene failed: ") + e.what();
1668 const Eigen::Matrix3f rot =
q.toRotationMatrix();
1669 return std::atan2(rot(1, 0), rot(0, 0));
1675 Eigen::Matrix4f pose = Eigen::Matrix4f::Identity();
1676 pose.block<3, 3>(0, 0) = obj.orientation.toRotationMatrix();
1677 pose.block<3, 1>(0, 3) = obj.position;
int Label(int n[], int size, int *curLabel, MiscLib::Vector< std::pair< int, size_t > > *labels)
armarx::viz::Client arviz
ComponentPropertyDefinitions(std::string prefix, bool hasObjectNameParameter=true)
std::string getConfigIdentifier()
Retrieve config identifier for this component as set in constructor.
Property< PropertyType > getProperty(const std::string &name)
std::string getName() const
Retrieve name of object.
Used to find objects in the ArmarX objects repository [1] (formerly [2]).
static VirtualRobot::ObstaclePtr loadObstacle(const std::optional< ObjectInfo > &ts)
static const std::string DefaultObjectsPackageName
Accessor for the object files.
std::string prefix
Prefix of the properties such as namespace, domain, component name, etc.
PropertyDefinition< PropertyType > & defineOptionalProperty(const std::string &name, PropertyType defaultValue, const std::string &description="", PropertyDefinitionBase::PropertyConstness constness=PropertyDefinitionBase::eConstant)
SceneEditorPropertyDefinitions(std::string prefix)
void onInitComponent() override
Pure virtual hook for the subclass.
void onDisconnectComponent() override
Hook for subclass.
void RemoteGui_update() override
void onConnectComponent() override
Pure virtual hook for the subclass.
PropertyDefinitionsPtr createPropertyDefinitions() override
void onExitComponent() override
Hook for subclass.
std::string getDefaultName() const override
Retrieve default name of component.
virtual Layer layer(std::string const &name) const
CommitResult commit(StagedCommit const &commit)
#define ARMARX_INFO
The normal logging level.
#define ARMARX_WARNING
The logging level for unexpected behaviour, but not a serious problem.
Quaternion< float, 0 > Quaternionf
InteractionDescription interaction()
@ Transform
The element was transformed (translated or rotated).
@ ContextMenuChosen
A context menu entry was chosen.
@ Deselect
An element was deselected.
@ Select
An element was selected.
This file offers overloads of toIce() and fromIce() functions for STL container types.
auto transform(const Container< InputT, Alloc > &in, OutputT(*func)(InputT const &)) -> Container< OutputT, typename std::allocator_traits< Alloc >::template rebind_alloc< OutputT > >
Convenience function (with less typing) to transform a container of type InputT into the same contain...
IceUtil::Handle< class PropertyDefinitionContainer > PropertyDefinitionsPtr
PropertyDefinitions smart pointer type.
std::vector< T > max(const std::vector< T > &v1, const std::vector< T > &v2)
std::vector< T > min(const std::vector< T > &v1, const std::vector< T > &v2)
Vertex target(const detail::edge_base< Directed, Vertex > &e, const PCG &)
double norm(const Point &a)
void RemoteGui_startRunningTask()
void RemoteGui_createTab(std::string const &name, RemoteGui::Client::Widget const &rootWidget, RemoteGui::Client::Tab *tab)
std::optional< bool > isStatic
Eigen::Quaternionf orientation
InteractionFeedbackRange interactions() const
Self & contextMenu(std::vector< std::string > const &options)
Self & hideDuringTransform()
A staged commit prepares multiple layers to be committed.
void requestInteraction(Layer const &layer)
Request interaction feedback for a particular layer.
void add(Layer const &layer)
Stage a layer to be committed later via client.apply(*this)
void reset()
Reset all staged layers and interaction requests.