28#include <SimoxUtility/algorithm/string/string_tools.h>
29#include <SimoxUtility/json.h>
30#include <VirtualRobot/BoundingBox.h>
31#include <VirtualRobot/CollisionDetection/CollisionChecker.h>
32#include <VirtualRobot/CollisionDetection/CollisionModel.h>
33#include <VirtualRobot/Obstacle.h>
46 const std::vector<std::string> contextMenuEntries = {
56 const std::string noneOption =
"<none>";
58 constexpr float rad2deg = 180.0f /
M_PI;
59 constexpr float deg2rad =
M_PI / 180.0f;
67 "Package containing the object models.");
71 "Package to whose data/<package> directory scenes are saved to and "
72 "loaded from (if SceneStorageDirectory is empty and the scene file "
73 "is not an absolute path).");
75 "SceneStorageDirectory",
77 "Directory that directly holds the scene files (new scenes are "
78 "created here and loaded from here). Grounding info is written to a "
79 "parallel 'groundings' directory (the scene path with its last "
80 "'scenes' component replaced by 'groundings'). If empty, the "
81 "ScenesPackage's <data>/<package>/scenes directory is used. "
82 "Use an absolute path for reliable resolution (a relative path is "
83 "resolved against the process working directory, not the package).");
87 "Optional class ID (Dataset/ClassName) of an object that is spawned "
88 "at the origin and designated as the ground object.");
90 "GroundZ", 0.0f,
"Initial height (z, in mm) of the ground plane objects are locked to.");
94 "Initial scene file (name in the scenes directory or absolute path).");
100 return "SceneEditor";
126 std::scoped_lock lock(mutex_);
130 for (
const auto& [dataset, infos] : objectFinder_.findAllObjectsByDataset(
true))
132 std::vector<std::string>& classNames = objectsByDataset_[dataset];
135 classNames.push_back(info.id().className());
137 std::sort(classNames.begin(), classNames.end());
140 catch (
const std::exception& e)
142 ARMARX_WARNING <<
"Failed to enumerate objects of package '" << objectsPackage_
143 <<
"': " << e.what();
145 if (!objectsByDataset_.empty())
147 currentDataset_ = objectsByDataset_.begin()->first;
149 ARMARX_INFO <<
"Found " << objectsByDataset_.size() <<
" datasets in package '"
150 << objectsPackage_ <<
"'.";
152 if (!initialGroundClass_.empty())
154 if (objectFinder_.findObject(initialGroundClass_))
156 Entry& ground = addObject(initialGroundClass_);
158 ground.manualPose =
true;
159 ground.groundRef.clear();
163 ARMARX_WARNING <<
"Ground object '" << initialGroundClass_ <<
"' not found.";
168 createRemoteGuiTab();
180 const bool join =
true;
192 SceneEditor::createRemoteGuiTab()
196 std::vector<std::string> datasets;
197 for (
const auto& datasetEntry : objectsByDataset_)
199 datasets.push_back(datasetEntry.first);
201 if (datasets.empty())
203 datasets.push_back(noneOption);
205 tab_.dataset.setOptions(datasets);
206 if (!currentDataset_.empty())
208 tab_.dataset.setValue(currentDataset_);
211 std::vector<std::string> classNames;
212 if (
auto it = objectsByDataset_.find(currentDataset_); it != objectsByDataset_.end())
214 classNames = it->second;
216 if (classNames.empty())
218 classNames.push_back(noneOption);
220 tab_.objectClass.setOptions(classNames);
222 std::vector<std::string> instanceOptions = {noneOption};
223 for (
const Entry& entry : entries_)
225 instanceOptions.push_back(entry.data.instanceName);
227 tab_.groundObject.setOptions(instanceOptions);
229 const Entry* selected = findEntry(selected_);
230 tab_.groundObject.setValue(
231 (selected && !selected->groundRef.empty()) ? selected->groundRef : noneOption);
235 const std::string previousAlignTarget =
236 guiInitialized_ ? tab_.alignTarget.getValue() : std::string();
237 tab_.alignTarget.setOptions(instanceOptions);
238 if (std::find(instanceOptions.begin(), instanceOptions.end(), previousAlignTarget) !=
239 instanceOptions.end())
241 tab_.alignTarget.setValue(previousAlignTarget);
244 if (!guiInitialized_)
246 guiInitialized_ =
true;
248 tab_.addObject.setLabel(
"Add object");
250 tab_.groundZ.setRange(-100000.0f, 100000.0f);
251 tab_.groundZ.setDecimals(1);
252 tab_.groundZ.setSteps(2000);
253 tab_.groundZ.setValue(groundZ_);
255 tab_.selectedInfo.setText(
"<none>");
256 tab_.instanceName.setValue(
"");
257 tab_.renameObject.setLabel(
"Rename");
258 for (FloatSpinBox* spin : {&tab_.posX, &tab_.posY, &tab_.posZ})
260 spin->setRange(-100000.0f, 100000.0f);
261 spin->setDecimals(1);
262 spin->setSteps(2000);
263 spin->setValue(0.0f);
265 tab_.yaw.setRange(-180.0f, 180.0f);
266 tab_.yaw.setDecimals(1);
267 tab_.yaw.setSteps(720);
268 tab_.yaw.setValue(0.0f);
270 tab_.alignOutside.setValue(
false);
271 tab_.frontXZ.setLabel(
"Front XZ plane (y min)");
272 tab_.backXZ.setLabel(
"Back XZ plane (y max)");
273 tab_.frontYZ.setLabel(
"Front YZ plane (x min)");
274 tab_.backYZ.setLabel(
"Back YZ plane (x max)");
276 tab_.isStatic.setValue(
true);
277 tab_.liveApply.setValue(
false);
278 tab_.applyPose.setLabel(
"Apply pose (manual)");
279 tab_.lockToGround.setLabel(
"Lock to ground");
280 tab_.flipX.setLabel(
"Flip X (+90°)");
281 tab_.flipY.setLabel(
"Flip Y (+90°)");
282 tab_.flipZ.setLabel(
"Flip Z (+90°)");
283 tab_.deleteObject.setLabel(
"Delete object");
285 tab_.sceneFile.setValue(initialSceneFile_);
286 tab_.saveScene.setLabel(
"Save scene");
287 tab_.loadScene.setLabel(
"Load scene");
288 tab_.clearScene.setLabel(
"Clear scene");
289 tab_.status.setText(
"");
292 GroupBox importGroup;
293 importGroup.setLabel(
"Import object");
296 grid.add(
Label(
"Dataset"), {0, 0}).add(tab_.dataset, {0, 1});
297 grid.add(
Label(
"Object"), {1, 0}).add(tab_.objectClass, {1, 1});
298 grid.add(tab_.addObject, {2, 0}, {1, 2});
299 importGroup.addChild(grid);
302 GroupBox groundGroup;
303 groundGroup.setLabel(
"Default ground");
306 grid.add(
Label(
"Default ground z (mm)"), {0, 0}).add(tab_.groundZ, {0, 1});
307 groundGroup.addChild(grid);
310 GroupBox selectedGroup;
311 selectedGroup.setLabel(
"Selected object (click an object in ArViz to select)");
315 grid.add(tab_.selectedInfo, {row++, 0}, {1, 4});
316 grid.add(
Label(
"Instance name"), {row, 0})
317 .add(tab_.instanceName, {row, 1}, {1, 2})
318 .add(tab_.renameObject, {row, 3});
320 grid.add(
Label(
"Grounded to"), {row, 0}).add(tab_.groundObject, {row, 1}, {1, 3});
322 grid.add(
Label(
"Static"), {row, 0}).add(tab_.isStatic, {row, 1});
324 grid.add(
Label(
"x (mm)"), {row, 0})
325 .add(tab_.posX, {row, 1})
326 .add(
Label(
"y (mm)"), {row, 2})
327 .add(tab_.posY, {row, 3});
329 grid.add(
Label(
"z (mm)"), {row, 0})
330 .add(tab_.posZ, {row, 1})
331 .add(
Label(
"yaw (°)"), {row, 2})
332 .add(tab_.yaw, {row, 3});
334 grid.add(
Label(
"Apply immediately"), {row, 0}).add(tab_.liveApply, {row, 1});
336 grid.add(tab_.applyPose, {row, 0}, {1, 2}).add(tab_.lockToGround, {row, 2}, {1, 2});
338 grid.add(tab_.flipX, {row, 0}).add(tab_.flipY, {row, 1}).add(tab_.flipZ, {row, 2});
339 grid.add(tab_.deleteObject, {row, 3});
340 selectedGroup.addChild(grid);
345 "Align the selected object's bounding box face with a reference object");
349 grid.add(
Label(
"Reference object"), {row, 0}).add(tab_.alignTarget, {row, 1});
351 grid.add(
Label(
"Align from outside (touching)"), {row, 0})
352 .add(tab_.alignOutside, {row, 1});
354 grid.add(tab_.frontYZ, {row, 0}).add(tab_.backYZ, {row, 1});
356 grid.add(tab_.frontXZ, {row, 0}).add(tab_.backXZ, {row, 1});
357 alignGroup.addChild(grid);
361 fileGroup.setLabel(
"Scene file");
364 grid.add(
Label(
"File"), {0, 0}).add(tab_.sceneFile, {0, 1}, {1, 3});
365 grid.add(tab_.saveScene, {1, 0})
366 .add(tab_.loadScene, {1, 1})
367 .add(tab_.clearScene, {1, 2});
368 fileGroup.addChild(grid);
371 VBoxLayout root = {importGroup, groundGroup, selectedGroup, alignGroup,
372 fileGroup, tab_.status, VSpacer()};
385 catch (
const std::exception& e)
394 std::scoped_lock lock(mutex_);
396 if (tab_.dataset.hasValueChanged())
398 const std::string dataset = tab_.dataset.getValue();
399 if (!dataset.empty() && dataset != currentDataset_)
401 currentDataset_ = dataset;
402 tabRebuildNeeded_ =
true;
407 if (tab_.groundObject.hasValueChanged())
409 const std::string value = tab_.groundObject.getValue();
410 const std::string ref = (value == noneOption) ?
"" : value;
411 if (Entry* entry = findEntry(selected_); entry && ref != entry->groundRef)
413 if (ref == entry->data.instanceName)
415 status_ =
"An object cannot be grounded to itself.";
420 entry->groundRef = ref;
421 if (!entry->manualPose)
423 entry->data.position.z() = groundSnappedZ(*entry, poseOf(entry->data));
424 sceneLayerDirty_ =
true;
427 status_ =
"Grounded '" + selected_ +
"' to " +
428 (ref.empty() ? std::string(
"the default ground.")
434 if (tab_.addObject.wasClicked())
436 const std::string className = tab_.objectClass.getValue();
437 if (className.empty() || className == noneOption || currentDataset_.empty() ||
438 currentDataset_ == noneOption)
440 status_ =
"No object class selected.";
444 Entry& entry = addObject(currentDataset_ +
"/" + className);
450 if (tab_.groundZ.hasValueChanged())
452 const float value = tab_.groundZ.getValue();
453 if (std::abs(value - groundZ_) > 0.01f)
459 Entry* selected = findEntry(selected_);
461 if (tab_.isStatic.hasValueChanged())
463 const bool isStatic = tab_.isStatic.getValue();
467 status_ =
"'" + selected_ +
"' is now " +
468 (isStatic ?
"static." :
"dynamic.");
472 const bool applyClicked = tab_.applyPose.wasClicked();
473 bool poseEdited =
false;
475 poseEdited |= tab_.posX.hasValueChanged();
476 poseEdited |= tab_.posY.hasValueChanged();
477 poseEdited |= tab_.posZ.hasValueChanged();
478 poseEdited |= tab_.yaw.hasValueChanged();
479 const bool liveApply = tab_.liveApply.getValue() && poseEdited;
481 if (applyClicked || liveApply)
485 const Eigen::Vector3f position(
486 tab_.posX.getValue(), tab_.posY.getValue(), tab_.posZ.getValue());
487 const float yawDeg = tab_.yaw.getValue();
491 const float yawDiff = std::remainder(
493 const bool differs = (position - selected->data.
position).
norm() > 0.01f ||
494 std::abs(yawDiff) > 1e-3f;
495 if (applyClicked || differs)
497 if (applyManualPose(*selected, position, yawDeg))
500 status_ =
"Applied manual pose to '" + selected_ +
501 "' (unlocked from ground).";
505 else if (applyClicked)
507 status_ =
"No object selected.";
511 if (tab_.renameObject.wasClicked())
515 renameEntry(*selected, tab_.instanceName.getValue());
519 status_ =
"No object selected.";
523 if (tab_.lockToGround.wasClicked())
527 status_ =
"No object selected.";
531 selected->manualPose =
false;
532 selected->data.
position.z() = groundSnappedZ(*selected, poseOf(selected->data));
533 sceneLayerDirty_ =
true;
535 status_ =
"Locked '" + selected_ +
"' to " +
536 (selected->groundRef.empty() ? std::string(
"the default ground.")
537 :
"'" + selected->groundRef +
"'.");
541 const Eigen::Vector3f flipAxes[] = {
542 Eigen::Vector3f::UnitX(), Eigen::Vector3f::UnitY(), Eigen::Vector3f::UnitZ()};
544 for (
int axis = 0; axis < 3; ++axis)
546 if (flipButtons[axis]->wasClicked())
550 flipEntry(*selected, flipAxes[axis]);
554 status_ =
"No object selected.";
559 if (tab_.deleteObject.wasClicked())
563 status_ =
"Deleted '" + selected_ +
"'.";
564 deleteEntry(selected_);
569 status_ =
"No object selected.";
573 if (tab_.saveScene.wasClicked())
575 saveScene(tab_.sceneFile.getValue());
578 if (tab_.loadScene.wasClicked())
580 loadScene(tab_.sceneFile.getValue());
583 if (tab_.frontYZ.wasClicked())
585 alignToPlane(0,
true,
"front yz-plane");
587 if (tab_.backYZ.wasClicked())
589 alignToPlane(0,
false,
"back yz-plane");
591 if (tab_.frontXZ.wasClicked())
593 alignToPlane(1,
true,
"front xz-plane");
595 if (tab_.backXZ.wasClicked())
597 alignToPlane(1,
false,
"back xz-plane");
600 if (tab_.clearScene.wasClicked())
603 pendingTransforms_.clear();
604 collisionModels_.clear();
606 sceneLayerDirty_ =
true;
607 groundLayerDirty_ =
true;
609 tabRebuildNeeded_ =
true;
610 status_ =
"Cleared scene.";
616 if (Entry* entry = findEntry(selected_))
618 const std::string grounding =
620 ? std::string(
", manual pose)")
621 : (entry->groundRef.empty()
622 ? std::string(
", locked to default ground)")
623 :
", locked to '" + entry->groundRef +
"')");
624 tab_.selectedInfo.setText(entry->data.instanceName +
" (" +
625 entry->data.className + grounding);
626 tab_.instanceName.setValue(entry->data.instanceName);
627 tab_.isStatic.setValue(entry->data.isStatic.value_or(
true));
628 tab_.posX.setValue(entry->data.position.x());
629 tab_.posY.setValue(entry->data.position.y());
630 tab_.posZ.setValue(entry->data.position.z());
631 tab_.yaw.setValue(yawOf(entry->data.orientation) * rad2deg);
632 tab_.groundObject.setValue(entry->groundRef.empty() ? noneOption
637 tab_.selectedInfo.setText(
"<none>");
638 tab_.instanceName.setValue(
"");
639 tab_.groundObject.setValue(noneOption);
641 tab_.groundZ.setValue(groundZ_);
644 tab_.status.setText(status_);
646 if (tabRebuildNeeded_)
648 tabRebuildNeeded_ =
false;
649 createRemoteGuiTab();
661 std::scoped_lock lock(mutex_);
664 stage.
add(sceneLayer_);
665 stage.
add(groundLayer_);
672 std::scoped_lock lock(mutex_);
674 rebuildGroundLayer();
675 stage.
add(sceneLayer_);
676 stage.
add(groundLayer_);
677 sceneLayerDirty_ =
false;
678 groundLayerDirty_ =
false;
686 CycleUtil cycle(10.0f);
687 while (!task_->isStopped())
689 result =
arviz.commit(stage);
694 std::scoped_lock lock(mutex_);
698 for (
const viz::InteractionFeedback& interaction : result.
interactions())
700 handleInteraction(interaction);
703 if (sceneLayerDirty_)
706 stage.
add(sceneLayer_);
707 sceneLayerDirty_ =
false;
709 if (groundLayerDirty_)
711 rebuildGroundLayer();
712 stage.
add(groundLayer_);
713 groundLayerDirty_ =
false;
717 cycle.waitForCycleDuration();
758 flipEntry(*entry, Eigen::Vector3f::UnitX());
761 flipEntry(*entry, Eigen::Vector3f::UnitY());
764 flipEntry(*entry, Eigen::Vector3f::UnitZ());
767 entry->manualPose = !entry->manualPose;
768 if (!entry->manualPose)
770 entry->data.position.z() =
771 groundSnappedZ(*entry, poseOf(entry->data));
773 sceneLayerDirty_ =
true;
791 SceneEditor::applyPendingTransform(
const std::string& instanceName)
793 auto it = pendingTransforms_.find(instanceName);
794 if (it == pendingTransforms_.end())
798 const Eigen::Matrix4f
transform = it->second;
799 pendingTransforms_.erase(it);
801 Entry* entry = findEntry(instanceName);
809 const Eigen::Matrix4f candidate =
transform * poseOf(entry->data);
811 Eigen::Vector3f newPosition = candidate.block<3, 1>(0, 3);
813 if (entry->manualPose)
822 newOrientation = (Eigen::AngleAxisf(deltaYaw, Eigen::Vector3f::UnitZ()) *
823 entry->data.orientation)
827 Eigen::Matrix4f newPose = Eigen::Matrix4f::Identity();
828 newPose.block<3, 3>(0, 0) = newOrientation.toRotationMatrix();
829 newPose.block<3, 1>(0, 3) = newPosition;
831 if (!entry->manualPose)
834 newPosition.z() = groundSnappedZ(*entry, newPose);
835 newPose(2, 3) = newPosition.z();
840 sceneLayerDirty_ =
true;
843 if (!isMoveAllowed(*entry, newPose))
848 entry->data.position = newPosition;
849 entry->data.orientation = newOrientation;
850 afterEntryPoseChanged(*entry);
854 SceneEditor::applyManualPose(Entry& entry,
const Eigen::Vector3f& position,
float yawDeg)
856 const float deltaYaw =
857 std::remainder(yawDeg * deg2rad - yawOf(entry.data.orientation), 2.0 *
M_PI);
859 (Eigen::AngleAxisf(deltaYaw, Eigen::Vector3f::UnitZ()) * entry.data.orientation)
862 Eigen::Matrix4f newPose = Eigen::Matrix4f::Identity();
863 newPose.block<3, 3>(0, 0) = newOrientation.toRotationMatrix();
864 newPose.block<3, 1>(0, 3) = position;
866 if (!isMoveAllowed(entry, newPose))
873 entry.manualPose =
true;
874 entry.data.position = position;
875 entry.data.orientation = newOrientation;
876 sceneLayerDirty_ =
true;
878 afterEntryPoseChanged(entry);
883 SceneEditor::collidesWith(
const Entry& entry,
const Eigen::Matrix4f& candidatePose)
885 VirtualRobot::ObstaclePtr model = collisionModelOf(entry);
886 if (!model || !model->getCollisionModel())
890 model->setGlobalPose(candidatePose);
892 auto checker = VirtualRobot::CollisionChecker::getGlobalCollisionChecker();
893 for (
const Entry& other : entries_)
895 if (&other == &entry)
900 if (other.data.instanceName == entry.groundRef ||
901 other.groundRef == entry.data.instanceName)
905 VirtualRobot::ObstaclePtr otherModel = collisionModelOf(other);
906 if (!otherModel || !otherModel->getCollisionModel())
910 otherModel->setGlobalPose(poseOf(other.data));
911 if (checker->checkCollision(model->getCollisionModel(),
912 otherModel->getCollisionModel()))
914 return other.data.instanceName;
921 SceneEditor::isMoveAllowed(Entry& entry,
const Eigen::Matrix4f& candidatePose)
925 const bool wasColliding = !collidesWith(entry, poseOf(entry.data)).empty();
930 const std::string hit = collidesWith(entry, candidatePose);
933 status_ =
"Move of '" + entry.data.instanceName +
"' rejected: collides with '" +
940 const std::optional<simox::AxisAlignedBoundingBox>&
941 SceneEditor::localAabbOf(
const Entry& entry)
943 auto it = localAabbs_.find(entry.data.className);
944 if (it == localAabbs_.end())
946 std::optional<simox::AxisAlignedBoundingBox> aabb;
949 if (std::optional<ObjectInfo> info =
950 objectFinder_.findObject(entry.data.className))
952 info->setLogError(
false);
953 aabb = info->loadAABB();
956 catch (
const std::exception& e)
958 ARMARX_WARNING <<
"Failed to load AABB of '" << entry.data.className
959 <<
"': " << e.what();
963 ARMARX_INFO <<
"No AABB (aabb.json) for '" << entry.data.className
964 <<
"', falling back to the collision model's bounding box.";
966 it = localAabbs_.emplace(entry.data.className, aabb).first;
971 std::optional<simox::AxisAlignedBoundingBox>
972 SceneEditor::globalAabb(
const Entry& entry,
const Eigen::Matrix4f& pose)
978 if (
const std::optional<simox::AxisAlignedBoundingBox>& aabb = localAabbOf(entry))
983 else if (VirtualRobot::ObstaclePtr model = collisionModelOf(entry);
984 model && model->getCollisionModel())
986 const VirtualRobot::BoundingBox bbox =
987 model->getCollisionModel()->getBoundingBox(
false);
997 const Eigen::Matrix3f rotation = pose.block<3, 3>(0, 0);
998 const Eigen::Vector3f translation = pose.block<3, 1>(0, 3);
999 Eigen::Vector3f globalMin =
1000 Eigen::Vector3f::Constant(std::numeric_limits<float>::max());
1001 Eigen::Vector3f globalMax =
1002 Eigen::Vector3f::Constant(std::numeric_limits<float>::lowest());
1003 for (
int i = 0; i < 8; ++i)
1005 const Eigen::Vector3f corner((i & 1) ?
max.x() :
min.x(),
1006 (i & 2) ?
max.y() :
min.y(),
1007 (i & 4) ?
max.z() :
min.z());
1008 const Eigen::Vector3f global = rotation * corner + translation;
1009 globalMin = globalMin.cwiseMin(global);
1010 globalMax = globalMax.cwiseMax(global);
1012 return simox::AxisAlignedBoundingBox(globalMin, globalMax);
1016 SceneEditor::groundHeightOf(
const std::string& ref)
1022 Entry* ground = findEntry(ref);
1027 if (
const auto aabb = globalAabb(*ground, poseOf(ground->data)))
1029 return aabb->max().z();
1035 SceneEditor::groundHeightFor(
const Entry& entry)
1037 return groundHeightOf(entry.groundRef);
1041 SceneEditor::reseatEntriesGroundedTo(
const std::string& ref)
1043 for (Entry& entry : entries_)
1045 if (!entry.manualPose && entry.groundRef == ref &&
1046 entry.data.instanceName != ref)
1048 entry.data.position.z() = groundSnappedZ(entry, poseOf(entry.data));
1049 sceneLayerDirty_ =
true;
1055 SceneEditor::afterEntryPoseChanged(
const Entry& entry)
1057 reseatEntriesGroundedTo(entry.data.instanceName);
1061 SceneEditor::groundSnappedZ(
const Entry& entry,
const Eigen::Matrix4f& candidatePose)
1063 const float ground = groundHeightFor(entry);
1064 const auto aabb = globalAabb(entry, candidatePose);
1069 return candidatePose(2, 3) + (ground - aabb->min().z());
1072 VirtualRobot::ObstaclePtr
1073 SceneEditor::collisionModelOf(
const Entry& entry)
1075 auto it = collisionModels_.find(entry.data.instanceName);
1076 if (it != collisionModels_.end())
1081 VirtualRobot::ObstaclePtr model;
1086 catch (
const std::exception& e)
1088 ARMARX_WARNING <<
"Failed to load collision model of '" << entry.data.className
1089 <<
"': " << e.what();
1093 ARMARX_WARNING <<
"No collision model for '" << entry.data.className
1094 <<
"', collision checks are skipped for '" << entry.data.instanceName
1097 collisionModels_[entry.data.instanceName] = model;
1102 SceneEditor::findEntry(
const std::string& instanceName)
1104 for (Entry& entry : entries_)
1106 if (entry.data.instanceName == instanceName)
1115 SceneEditor::addObject(
const std::string& classId)
1117 Entry& entry = entries_.emplace_back();
1118 entry.data.className = classId;
1119 entry.data.instanceName = makeUniqueInstanceName(classId);
1120 entry.groundRef.clear();
1121 entry.data.position = Eigen::Vector3f(0.0f, 0.0f, groundZ_);
1122 entry.data.orientation = Eigen::Quaternionf::Identity();
1123 entry.data.isStatic =
true;
1124 collisionModelOf(entry);
1125 entry.data.position.z() = groundSnappedZ(entry, poseOf(entry.data));
1126 sceneLayerDirty_ =
true;
1127 tabRebuildNeeded_ =
true;
1132 SceneEditor::renameEntry(Entry& entry,
const std::string& newName)
1134 const std::string oldName = entry.data.instanceName;
1135 if (newName.empty() || newName == noneOption)
1137 status_ =
"Cannot rename '" + oldName +
"': invalid name '" + newName +
"'.";
1141 if (newName == oldName)
1145 if (findEntry(newName) !=
nullptr)
1147 status_ =
"Cannot rename '" + oldName +
"': an object named '" + newName +
1148 "' already exists.";
1153 entry.data.instanceName = newName;
1156 for (Entry& other : entries_)
1158 if (other.groundRef == oldName)
1160 other.groundRef = newName;
1163 if (
auto node = collisionModels_.extract(oldName); !node.empty())
1165 node.key() = newName;
1166 collisionModels_.insert(std::move(node));
1168 if (
auto it = pendingTransforms_.find(oldName); it != pendingTransforms_.end())
1170 pendingTransforms_[newName] = it->second;
1171 pendingTransforms_.erase(it);
1173 if (selected_ == oldName)
1175 selected_ = newName;
1178 sceneLayerDirty_ =
true;
1180 tabRebuildNeeded_ =
true;
1181 status_ =
"Renamed '" + oldName +
"' to '" + newName +
"'.";
1185 SceneEditor::deleteEntry(
const std::string& instanceName)
1197 return e.data.instanceName == instanceName;
1202 pendingTransforms_.erase(instanceName);
1203 collisionModels_.erase(instanceName);
1205 for (Entry& entry : entries_)
1207 if (entry.groundRef == instanceName)
1209 entry.groundRef.clear();
1210 if (!entry.manualPose)
1212 entry.data.position.z() = groundSnappedZ(entry, poseOf(entry.data));
1216 if (selected_ == instanceName)
1220 sceneLayerDirty_ =
true;
1222 tabRebuildNeeded_ =
true;
1226 SceneEditor::flipEntry(Entry& entry,
const Eigen::Vector3f& axis)
1232 Eigen::Matrix4f newPose = Eigen::Matrix4f::Identity();
1233 newPose.block<3, 3>(0, 0) = newOrientation.toRotationMatrix();
1234 newPose.block<3, 1>(0, 3) = entry.data.position;
1237 if (!entry.manualPose)
1239 newPose(2, 3) = groundSnappedZ(entry, newPose);
1242 if (!isMoveAllowed(entry, newPose))
1247 entry.data.orientation = newOrientation;
1248 entry.data.position.z() = newPose(2, 3);
1249 sceneLayerDirty_ =
true;
1251 status_ =
"Flipped '" + entry.data.instanceName +
"'.";
1252 afterEntryPoseChanged(entry);
1256 SceneEditor::selectEntry(
const std::string& instanceName)
1258 if (findEntry(instanceName))
1260 selected_ = instanceName;
1266 SceneEditor::setGroundZ(
float groundZ)
1269 reseatEntriesGroundedTo(
"");
1270 groundLayerDirty_ =
true;
1275 SceneEditor::alignToPlane(
int axis,
bool minSide,
const std::string& planeName)
1277 Entry* selected = findEntry(selected_);
1280 status_ =
"No object selected.";
1283 const std::string targetName = tab_.alignTarget.getValue();
1284 Entry*
target = findEntry(targetName);
1287 status_ =
"No reference object selected.";
1290 if (target == selected)
1292 status_ =
"Cannot align an object with itself.";
1296 const auto selectedBox = globalAabb(*selected, poseOf(selected->data));
1297 const auto targetBox = globalAabb(*target, poseOf(
target->data));
1298 if (!selectedBox || !targetBox)
1300 status_ =
"Cannot align: missing bounding box for '" +
1301 (selectedBox ? targetName : selected_) +
"'.";
1309 const bool outside = tab_.alignOutside.getValue();
1310 constexpr float clearance = 0.5f;
1315 ? (targetBox->min()(axis) - clearance) - selectedBox->max()(axis)
1316 : (targetBox->
max()(axis) + clearance) - selectedBox->
min()(axis);
1320 delta = minSide ? targetBox->min()(axis) - selectedBox->min()(axis)
1321 : targetBox->
max()(axis) - selectedBox->
max()(axis);
1324 Eigen::Vector3f newPosition = selected->data.position;
1325 newPosition(axis) += delta;
1327 Eigen::Matrix4f newPose = poseOf(selected->data);
1328 newPose.block<3, 1>(0, 3) = newPosition;
1330 if (!isMoveAllowed(*selected, newPose))
1336 selected->data.position = newPosition;
1337 sceneLayerDirty_ =
true;
1339 status_ =
"Aligned '" + selected_ +
"' with the " + planeName +
" of '" + targetName +
1340 (outside ?
"' (outside)." :
"' (inside).");
1341 afterEntryPoseChanged(*selected);
1345 SceneEditor::makeUniqueInstanceName(
const std::string& classId)
1347 const std::string className =
ObjectID(classId).className();
1351 name = className +
"_" + std::to_string(nextId_++);
1352 }
while (findEntry(name) !=
nullptr);
1357 SceneEditor::rebuildSceneLayer()
1359 sceneLayer_ =
arviz.layer(
"Scene");
1360 for (
const Entry& entry : entries_)
1362 viz::Object object(entry.data.instanceName);
1363 object.fileByObjectFinder(entry.data.className, objectsPackage_);
1364 object.position(entry.data.position).orientation(entry.data.orientation);
1370 object.enable(interaction);
1372 sceneLayer_.add(
object);
1377 SceneEditor::rebuildGroundLayer()
1379 groundLayer_ =
arviz.layer(
"Ground");
1382 viz::Box plane(
"GroundPlane");
1383 plane.position(Eigen::Vector3f(0.0f, 0.0f, groundZ_ - 5.0f))
1384 .size(Eigen::Vector3f(10000.0f, 10000.0f, 10.0f))
1385 .color(viz::Color(128, 128, 128, 64));
1386 groundLayer_.add(plane);
1389 std::filesystem::path
1390 SceneEditor::resolveScenePath(std::string name)
const
1396 if (not simox::alg::ends_with(name,
".json"))
1400 std::filesystem::path path(name);
1401 if (!path.is_absolute())
1403 if (!sceneStorageDirectory_.empty())
1406 path = std::filesystem::path(sceneStorageDirectory_) / path;
1411 CMakePackageFinder packageFinder(scenesPackage_);
1412 const std::string dataDir = packageFinder.getDataDir();
1413 if (!packageFinder.packageFound() || dataDir.empty())
1415 throw LocalException(
1416 "Could not locate the scenes package '" + scenesPackage_ +
1417 "'. Set the SceneStorageDirectory property to an absolute path.");
1419 path = std::filesystem::path(dataDir) / scenesPackage_ /
"scenes" / path;
1425 if (!path.is_absolute())
1427 path = std::filesystem::absolute(path);
1429 return path.lexically_normal();
1432 std::filesystem::path
1433 SceneEditor::groundingPathFor(
const std::filesystem::path& scenePath)
const
1435 std::vector<std::filesystem::path> parts(scenePath.begin(), scenePath.end());
1436 int lastScenes = -1;
1437 for (std::size_t i = 0; i < parts.size(); ++i)
1439 if (parts[i].
string() ==
"scenes")
1441 lastScenes =
static_cast<int>(i);
1446 return scenePath.parent_path() / (scenePath.stem().
string() +
".groundings.json");
1448 std::filesystem::path result;
1449 for (std::size_t i = 0; i < parts.size(); ++i)
1451 result /= (
static_cast<int>(i) == lastScenes ? std::filesystem::path(
"groundings")
1458 SceneEditor::saveScene(
const std::string& fileArg)
1462 const std::filesystem::path path = resolveScenePath(fileArg);
1464 objects::Scene scene;
1467 std::map<std::string, int> perClassCount;
1468 for (
const Entry& entry : entries_)
1470 objects::SceneObject& obj = scene.objects.emplace_back(entry.data);
1471 const std::string className =
ObjectID(entry.data.className).className();
1473 className +
"_" + std::to_string(perClassCount[className]++);
1476 std::filesystem::create_directories(path.parent_path());
1477 const simox::json::json j = scene;
1478 simox::json::write(path.string(), j, 2);
1482 const std::filesystem::path groundingPath = groundingPathFor(path);
1485 const auto indexOf = [
this](
const std::string& instanceName) ->
int
1487 for (std::size_t i = 0; i < entries_.size(); ++i)
1489 if (entries_[i].
data.instanceName == instanceName)
1491 return static_cast<int>(i);
1496 simox::json::json grounding;
1497 grounding[
"groundings"] = simox::json::json::array();
1498 for (std::size_t i = 0; i < entries_.size(); ++i)
1500 const Entry& entry = entries_[i];
1501 grounding[
"groundings"].push_back({
1502 {
"index",
static_cast<int>(i)},
1503 {
"instanceName", scene.objects[i].instanceName},
1504 {
"ground", entry.groundRef.empty() ? -1 : indexOf(entry.groundRef)},
1505 {
"locked", !entry.manualPose},
1508 std::filesystem::create_directories(groundingPath.parent_path());
1509 simox::json::write(groundingPath.string(), grounding, 2);
1511 status_ =
"Saved " + std::to_string(scene.objects.size()) +
" objects to " +
1512 path.string() +
" (grounding info: " + groundingPath.string() +
").";
1515 catch (
const std::exception& e)
1517 status_ = std::string(
"Saving scene failed: ") + e.what();
1523 SceneEditor::loadScene(
const std::string& fileArg)
1527 const std::filesystem::path path = resolveScenePath(fileArg);
1528 ARMARX_INFO <<
"Loading scene from " << path <<
".";
1529 if (!std::filesystem::exists(path))
1531 status_ =
"Scene file does not exist: " + path.string();
1536 const simox::json::json j = simox::json::read(path.string());
1537 const auto scene = j.get<objects::Scene>();
1540 pendingTransforms_.clear();
1541 collisionModels_.clear();
1544 for (
const objects::SceneObject& obj : scene.objects)
1546 Entry& entry = entries_.emplace_back();
1548 if (entry.data.instanceName.empty() ||
1549 findEntry(entry.data.instanceName) != &entry)
1551 entry.data.instanceName = makeUniqueInstanceName(entry.data.className);
1556 bool groundingLoaded =
false;
1557 const std::filesystem::path groundingPath = groundingPathFor(path);
1558 if (std::filesystem::exists(groundingPath))
1562 const simox::json::json grounding =
1563 simox::json::read(groundingPath.string());
1564 for (
const auto& item : grounding.at(
"groundings"))
1568 Entry* entry =
nullptr;
1569 const int index = item.value(
"index", -1);
1570 if (
index >= 0 &&
index <
static_cast<int>(entries_.size()))
1572 entry = &entries_[
index];
1574 else if (item.contains(
"instanceName"))
1576 entry = findEntry(item.at(
"instanceName").get<std::string>());
1583 entry->groundRef.clear();
1584 if (item.contains(
"ground"))
1586 const auto& ground = item.at(
"ground");
1587 if (ground.is_number_integer())
1589 const int groundIndex = ground.get<
int>();
1590 if (groundIndex >= 0 &&
1591 groundIndex <
static_cast<int>(entries_.size()))
1594 entries_[groundIndex].data.instanceName;
1597 else if (ground.is_string())
1599 entry->groundRef = ground.get<std::string>();
1602 entry->manualPose = !item.value(
"locked",
true);
1604 groundingLoaded =
true;
1606 catch (
const std::exception& e)
1608 ARMARX_WARNING <<
"Failed to load grounding info from " << groundingPath
1609 <<
": " << e.what();
1613 for (Entry& entry : entries_)
1615 if (!entry.groundRef.empty() &&
1616 (entry.groundRef == entry.data.instanceName ||
1617 findEntry(entry.groundRef) ==
nullptr))
1619 entry.groundRef.clear();
1621 if (groundingLoaded)
1623 if (!entry.manualPose)
1625 entry.data.position.z() = groundSnappedZ(entry, poseOf(entry.data));
1633 std::abs(entry.data.position.z() -
1634 groundSnappedZ(entry, poseOf(entry.data))) > 0.5f;
1638 sceneLayerDirty_ =
true;
1639 groundLayerDirty_ =
true;
1641 tabRebuildNeeded_ =
true;
1642 status_ =
"Loaded " + std::to_string(entries_.size()) +
" objects from " +
1643 path.string() +
".";
1646 catch (
const std::exception& e)
1648 status_ = std::string(
"Loading scene failed: ") + e.what();
1656 const Eigen::Matrix3f rot =
q.toRotationMatrix();
1657 return std::atan2(rot(1, 0), rot(0, 0));
1663 Eigen::Matrix4f pose = Eigen::Matrix4f::Identity();
1664 pose.block<3, 3>(0, 0) = obj.orientation.toRotationMatrix();
1665 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.