SceneEditor.cpp
Go to the documentation of this file.
1/*
2 * This file is part of ArmarX.
3 *
4 * ArmarX is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License version 2 as
6 * published by the Free Software Foundation.
7 *
8 * ArmarX is distributed in the hope that it will be useful, but
9 * WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * @package RobotAPI::ArmarXObjects::SceneEditor
17 * @copyright http://www.gnu.org/licenses/gpl-2.0.txt
18 * GNU General Public License
19 */
20
21#include "SceneEditor.h"
22
23#include <algorithm>
24#include <cmath>
25#include <limits>
26#include <set>
27#include <utility>
28
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>
35
39
42
43namespace armarx
44{
45 namespace
46 {
47 const std::vector<std::string> contextMenuEntries = {
48 "Flip X (+90°)",
49 "Flip Y (+90°)",
50 "Flip Z (+90°)",
51 "Toggle ground lock",
52 "Delete",
53 };
54
55 // Placeholder entry: ComboBoxes must never have an empty option list,
56 // otherwise the RemoteGuiProvider rejects the whole tab.
57 const std::string noneOption = "<none>";
58
59 constexpr float rad2deg = 180.0f / M_PI;
60 constexpr float deg2rad = M_PI / 180.0f;
61 } // namespace
62
65 {
68 "Package containing the object models.");
70 "ScenesPackage",
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",
77 "",
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).");
86 "GroundObject",
87 "",
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.");
93 "SceneFile",
94 "NewScene",
95 "Initial scene file (name in the scenes directory or absolute path).");
96 }
97
98 std::string
100 {
101 return "SceneEditor";
102 }
103
109
110 void
112 {
113 objectsPackage_ = getProperty<std::string>("ObjectsPackage").getValue();
114 scenesPackage_ = getProperty<std::string>("ScenesPackage").getValue();
115 sceneStorageDirectory_ = getProperty<std::string>("SceneStorageDirectory").getValue();
116 initialGroundClass_ = getProperty<std::string>("GroundObject").getValue();
117 groundZ_ = getProperty<float>("GroundZ").getValue();
118 initialSceneFile_ = getProperty<std::string>("SceneFile").getValue();
119
120 objectFinder_ = ObjectFinder(objectsPackage_);
121 }
122
123 void
125 {
126 {
127 std::scoped_lock lock(mutex_);
128
129 try
130 {
131 for (const auto& [dataset, infos] : objectFinder_.findAllObjectsByDataset(true))
132 {
133 std::vector<std::string>& classNames = objectsByDataset_[dataset];
134 for (const ObjectInfo& info : infos)
135 {
136 classNames.push_back(info.id().className());
137 }
138 std::sort(classNames.begin(), classNames.end());
139 }
140 }
141 catch (const std::exception& e)
142 {
143 ARMARX_WARNING << "Failed to enumerate objects of package '" << objectsPackage_
144 << "': " << e.what();
145 }
146 if (!objectsByDataset_.empty())
147 {
148 currentDataset_ = objectsByDataset_.begin()->first;
149 }
150 ARMARX_INFO << "Found " << objectsByDataset_.size() << " datasets in package '"
151 << objectsPackage_ << "'.";
152
153 if (!initialGroundClass_.empty())
154 {
155 if (objectFinder_.findObject(initialGroundClass_))
156 {
157 Entry& ground = addObject(initialGroundClass_);
158 // The ground object itself is not locked to anything.
159 ground.manualPose = true;
160 ground.groundRef.clear();
161 }
162 else
163 {
164 ARMARX_WARNING << "Ground object '" << initialGroundClass_ << "' not found.";
165 }
166 }
167 }
168
169 createRemoteGuiTab();
171
172 task_ = new RunningTask<SceneEditor>(this, &SceneEditor::run);
173 task_->start();
174 }
175
176 void
178 {
179 if (task_)
180 {
181 const bool join = true;
182 task_->stop(join);
183 task_ = nullptr;
184 }
185 }
186
187 void
191
192 void
193 SceneEditor::createRemoteGuiTab()
194 {
195 using namespace RemoteGui::Client;
196
197 std::vector<std::string> datasets;
198 for (const auto& datasetEntry : objectsByDataset_)
199 {
200 datasets.push_back(datasetEntry.first);
201 }
202 if (datasets.empty())
203 {
204 datasets.push_back(noneOption);
205 }
206 tab_.dataset.setOptions(datasets);
207 if (!currentDataset_.empty())
208 {
209 tab_.dataset.setValue(currentDataset_);
210 }
211
212 std::vector<std::string> classNames;
213 if (auto it = objectsByDataset_.find(currentDataset_); it != objectsByDataset_.end())
214 {
215 classNames = it->second;
216 }
217 if (classNames.empty())
218 {
219 classNames.push_back(noneOption);
220 }
221 tab_.objectClass.setOptions(classNames);
222
223 std::vector<std::string> instanceOptions = {noneOption};
224 for (const Entry& entry : entries_)
225 {
226 instanceOptions.push_back(entry.data.instanceName);
227 }
228 tab_.groundObject.setOptions(instanceOptions);
229 {
230 const Entry* selected = findEntry(selected_);
231 tab_.groundObject.setValue(
232 (selected && !selected->groundRef.empty()) ? selected->groundRef : noneOption);
233 }
234
235 // Reading widget values throws before the tab exists (first build).
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())
241 {
242 tab_.alignTarget.setValue(previousAlignTarget);
243 }
244
245 if (!guiInitialized_)
246 {
247 guiInitialized_ = true;
248
249 tab_.addObject.setLabel("Add object");
250
251 tab_.groundZ.setRange(-100000.0f, 100000.0f);
252 tab_.groundZ.setDecimals(1);
253 tab_.groundZ.setSteps(2000);
254 tab_.groundZ.setValue(groundZ_);
255
256 tab_.selectedInfo.setText("<none>");
257 tab_.instanceName.setValue("");
258 tab_.renameObject.setLabel("Rename");
259 for (FloatSpinBox* spin : {&tab_.posX, &tab_.posY, &tab_.posZ})
260 {
261 spin->setRange(-100000.0f, 100000.0f);
262 spin->setDecimals(1);
263 spin->setSteps(2000);
264 spin->setValue(0.0f);
265 }
266 tab_.yaw.setRange(-180.0f, 180.0f);
267 tab_.yaw.setDecimals(1);
268 tab_.yaw.setSteps(720);
269 tab_.yaw.setValue(0.0f);
270
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)");
276
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");
285
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("");
291 }
292
293 GroupBox importGroup;
294 importGroup.setLabel("Import object");
295 {
296 GridLayout grid;
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);
301 }
302
303 GroupBox groundGroup;
304 groundGroup.setLabel("Default ground");
305 {
306 GridLayout grid;
307 grid.add(Label("Default ground z (mm)"), {0, 0}).add(tab_.groundZ, {0, 1});
308 groundGroup.addChild(grid);
309 }
310
311 GroupBox selectedGroup;
312 selectedGroup.setLabel("Selected object (click an object in ArViz to select)");
313 {
314 GridLayout grid;
315 int row = 0;
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});
320 ++row;
321 grid.add(Label("Grounded to"), {row, 0}).add(tab_.groundObject, {row, 1}, {1, 3});
322 ++row;
323 grid.add(Label("Static"), {row, 0}).add(tab_.isStatic, {row, 1});
324 ++row;
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});
329 ++row;
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});
334 ++row;
335 grid.add(Label("Apply immediately"), {row, 0}).add(tab_.liveApply, {row, 1});
336 ++row;
337 grid.add(tab_.applyPose, {row, 0}, {1, 2}).add(tab_.lockToGround, {row, 2}, {1, 2});
338 ++row;
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);
342 }
343
344 GroupBox alignGroup;
345 alignGroup.setLabel(
346 "Align the selected object's bounding box face with a reference object");
347 {
348 GridLayout grid;
349 int row = 0;
350 grid.add(Label("Reference object"), {row, 0}).add(tab_.alignTarget, {row, 1});
351 ++row;
352 grid.add(Label("Align from outside (touching)"), {row, 0})
353 .add(tab_.alignOutside, {row, 1});
354 ++row;
355 grid.add(tab_.frontYZ, {row, 0}).add(tab_.backYZ, {row, 1});
356 ++row;
357 grid.add(tab_.frontXZ, {row, 0}).add(tab_.backXZ, {row, 1});
358 alignGroup.addChild(grid);
359 }
360
361 GroupBox fileGroup;
362 fileGroup.setLabel("Scene file");
363 {
364 GridLayout grid;
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);
370 }
371
372 VBoxLayout root = {importGroup, groundGroup, selectedGroup, alignGroup,
373 fileGroup, tab_.status, VSpacer()};
374 RemoteGui_createTab(getName(), root, &tab_);
375 }
376
377 void
379 {
380 // An uncaught exception would terminate the RemoteGui update task
381 // and freeze the whole GUI, so log and continue instead.
382 try
383 {
385 }
386 catch (const std::exception& e)
387 {
388 ARMARX_WARNING << "RemoteGui update failed: " << e.what();
389 }
390 }
391
392 void
394 {
395 std::scoped_lock lock(mutex_);
396
397 if (tab_.dataset.hasValueChanged())
398 {
399 const std::string dataset = tab_.dataset.getValue();
400 if (!dataset.empty() && dataset != currentDataset_)
401 {
402 currentDataset_ = dataset;
403 tabRebuildNeeded_ = true;
404 }
405 }
406
407 // The ground combo box sets the ground of the currently selected object.
408 if (tab_.groundObject.hasValueChanged())
409 {
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)
413 {
414 if (ref == entry->data.instanceName)
415 {
416 status_ = "An object cannot be grounded to itself.";
417 syncGui_ = true;
418 }
419 else
420 {
421 entry->groundRef = ref;
422 if (!entry->manualPose)
423 {
424 entry->data.position.z() = groundSnappedZ(*entry, poseOf(entry->data));
425 sceneLayerDirty_ = true;
426 }
427 syncGui_ = true;
428 status_ = "Grounded '" + selected_ + "' to " +
429 (ref.empty() ? std::string("the default ground.")
430 : "'" + ref + "'.");
431 }
432 }
433 }
434
435 if (tab_.addObject.wasClicked())
436 {
437 const std::string className = tab_.objectClass.getValue();
438 if (className.empty() || className == noneOption || currentDataset_.empty() ||
439 currentDataset_ == noneOption)
440 {
441 status_ = "No object class selected.";
442 }
443 else
444 {
445 Entry& entry = addObject(currentDataset_ + "/" + className);
446 selectEntry(entry.data.instanceName);
447 status_ = "Added '" + entry.data.instanceName + "'.";
448 }
449 }
450
451 if (tab_.groundZ.hasValueChanged())
452 {
453 const float value = tab_.groundZ.getValue();
454 if (std::abs(value - groundZ_) > 0.01f)
455 {
456 setGroundZ(value);
457 }
458 }
459
460 Entry* selected = findEntry(selected_);
461
462 if (tab_.isStatic.hasValueChanged())
463 {
464 const bool isStatic = tab_.isStatic.getValue();
465 if (selected)
466 {
467 selected->data.isStatic = isStatic;
468 status_ = "'" + selected_ + "' is now " +
469 (isStatic ? "static." : "dynamic.");
470 }
471 }
472
473 const bool applyClicked = tab_.applyPose.wasClicked();
474 bool poseEdited = false;
475 // Reading the values consumes the change flags, so read them all.
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;
481
482 if (applyClicked || liveApply)
483 {
484 if (selected)
485 {
486 const Eigen::Vector3f position(
487 tab_.posX.getValue(), tab_.posY.getValue(), tab_.posZ.getValue());
488 const float yawDeg = tab_.yaw.getValue();
489
490 // Skip if the values merely echo the current pose (e.g. after
491 // the GUI was synced to a new selection).
492 const float yawDiff = std::remainder(
493 yawDeg * deg2rad - yawOf(selected->data.orientation), 2.0 * M_PI);
494 const bool differs = (position - selected->data.position).norm() > 0.01f ||
495 std::abs(yawDiff) > 1e-3f;
496 if (applyClicked || differs)
497 {
498 if (applyManualPose(*selected, position, yawDeg))
499 {
500 // Manually set coordinates override the ground locking.
501 status_ = "Applied manual pose to '" + selected_ +
502 "' (unlocked from ground).";
503 }
504 }
505 }
506 else if (applyClicked)
507 {
508 status_ = "No object selected.";
509 }
510 }
511
512 if (tab_.renameObject.wasClicked())
513 {
514 if (selected)
515 {
516 renameEntry(*selected, tab_.instanceName.getValue());
517 }
518 else
519 {
520 status_ = "No object selected.";
521 }
522 }
523
524 if (tab_.lockToGround.wasClicked())
525 {
526 if (!selected)
527 {
528 status_ = "No object selected.";
529 }
530 else
531 {
532 selected->manualPose = false;
533 selected->data.position.z() = groundSnappedZ(*selected, poseOf(selected->data));
534 sceneLayerDirty_ = true;
535 syncGui_ = true;
536 status_ = "Locked '" + selected_ + "' to " +
537 (selected->groundRef.empty() ? std::string("the default ground.")
538 : "'" + selected->groundRef + "'.");
539 }
540 }
541
542 const Eigen::Vector3f flipAxes[] = {
543 Eigen::Vector3f::UnitX(), Eigen::Vector3f::UnitY(), Eigen::Vector3f::UnitZ()};
544 RemoteGui::Client::Button* flipButtons[] = {&tab_.flipX, &tab_.flipY, &tab_.flipZ};
545 for (int axis = 0; axis < 3; ++axis)
546 {
547 if (flipButtons[axis]->wasClicked())
548 {
549 if (selected)
550 {
551 flipEntry(*selected, flipAxes[axis]);
552 }
553 else
554 {
555 status_ = "No object selected.";
556 }
557 }
558 }
559
560 if (tab_.deleteObject.wasClicked())
561 {
562 if (selected)
563 {
564 status_ = "Deleted '" + selected_ + "'.";
565 deleteEntry(selected_);
566 selected = nullptr;
567 }
568 else
569 {
570 status_ = "No object selected.";
571 }
572 }
573
574 if (tab_.saveScene.wasClicked())
575 {
576 saveScene(tab_.sceneFile.getValue());
577 }
578
579 if (tab_.loadScene.wasClicked())
580 {
581 loadScene(tab_.sceneFile.getValue());
582 }
583
584 if (tab_.frontYZ.wasClicked())
585 {
586 alignToPlane(0, true, "front yz-plane");
587 }
588 if (tab_.backYZ.wasClicked())
589 {
590 alignToPlane(0, false, "back yz-plane");
591 }
592 if (tab_.frontXZ.wasClicked())
593 {
594 alignToPlane(1, true, "front xz-plane");
595 }
596 if (tab_.backXZ.wasClicked())
597 {
598 alignToPlane(1, false, "back xz-plane");
599 }
600
601 if (tab_.clearScene.wasClicked())
602 {
603 entries_.clear();
604 pendingTransforms_.clear();
605 collisionModels_.clear();
606 selected_.clear();
607 sceneLayerDirty_ = true;
608 groundLayerDirty_ = true;
609 syncGui_ = true;
610 tabRebuildNeeded_ = true;
611 status_ = "Cleared scene.";
612 }
613
614 if (syncGui_)
615 {
616 syncGui_ = false;
617 if (Entry* entry = findEntry(selected_))
618 {
619 const std::string grounding =
620 entry->manualPose
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
634 : entry->groundRef);
635 }
636 else
637 {
638 tab_.selectedInfo.setText("<none>");
639 tab_.instanceName.setValue("");
640 tab_.groundObject.setValue(noneOption);
641 }
642 tab_.groundZ.setValue(groundZ_);
643 }
644
645 tab_.status.setText(status_);
646
647 if (tabRebuildNeeded_)
648 {
649 tabRebuildNeeded_ = false;
650 createRemoteGuiTab();
651 }
652 }
653
654 void
655 SceneEditor::run()
656 {
657 viz::StagedCommit stage;
658
659 // Immediately clear any layers left over from a previous run by
660 // committing them empty.
661 {
662 std::scoped_lock lock(mutex_);
663 sceneLayer_ = arviz.layer("Scene");
664 groundLayer_ = arviz.layer("Ground");
665 stage.add(sceneLayer_);
666 stage.add(groundLayer_);
667 }
668 viz::CommitResult result = arviz.commit(stage);
669 stage.reset();
670
671 // Stage the initial scene content and the first interaction request.
672 {
673 std::scoped_lock lock(mutex_);
674 rebuildSceneLayer();
675 rebuildGroundLayer();
676 stage.add(sceneLayer_);
677 stage.add(groundLayer_);
678 sceneLayerDirty_ = false;
679 groundLayerDirty_ = false;
680 stage.requestInteraction(sceneLayer_);
681 }
682
683 // This loop is structured like in ArVizInteractExample: commit the
684 // stage, then reset and rebuild it (interaction request plus all
685 // layers changed by interactions or by the RemoteGui thread) for the
686 // commit in the next cycle.
687 CycleUtil cycle(10.0f);
688 while (!task_->isStopped())
689 {
690 result = arviz.commit(stage);
691
692 stage.reset();
693
694 {
695 std::scoped_lock lock(mutex_);
696
697 stage.requestInteraction(sceneLayer_);
698
699 for (const viz::InteractionFeedback& interaction : result.interactions())
700 {
701 handleInteraction(interaction);
702 }
703
704 if (sceneLayerDirty_)
705 {
706 rebuildSceneLayer();
707 stage.add(sceneLayer_);
708 sceneLayerDirty_ = false;
709 }
710 if (groundLayerDirty_)
711 {
712 rebuildGroundLayer();
713 stage.add(groundLayer_);
714 groundLayerDirty_ = false;
715 }
716 }
717
718 cycle.waitForCycleDuration();
719 }
720 }
721
722 void
723 SceneEditor::handleInteraction(const viz::InteractionFeedback& interaction)
724 {
725 switch (interaction.type())
726 {
728 {
729 selectEntry(interaction.element());
730 }
731 break;
732
734 {
735 // The transformation is cumulative over the whole selection
736 // (the manipulator stays active between drags), so only track
737 // it here and apply it once, on deselection — applying it
738 // earlier would double it up.
739 pendingTransforms_[interaction.element()] = interaction.transformation();
740 }
741 break;
742
744 {
745 applyPendingTransform(interaction.element());
746 }
747 break;
748
750 {
751 Entry* entry = findEntry(interaction.element());
752 if (!entry)
753 {
754 break;
755 }
756 switch (interaction.chosenContextMenuEntry())
757 {
758 case 0:
759 flipEntry(*entry, Eigen::Vector3f::UnitX());
760 break;
761 case 1:
762 flipEntry(*entry, Eigen::Vector3f::UnitY());
763 break;
764 case 2:
765 flipEntry(*entry, Eigen::Vector3f::UnitZ());
766 break;
767 case 3:
768 entry->manualPose = !entry->manualPose;
769 if (!entry->manualPose)
770 {
771 entry->data.position.z() =
772 groundSnappedZ(*entry, poseOf(entry->data));
773 }
774 sceneLayerDirty_ = true;
775 syncGui_ = true;
776 break;
777 case 4:
778 deleteEntry(interaction.element());
779 break;
780 default:
781 break;
782 }
783 }
784 break;
785
786 default:
787 break;
788 }
789 }
790
791 void
792 SceneEditor::applyPendingTransform(const std::string& instanceName)
793 {
794 auto it = pendingTransforms_.find(instanceName);
795 if (it == pendingTransforms_.end())
796 {
797 return;
798 }
799 const Eigen::Matrix4f transform = it->second;
800 pendingTransforms_.erase(it);
801
802 Entry* entry = findEntry(instanceName);
803 if (!entry)
804 {
805 return;
806 }
807
808 // The interaction in ArViz allows a full 6-DOF transform (restricting
809 // the axes viewer-side is unreliable). Constrain it here instead.
810 const Eigen::Matrix4f candidate = transform * poseOf(entry->data);
811
812 Eigen::Vector3f newPosition = candidate.block<3, 1>(0, 3);
813 Eigen::Quaternionf newOrientation;
814 if (entry->manualPose)
815 {
816 newOrientation = Eigen::Quaternionf(candidate.block<3, 3>(0, 0)).normalized();
817 }
818 else
819 {
820 // Ground lock: keep only the yaw component of the applied
821 // rotation and stay on the ground plane.
822 const float deltaYaw = yawOf(Eigen::Quaternionf(transform.block<3, 3>(0, 0)));
823 newOrientation = (Eigen::AngleAxisf(deltaYaw, Eigen::Vector3f::UnitZ()) *
824 entry->data.orientation)
825 .normalized();
826 }
827
828 Eigen::Matrix4f newPose = Eigen::Matrix4f::Identity();
829 newPose.block<3, 3>(0, 0) = newOrientation.toRotationMatrix();
830 newPose.block<3, 1>(0, 3) = newPosition;
831
832 if (!entry->manualPose)
833 {
834 // Rest the lowest point of the bounding box on the ground plane.
835 newPosition.z() = groundSnappedZ(*entry, newPose);
836 newPose(2, 3) = newPosition.z();
837 }
838
839 // Even when the move is rejected, the layer must be re-committed to
840 // snap the visualization back to the entry's pose.
841 sceneLayerDirty_ = true;
842 syncGui_ = true;
843
844 if (!isMoveAllowed(*entry, newPose))
845 {
846 return;
847 }
848
849 entry->data.position = newPosition;
850 entry->data.orientation = newOrientation;
851 afterEntryPoseChanged(*entry);
852 }
853
854 bool
855 SceneEditor::applyManualPose(Entry& entry, const Eigen::Vector3f& position, float yawDeg)
856 {
857 const float deltaYaw =
858 std::remainder(yawDeg * deg2rad - yawOf(entry.data.orientation), 2.0 * M_PI);
859 const Eigen::Quaternionf newOrientation =
860 (Eigen::AngleAxisf(deltaYaw, Eigen::Vector3f::UnitZ()) * entry.data.orientation)
861 .normalized();
862
863 Eigen::Matrix4f newPose = Eigen::Matrix4f::Identity();
864 newPose.block<3, 3>(0, 0) = newOrientation.toRotationMatrix();
865 newPose.block<3, 1>(0, 3) = position;
866
867 if (!isMoveAllowed(entry, newPose))
868 {
869 // Snap the GUI back to the entry's actual pose.
870 syncGui_ = true;
871 return false;
872 }
873
874 entry.manualPose = true;
875 entry.data.position = position;
876 entry.data.orientation = newOrientation;
877 sceneLayerDirty_ = true;
878 syncGui_ = true;
879 afterEntryPoseChanged(entry);
880 return true;
881 }
882
883 std::string
884 SceneEditor::collidesWith(const Entry& entry, const Eigen::Matrix4f& candidatePose)
885 {
886 VirtualRobot::ObstaclePtr model = collisionModelOf(entry);
887 if (!model || !model->getCollisionModel())
888 {
889 return "";
890 }
891 model->setGlobalPose(candidatePose);
892
893 auto checker = VirtualRobot::CollisionChecker::getGlobalCollisionChecker();
894 for (const Entry& other : entries_)
895 {
896 if (&other == &entry)
897 {
898 continue;
899 }
900 // Objects rest on their ground, so do not count that contact.
901 if (other.data.instanceName == entry.groundRef ||
902 other.groundRef == entry.data.instanceName)
903 {
904 continue;
905 }
906 VirtualRobot::ObstaclePtr otherModel = collisionModelOf(other);
907 if (!otherModel || !otherModel->getCollisionModel())
908 {
909 continue;
910 }
911 otherModel->setGlobalPose(poseOf(other.data));
912 if (checker->checkCollision(model->getCollisionModel(),
913 otherModel->getCollisionModel()))
914 {
915 return other.data.instanceName;
916 }
917 }
918 return "";
919 }
920
921 bool
922 SceneEditor::isMoveAllowed(Entry& entry, const Eigen::Matrix4f& candidatePose)
923 {
924 // Entries that already collide may always be moved, so that existing
925 // collisions can be resolved.
926 const bool wasColliding = !collidesWith(entry, poseOf(entry.data)).empty();
927 if (wasColliding)
928 {
929 return true;
930 }
931 const std::string hit = collidesWith(entry, candidatePose);
932 if (!hit.empty())
933 {
934 status_ = "Move of '" + entry.data.instanceName + "' rejected: collides with '" +
935 hit + "'.";
936 return false;
937 }
938 return true;
939 }
940
941 const std::optional<simox::AxisAlignedBoundingBox>&
942 SceneEditor::localAabbOf(const Entry& entry)
943 {
944 auto it = localAabbs_.find(entry.data.className);
945 if (it == localAabbs_.end())
946 {
947 std::optional<simox::AxisAlignedBoundingBox> aabb;
948 try
949 {
950 if (std::optional<ObjectInfo> info =
951 objectFinder_.findObject(entry.data.className))
952 {
953 info->setLogError(false);
954 aabb = info->loadAABB();
955 }
956 }
957 catch (const std::exception& e)
958 {
959 ARMARX_WARNING << "Failed to load AABB of '" << entry.data.className
960 << "': " << e.what();
961 }
962 if (!aabb)
963 {
964 ARMARX_INFO << "No AABB (aabb.json) for '" << entry.data.className
965 << "', falling back to the collision model's bounding box.";
966 }
967 it = localAabbs_.emplace(entry.data.className, aabb).first;
968 }
969 return it->second;
970 }
971
972 std::optional<simox::AxisAlignedBoundingBox>
973 SceneEditor::globalAabb(const Entry& entry, const Eigen::Matrix4f& pose)
974 {
975 // Prefer the precomputed local AABB shipped with the object data;
976 // fall back to the collision model's local bounding box.
977 Eigen::Vector3f min;
978 Eigen::Vector3f max;
979 if (const std::optional<simox::AxisAlignedBoundingBox>& aabb = localAabbOf(entry))
980 {
981 min = aabb->min();
982 max = aabb->max();
983 }
984 else if (VirtualRobot::ObstaclePtr model = collisionModelOf(entry);
985 model && model->getCollisionModel())
986 {
987 const VirtualRobot::BoundingBox bbox =
988 model->getCollisionModel()->getBoundingBox(false);
989 min = bbox.getMin();
990 max = bbox.getMax();
991 }
992 else
993 {
994 return std::nullopt;
995 }
996
997 // Transform all 8 corners of the local box and take the extrema.
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)
1005 {
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);
1012 }
1013 return simox::AxisAlignedBoundingBox(globalMin, globalMax);
1014 }
1015
1016 float
1017 SceneEditor::groundHeightOf(const std::string& ref)
1018 {
1019 if (ref.empty())
1020 {
1021 return groundZ_;
1022 }
1023 Entry* ground = findEntry(ref);
1024 if (!ground)
1025 {
1026 return groundZ_;
1027 }
1028 if (const auto aabb = globalAabb(*ground, poseOf(ground->data)))
1029 {
1030 return aabb->max().z();
1031 }
1032 return groundZ_;
1033 }
1034
1035 float
1036 SceneEditor::groundHeightFor(const Entry& entry)
1037 {
1038 return groundHeightOf(entry.groundRef);
1039 }
1040
1041 void
1042 SceneEditor::reseatEntriesGroundedTo(const std::string& ref)
1043 {
1044 for (Entry& entry : entries_)
1045 {
1046 if (!entry.manualPose && entry.groundRef == ref &&
1047 entry.data.instanceName != ref)
1048 {
1049 entry.data.position.z() = groundSnappedZ(entry, poseOf(entry.data));
1050 sceneLayerDirty_ = true;
1051 }
1052 }
1053 }
1054
1055 void
1056 SceneEditor::afterEntryPoseChanged(const Entry& entry)
1057 {
1058 reseatEntriesGroundedTo(entry.data.instanceName);
1059 }
1060
1061 float
1062 SceneEditor::groundSnappedZ(const Entry& entry, const Eigen::Matrix4f& candidatePose)
1063 {
1064 const float ground = groundHeightFor(entry);
1065 const auto aabb = globalAabb(entry, candidatePose);
1066 if (!aabb)
1067 {
1068 return ground;
1069 }
1070 return candidatePose(2, 3) + (ground - aabb->min().z());
1071 }
1072
1073 VirtualRobot::ObstaclePtr
1074 SceneEditor::collisionModelOf(const Entry& entry)
1075 {
1076 auto it = collisionModels_.find(entry.data.instanceName);
1077 if (it != collisionModels_.end())
1078 {
1079 return it->second;
1080 }
1081
1082 VirtualRobot::ObstaclePtr model;
1083 try
1084 {
1085 model = ObjectFinder::loadObstacle(objectFinder_.findObject(entry.data.className));
1086 }
1087 catch (const std::exception& e)
1088 {
1089 ARMARX_WARNING << "Failed to load collision model of '" << entry.data.className
1090 << "': " << e.what();
1091 }
1092 if (!model)
1093 {
1094 ARMARX_WARNING << "No collision model for '" << entry.data.className
1095 << "', collision checks are skipped for '" << entry.data.instanceName
1096 << "'.";
1097 }
1098 collisionModels_[entry.data.instanceName] = model;
1099 return model;
1100 }
1101
1102 SceneEditor::Entry*
1103 SceneEditor::findEntry(const std::string& instanceName)
1104 {
1105 for (Entry& entry : entries_)
1106 {
1107 if (entry.data.instanceName == instanceName)
1108 {
1109 return &entry;
1110 }
1111 }
1112 return nullptr;
1113 }
1114
1115 SceneEditor::Entry&
1116 SceneEditor::addObject(const std::string& classId)
1117 {
1118 Entry& entry = entries_.emplace_back();
1119 entry.data.className = classId;
1120 entry.data.instanceName = makeUniqueInstanceName(classId);
1121 entry.groundRef.clear(); // New objects start on the default ground.
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); // Preload, so the first move does not stall.
1126 entry.data.position.z() = groundSnappedZ(entry, poseOf(entry.data));
1127 sceneLayerDirty_ = true;
1128 tabRebuildNeeded_ = true;
1129 return entry;
1130 }
1131
1132 void
1133 SceneEditor::renameEntry(Entry& entry, const std::string& newName)
1134 {
1135 const std::string oldName = entry.data.instanceName;
1136 if (newName.empty() || newName == noneOption)
1137 {
1138 status_ = "Cannot rename '" + oldName + "': invalid name '" + newName + "'.";
1139 syncGui_ = true;
1140 return;
1141 }
1142 if (newName == oldName)
1143 {
1144 return;
1145 }
1146 if (findEntry(newName) != nullptr)
1147 {
1148 status_ = "Cannot rename '" + oldName + "': an object named '" + newName +
1149 "' already exists.";
1150 syncGui_ = true;
1151 return;
1152 }
1153
1154 entry.data.instanceName = newName;
1155
1156 // Update all references to the old name.
1157 for (Entry& other : entries_)
1158 {
1159 if (other.groundRef == oldName)
1160 {
1161 other.groundRef = newName;
1162 }
1163 }
1164 if (auto node = collisionModels_.extract(oldName); !node.empty())
1165 {
1166 node.key() = newName;
1167 collisionModels_.insert(std::move(node));
1168 }
1169 if (auto it = pendingTransforms_.find(oldName); it != pendingTransforms_.end())
1170 {
1171 pendingTransforms_[newName] = it->second;
1172 pendingTransforms_.erase(it);
1173 }
1174 if (selected_ == oldName)
1175 {
1176 selected_ = newName;
1177 }
1178
1179 sceneLayerDirty_ = true;
1180 syncGui_ = true;
1181 tabRebuildNeeded_ = true;
1182 status_ = "Renamed '" + oldName + "' to '" + newName + "'.";
1183 }
1184
1185 void
1186 SceneEditor::deleteEntry(const std::string& instanceName)
1187 {
1188 // Requires GCC >8
1189 // std::erase_if(entries_,
1190 // [&](const Entry& e) { return e.data.instanceName == instanceName; });
1191
1192 entries_.erase(
1193 std::remove_if(
1194 entries_.begin(),
1195 entries_.end(),
1196 [&](const Entry& e)
1197 {
1198 return e.data.instanceName == instanceName;
1199 }),
1200 entries_.end());
1201
1202
1203 pendingTransforms_.erase(instanceName);
1204 collisionModels_.erase(instanceName);
1205 // Entries grounded to the deleted object fall back to the default ground.
1206 for (Entry& entry : entries_)
1207 {
1208 if (entry.groundRef == instanceName)
1209 {
1210 entry.groundRef.clear();
1211 if (!entry.manualPose)
1212 {
1213 entry.data.position.z() = groundSnappedZ(entry, poseOf(entry.data));
1214 }
1215 }
1216 }
1217 if (selected_ == instanceName)
1218 {
1219 selected_.clear();
1220 }
1221 sceneLayerDirty_ = true;
1222 syncGui_ = true;
1223 tabRebuildNeeded_ = true;
1224 }
1225
1226 void
1227 SceneEditor::flipEntry(Entry& entry, const Eigen::Vector3f& axis)
1228 {
1229 const Eigen::Quaternionf newOrientation =
1230 (Eigen::Quaternionf(Eigen::AngleAxisf(M_PI_2, axis)) * entry.data.orientation)
1231 .normalized();
1232
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;
1236
1237 // Flipping changes the bounding box, so re-snap locked objects.
1238 if (!entry.manualPose)
1239 {
1240 newPose(2, 3) = groundSnappedZ(entry, newPose);
1241 }
1242
1243 if (!isMoveAllowed(entry, newPose))
1244 {
1245 return;
1246 }
1247
1248 entry.data.orientation = newOrientation;
1249 entry.data.position.z() = newPose(2, 3);
1250 sceneLayerDirty_ = true;
1251 syncGui_ = true;
1252 status_ = "Flipped '" + entry.data.instanceName + "'.";
1253 afterEntryPoseChanged(entry);
1254 }
1255
1256 void
1257 SceneEditor::selectEntry(const std::string& instanceName)
1258 {
1259 if (findEntry(instanceName))
1260 {
1261 selected_ = instanceName;
1262 syncGui_ = true;
1263 }
1264 }
1265
1266 void
1267 SceneEditor::setGroundZ(float groundZ)
1268 {
1269 groundZ_ = groundZ;
1270 reseatEntriesGroundedTo("");
1271 groundLayerDirty_ = true;
1272 syncGui_ = true;
1273 }
1274
1275 void
1276 SceneEditor::alignToPlane(int axis, bool minSide, const std::string& planeName)
1277 {
1278 Entry* selected = findEntry(selected_);
1279 if (!selected)
1280 {
1281 status_ = "No object selected.";
1282 return;
1283 }
1284 const std::string targetName = tab_.alignTarget.getValue();
1285 Entry* target = findEntry(targetName);
1286 if (!target)
1287 {
1288 status_ = "No reference object selected.";
1289 return;
1290 }
1291 if (target == selected)
1292 {
1293 status_ = "Cannot align an object with itself.";
1294 return;
1295 }
1296
1297 const auto selectedBox = globalAabb(*selected, poseOf(selected->data));
1298 const auto targetBox = globalAabb(*target, poseOf(target->data));
1299 if (!selectedBox || !targetBox)
1300 {
1301 status_ = "Cannot align: missing bounding box for '" +
1302 (selectedBox ? targetName : selected_) + "'.";
1303 return;
1304 }
1305
1306 // Inside: make the selected object's face coplanar with the
1307 // reference's face. Outside: place the selected object beyond that
1308 // plane, touching it with its opposite face (with a small clearance
1309 // so that exact contact does not count as a collision).
1310 const bool outside = tab_.alignOutside.getValue();
1311 constexpr float clearance = 0.5f;
1312 float delta = 0.0f;
1313 if (outside)
1314 {
1315 delta = minSide
1316 ? (targetBox->min()(axis) - clearance) - selectedBox->max()(axis)
1317 : (targetBox->max()(axis) + clearance) - selectedBox->min()(axis);
1318 }
1319 else
1320 {
1321 delta = minSide ? targetBox->min()(axis) - selectedBox->min()(axis)
1322 : targetBox->max()(axis) - selectedBox->max()(axis);
1323 }
1324
1325 Eigen::Vector3f newPosition = selected->data.position;
1326 newPosition(axis) += delta;
1327
1328 Eigen::Matrix4f newPose = poseOf(selected->data);
1329 newPose.block<3, 1>(0, 3) = newPosition;
1330
1331 if (!isMoveAllowed(*selected, newPose))
1332 {
1333 syncGui_ = true;
1334 return;
1335 }
1336
1337 selected->data.position = newPosition;
1338 sceneLayerDirty_ = true;
1339 syncGui_ = true;
1340 status_ = "Aligned '" + selected_ + "' with the " + planeName + " of '" + targetName +
1341 (outside ? "' (outside)." : "' (inside).");
1342 afterEntryPoseChanged(*selected);
1343 }
1344
1345 std::string
1346 SceneEditor::makeUniqueInstanceName(const std::string& classId)
1347 {
1348 const std::string className = ObjectID(classId).className();
1349 std::string name;
1350 do
1351 {
1352 name = className + "_" + std::to_string(nextId_++);
1353 } while (findEntry(name) != nullptr);
1354 return name;
1355 }
1356
1357 void
1358 SceneEditor::rebuildSceneLayer()
1359 {
1360 sceneLayer_ = arviz.layer("Scene");
1361 for (const Entry& entry : entries_)
1362 {
1363 viz::Object object(entry.data.instanceName);
1364 object.fileByObjectFinder(entry.data.className, objectsPackage_);
1365 object.position(entry.data.position).orientation(entry.data.orientation);
1366
1367 // Enable the full transform and constrain it client-side when the
1368 // resulting transformation is applied (see applyPendingTransform).
1369 viz::InteractionDescription interaction = viz::interaction();
1370 interaction.contextMenu(contextMenuEntries).hideDuringTransform().transform();
1371 object.enable(interaction);
1372
1373 sceneLayer_.add(object);
1374 }
1375 }
1376
1377 void
1378 SceneEditor::rebuildGroundLayer()
1379 {
1380 groundLayer_ = arviz.layer("Ground");
1381 // The default ground plane is always shown; a designated ground
1382 // object is drawn (and interactable) in the scene layer on top.
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);
1388 }
1389
1390 std::filesystem::path
1391 SceneEditor::resolveScenePath(std::string name) const
1392 {
1393 if (name.empty())
1394 {
1395 name = "NewScene";
1396 }
1397 if (not simox::alg::ends_with(name, ".json"))
1398 {
1399 name += ".json";
1400 }
1401 std::filesystem::path path(name);
1402 if (!path.is_absolute())
1403 {
1404 if (!sceneStorageDirectory_.empty())
1405 {
1406 // SceneStorageDirectory holds the scene files directly.
1407 path = std::filesystem::path(sceneStorageDirectory_) / path;
1408 }
1409 else
1410 {
1411 // Resolve relative to the scenes package's data directory.
1412 CMakePackageFinder packageFinder(scenesPackage_);
1413 const std::string dataDir = packageFinder.getDataDir();
1414 if (!packageFinder.packageFound() || dataDir.empty())
1415 {
1416 throw LocalException(
1417 "Could not locate the scenes package '" + scenesPackage_ +
1418 "'. Set the SceneStorageDirectory property to an absolute path.");
1419 }
1420 path = std::filesystem::path(dataDir) / scenesPackage_ / "scenes" / path;
1421 }
1422 }
1423
1424 // Make the path absolute so it does not depend on the process's
1425 // current working directory (which is not where scenes live).
1426 if (!path.is_absolute())
1427 {
1428 path = std::filesystem::absolute(path);
1429 }
1430 return path.lexically_normal();
1431 }
1432
1433 std::filesystem::path
1434 SceneEditor::groundingPathFor(const std::filesystem::path& scenePath) const
1435 {
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)
1439 {
1440 if (parts[i].string() == "scenes")
1441 {
1442 lastScenes = static_cast<int>(i);
1443 }
1444 }
1445 if (lastScenes < 0)
1446 {
1447 return scenePath.parent_path() / (scenePath.stem().string() + ".groundings.json");
1448 }
1449 std::filesystem::path result;
1450 for (std::size_t i = 0; i < parts.size(); ++i)
1451 {
1452 result /= (static_cast<int>(i) == lastScenes ? std::filesystem::path("groundings")
1453 : parts[i]);
1454 }
1455 return result;
1456 }
1457
1458 void
1459 SceneEditor::saveScene(const std::string& fileArg)
1460 {
1461 try
1462 {
1463 const std::filesystem::path path = resolveScenePath(fileArg);
1464
1465 objects::Scene scene;
1466 // Instance names are kept as they are; only unnamed entries get a
1467 // generated name (per class, not colliding with the kept names).
1468 std::set<std::string> takenNames;
1469 for (const Entry& entry : entries_)
1470 {
1471 takenNames.insert(entry.data.instanceName);
1472 }
1473 std::map<std::string, int> perClassCount;
1474 for (const Entry& entry : entries_)
1475 {
1476 objects::SceneObject& obj = scene.objects.emplace_back(entry.data);
1477 if (!obj.instanceName.empty())
1478 {
1479 continue;
1480 }
1481 const std::string className = ObjectID(entry.data.className).className();
1482 do
1483 {
1484 obj.instanceName =
1485 className + "_" + std::to_string(perClassCount[className]++);
1486 } while (!takenNames.insert(obj.instanceName).second);
1487 }
1488
1489 std::filesystem::create_directories(path.parent_path());
1490 const simox::json::json j = scene;
1491 simox::json::write(path.string(), j, 2);
1492
1493 // Save the grounding info to a parallel directory with the same
1494 // file name, so it can be restored when the scene is loaded.
1495 const std::filesystem::path groundingPath = groundingPathFor(path);
1496 // Objects are referenced by their index in the scene file.
1497 const auto indexOf = [this](const std::string& instanceName) -> int
1498 {
1499 for (std::size_t i = 0; i < entries_.size(); ++i)
1500 {
1501 if (entries_[i].data.instanceName == instanceName)
1502 {
1503 return static_cast<int>(i);
1504 }
1505 }
1506 return -1;
1507 };
1508 simox::json::json grounding;
1509 grounding["groundings"] = simox::json::json::array();
1510 for (std::size_t i = 0; i < entries_.size(); ++i)
1511 {
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},
1518 });
1519 }
1520 std::filesystem::create_directories(groundingPath.parent_path());
1521 simox::json::write(groundingPath.string(), grounding, 2);
1522
1523 status_ = "Saved " + std::to_string(scene.objects.size()) + " objects to " +
1524 path.string() + " (grounding info: " + groundingPath.string() + ").";
1525 ARMARX_INFO << status_;
1526 }
1527 catch (const std::exception& e)
1528 {
1529 status_ = std::string("Saving scene failed: ") + e.what();
1530 ARMARX_WARNING << status_;
1531 }
1532 }
1533
1534 void
1535 SceneEditor::loadScene(const std::string& fileArg)
1536 {
1537 try
1538 {
1539 const std::filesystem::path path = resolveScenePath(fileArg);
1540 ARMARX_INFO << "Loading scene from " << path << ".";
1541 if (!std::filesystem::exists(path))
1542 {
1543 status_ = "Scene file does not exist: " + path.string();
1544 ARMARX_WARNING << status_;
1545 return;
1546 }
1547
1548 const simox::json::json j = simox::json::read(path.string());
1549 const auto scene = j.get<objects::Scene>();
1550
1551 entries_.clear();
1552 pendingTransforms_.clear();
1553 collisionModels_.clear();
1554 selected_.clear();
1555
1556 for (const objects::SceneObject& obj : scene.objects)
1557 {
1558 Entry& entry = entries_.emplace_back();
1559 entry.data = obj;
1560 if (entry.data.instanceName.empty() ||
1561 findEntry(entry.data.instanceName) != &entry)
1562 {
1563 entry.data.instanceName = makeUniqueInstanceName(entry.data.className);
1564 }
1565 }
1566
1567 // Restore the grounding info from the parallel groundings file.
1568 bool groundingLoaded = false;
1569 const std::filesystem::path groundingPath = groundingPathFor(path);
1570 if (std::filesystem::exists(groundingPath))
1571 {
1572 try
1573 {
1574 const simox::json::json grounding =
1575 simox::json::read(groundingPath.string());
1576 for (const auto& item : grounding.at("groundings"))
1577 {
1578 // Objects are referenced by their index in the scene
1579 // file; the instance name is a fallback.
1580 Entry* entry = nullptr;
1581 const int index = item.value("index", -1);
1582 if (index >= 0 && index < static_cast<int>(entries_.size()))
1583 {
1584 entry = &entries_[index];
1585 }
1586 else if (item.contains("instanceName"))
1587 {
1588 entry = findEntry(item.at("instanceName").get<std::string>());
1589 }
1590 if (!entry)
1591 {
1592 continue;
1593 }
1594
1595 entry->groundRef.clear();
1596 if (item.contains("ground"))
1597 {
1598 const auto& ground = item.at("ground");
1599 if (ground.is_number_integer())
1600 {
1601 const int groundIndex = ground.get<int>();
1602 if (groundIndex >= 0 &&
1603 groundIndex < static_cast<int>(entries_.size()))
1604 {
1605 entry->groundRef =
1606 entries_[groundIndex].data.instanceName;
1607 }
1608 }
1609 else if (ground.is_string())
1610 {
1611 entry->groundRef = ground.get<std::string>();
1612 }
1613 }
1614 entry->manualPose = !item.value("locked", true);
1615 }
1616 groundingLoaded = true;
1617 }
1618 catch (const std::exception& e)
1619 {
1620 ARMARX_WARNING << "Failed to load grounding info from " << groundingPath
1621 << ": " << e.what();
1622 }
1623 }
1624
1625 for (Entry& entry : entries_)
1626 {
1627 if (!entry.groundRef.empty() &&
1628 (entry.groundRef == entry.data.instanceName ||
1629 findEntry(entry.groundRef) == nullptr))
1630 {
1631 entry.groundRef.clear();
1632 }
1633 if (groundingLoaded)
1634 {
1635 if (!entry.manualPose)
1636 {
1637 entry.data.position.z() = groundSnappedZ(entry, poseOf(entry.data));
1638 }
1639 }
1640 else
1641 {
1642 // No grounding info: treat objects resting on the default
1643 // ground as locked, everything else as manually posed.
1644 entry.manualPose =
1645 std::abs(entry.data.position.z() -
1646 groundSnappedZ(entry, poseOf(entry.data))) > 0.5f;
1647 }
1648 }
1649
1650 sceneLayerDirty_ = true;
1651 groundLayerDirty_ = true;
1652 syncGui_ = true;
1653 tabRebuildNeeded_ = true;
1654 status_ = "Loaded " + std::to_string(entries_.size()) + " objects from " +
1655 path.string() + ".";
1656 ARMARX_INFO << status_;
1657 }
1658 catch (const std::exception& e)
1659 {
1660 status_ = std::string("Loading scene failed: ") + e.what();
1661 ARMARX_WARNING << status_;
1662 }
1663 }
1664
1665 float
1666 SceneEditor::yawOf(const Eigen::Quaternionf& q)
1667 {
1668 const Eigen::Matrix3f rot = q.toRotationMatrix();
1669 return std::atan2(rot(1, 0), rot(0, 0));
1670 }
1671
1672 Eigen::Matrix4f
1673 SceneEditor::poseOf(const objects::SceneObject& obj)
1674 {
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;
1678 return pose;
1679 }
1680} // namespace armarx
int Label(int n[], int size, int *curLabel, MiscLib::Vector< std::pair< int, size_t > > *labels)
Definition Bitmap.cpp:801
uint8_t data[1]
uint8_t index
#define M_PI
Definition MathTools.h:17
ComponentPropertyDefinitions(std::string prefix, bool hasObjectNameParameter=true)
Definition Component.cpp:44
std::string getConfigIdentifier()
Retrieve config identifier for this component as set in constructor.
Definition Component.cpp:88
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.
Definition ObjectInfo.h:37
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
Definition Client.cpp:80
CommitResult commit(StagedCommit const &commit)
Definition Client.cpp:89
#define ARMARX_INFO
The normal logging level.
Definition Logging.h:179
#define ARMARX_WARNING
The logging level for unexpected behaviour, but not a serious problem.
Definition Logging.h:191
#define q
Quaternion< float, 0 > Quaternionf
armem::MemoryID ObjectID
Definition types.h:79
InteractionDescription interaction()
Definition ElementOps.h:109
@ Transform
The element was transformed (translated or rotated).
Definition Interaction.h:24
@ ContextMenuChosen
A context menu entry was chosen.
Definition Interaction.h:21
@ Deselect
An element was deselected.
Definition Interaction.h:18
@ Select
An element was selected.
Definition Interaction.h:16
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...
Definition algorithm.h:351
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)
Definition point.hpp:102
void RemoteGui_createTab(std::string const &name, RemoteGui::Client::Widget const &rootWidget, RemoteGui::Client::Tab *tab)
std::optional< bool > isStatic
Definition Scene.h:47
Eigen::Quaternionf orientation
Definition Scene.h:45
std::string instanceName
Definition Scene.h:40
Eigen::Vector3f position
Definition Scene.h:44
InteractionFeedbackRange interactions() const
Definition Client.h:85
Self & contextMenu(std::vector< std::string > const &options)
Definition ElementOps.h:54
A staged commit prepares multiple layers to be committed.
Definition Client.h:30
void requestInteraction(Layer const &layer)
Request interaction feedback for a particular layer.
Definition Client.h:56
void add(Layer const &layer)
Stage a layer to be committed later via client.apply(*this)
Definition Client.h:36
void reset()
Reset all staged layers and interaction requests.
Definition Client.h:66