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