RoomEditor.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 navigation::ArmarXObjects::room_editor
17 * @author Fabian Reister ( fabian dot reister at kit dot edu )
18 * @date 2026
19 * @copyright http://www.gnu.org/licenses/gpl-2.0.txt
20 * GNU General Public License
21 */
22
23#include "RoomEditor.h"
24
25#include <algorithm>
26#include <cmath>
27#include <cstddef>
28#include <string>
29#include <utility>
30#include <vector>
31
32#include <Eigen/Core>
33#include <Eigen/Geometry>
34
36
41
43{
44
45 namespace
46 {
47 /// The minimum number of vertices a room polygon must retain.
48 constexpr std::size_t MinPolygonSize = 3;
49
50 const std::vector<std::string> VertexContextMenu{"Insert point after",
51 "Insert point before",
52 "Delete point"};
53
54 const std::vector<std::string> EdgeContextMenu{"Cut edge here"};
55
56 /// Human readable description of an interaction, for diagnosing the ArViz setup.
57 std::string
58 describe(const viz::InteractionFeedback& interaction)
59 {
60 std::string description = viz::toString(interaction.type());
61
63 {
64 description += "(";
65 description += interaction.isTransformBegin() ? "begin " : "";
66 description += interaction.isTransformDuring() ? "during " : "";
67 description += interaction.isTransformEnd() ? "end" : "";
68 description += ")";
69 }
70
71 return description + " on `" + interaction.element() + "`";
72 }
73
74 Eigen::Vector3f
75 to3D(const Eigen::Vector2f& point, float z)
76 {
77 return Eigen::Vector3f{point.x(), point.y(), z};
78 }
79
80 Eigen::Vector2f
81 centroid(const std::vector<Eigen::Vector2f>& polygon)
82 {
83 Eigen::Vector2f sum = Eigen::Vector2f::Zero();
84 for (const auto& point : polygon)
85 {
86 sum += point;
87 }
88 return sum / static_cast<float>(std::max<std::size_t>(polygon.size(), 1));
89 }
90
91 /// Rotates `polygon` by `angle` [rad] about `pivot`, then translates it by `translation`.
92 std::vector<Eigen::Vector2f>
93 applyRigidMotion(const std::vector<Eigen::Vector2f>& polygon,
94 const Eigen::Vector2f& pivot,
95 float angle,
96 const Eigen::Vector2f& translation)
97 {
98 const Eigen::Rotation2Df rotation(angle);
99
100 std::vector<Eigen::Vector2f> result;
101 result.reserve(polygon.size());
102 for (const auto& point : polygon)
103 {
104 result.push_back(pivot + rotation * (point - pivot) + translation);
105 }
106 return result;
107 }
108 } // namespace
109
111 arviz(std::move(arviz)), parameters(parameters)
112 {
113 }
114
115 void
116 RoomEditor::setRooms(const std::vector<EditableRoom>& rooms)
117 {
118 pendingTransform.reset();
119 rooms_ = rooms;
120 }
121
122 const std::vector<EditableRoom>&
124 {
125 return rooms_;
126 }
127
128 void
130 {
131 roomChangedCallback = std::move(callback);
132 }
133
134 bool
136 {
137 return pendingTransform.has_value();
138 }
139
140 void
142 {
143 if (handleLayer.data_.name.empty())
144 {
145 // Nothing has been drawn yet, so there cannot be any interaction either.
146 return;
147 }
148
149 stage.requestInteraction(handleLayer);
150
151 for (const viz::InteractionFeedback& interaction : result.interactions())
152 {
153 if (interaction.layer() != handleLayer.data_.name)
154 {
155 continue;
156 }
157
158 handleInteraction(interaction, stage);
159 }
160 }
161
162 void
163 RoomEditor::handleInteraction(const viz::InteractionFeedback& interaction,
164 viz::StagedCommit& stage)
165 {
166 // Dragging produces a lot of intermediate events, so only the interesting ones are
167 // logged at INFO. If nothing shows up here at all while dragging in ArViz, the
168 // interaction never reached this component (see the note in `Component::run()`).
169 const bool isDragUpdate = interaction.type() == viz::InteractionFeedbackType::Transform and
170 not interaction.isTransformEnd();
171 if (isDragUpdate)
172 {
173 ARMARX_VERBOSE << "Interaction: " << describe(interaction);
174 }
175 else
176 {
177 ARMARX_INFO << "Interaction: " << describe(interaction);
178 }
179
180 const auto it = handleByElement.find(interaction.element());
181 if (it == handleByElement.end())
182 {
183 ARMARX_VERBOSE << "Interaction with unknown element `" << interaction.element() << "`.";
184 return;
185 }
186
187 const Handle handle = it->second;
188
189 switch (interaction.type())
190 {
192 {
193 const Eigen::Matrix4f transform = interaction.transformation();
194 const Eigen::Vector3f translation = transform.block<3, 1>(0, 3);
195
196 // The interaction is restricted to the XY plane, so the z component is dropped.
197 if (handle.type == Handle::Type::Move)
198 {
199 const Eigen::Matrix3f rotation = transform.block<3, 3>(0, 0);
200 const float angle = std::atan2(rotation(1, 0), rotation(0, 0));
201
202 pendingTransform = PendingTransform{.element = interaction.element(),
203 .handle = handle,
204 .rotation = angle,
205 .translation = translation.head<2>()};
206 }
207 else
208 {
209 pendingTransform =
210 PendingTransform{.element = interaction.element(),
211 .handle = handle,
212 .position = handlePosition(handle) + translation.head<2>()};
213 }
214
215 if (interaction.isTransformEnd())
216 {
217 applyPendingTransform(stage);
218 }
219 else
220 {
221 // Live preview of the polygon while dragging.
222 redrawRooms(stage);
223 }
224 }
225 break;
226
228 {
229 // Depending on the ArViz GUI, the drag may only be concluded by a deselect.
230 if (pendingTransform.has_value() and
231 pendingTransform->element == interaction.element())
232 {
233 applyPendingTransform(stage);
234 }
235 }
236 break;
237
239 {
240 handleContextMenu(handle, interaction.chosenContextMenuEntry(), stage);
241 }
242 break;
243
244 default:
245 // Nothing to do for the remaining interaction types.
246 break;
247 }
248 }
249
250 void
251 RoomEditor::applyPendingTransform(viz::StagedCommit& stage)
252 {
253 if (not pendingTransform.has_value())
254 {
255 return;
256 }
257
258 const PendingTransform pending = *pendingTransform;
259 pendingTransform.reset();
260
261 EditableRoom& editable = rooms_.at(pending.handle.roomIndex);
262 std::vector<Eigen::Vector2f>& polygon = editable.room.polygon;
263
264 if (pending.handle.type == Handle::Type::Move)
265 {
266 polygon = applyRigidMotion(
267 polygon, centroid(polygon), pending.rotation, pending.translation);
268
269 ARMARX_INFO << "Moved room `" << editable.room.name << "` by ["
270 << pending.translation.x() << ", " << pending.translation.y()
271 << "] and rotated it by " << pending.rotation << " rad.";
272
273 notifyRoomChanged(pending.handle.roomIndex);
274 redrawAll(stage);
275 return;
276 }
277
278 if (pending.handle.pointIndex >= polygon.size())
279 {
280 ARMARX_WARNING << "The edited polygon changed underneath the interaction. "
281 "Discarding the transformation.";
282 redrawAll(stage);
283 return;
284 }
285
286 if (pending.handle.type == Handle::Type::Vertex)
287 {
288 polygon.at(pending.handle.pointIndex) = pending.position;
289
290 ARMARX_INFO << "Moved point " << pending.handle.pointIndex << " of room `"
291 << editable.room.name << "` to [" << pending.position.x() << ", "
292 << pending.position.y() << "].";
293 }
294 else
295 {
296 polygon.insert(polygon.begin() +
297 static_cast<std::ptrdiff_t>(pending.handle.pointIndex) + 1,
298 pending.position);
299
300 ARMARX_INFO << "Cut edge " << pending.handle.pointIndex << " of room `"
301 << editable.room.name << "` at [" << pending.position.x() << ", "
302 << pending.position.y() << "].";
303 }
304
305 notifyRoomChanged(pending.handle.roomIndex);
306 redrawAll(stage);
307 }
308
309 void
310 RoomEditor::handleContextMenu(const Handle& handle, int entry, viz::StagedCommit& stage)
311 {
312 // A context menu action always supersedes a (possibly) ongoing drag.
313 pendingTransform.reset();
314
315 EditableRoom& editable = rooms_.at(handle.roomIndex);
316 std::vector<Eigen::Vector2f>& polygon = editable.room.polygon;
317
318 if (handle.pointIndex >= polygon.size())
319 {
320 ARMARX_WARNING << "The edited polygon changed underneath the interaction. "
321 "Ignoring the context menu action.";
322 return;
323 }
324
325 const auto insertAt = [&polygon](std::size_t index, const Eigen::Vector2f& point)
326 { polygon.insert(polygon.begin() + static_cast<std::ptrdiff_t>(index), point); };
327
328 if (handle.type == Handle::Type::Edge)
329 {
330 // The edge handle has a single entry: cut the edge at its midpoint.
331 const Eigen::Vector2f midpoint = handlePosition(handle);
332 insertAt(handle.pointIndex + 1, midpoint);
333
334 ARMARX_INFO << "Cut edge " << handle.pointIndex << " of room `" << editable.room.name
335 << "`.";
336 }
337 else
338 {
339 const std::size_t i = handle.pointIndex;
340 const std::size_t n = polygon.size();
341
342 switch (static_cast<ContextMenuEntry>(entry))
343 {
344 case ContextMenuEntry::InsertPointAfter:
345 {
346 const Eigen::Vector2f newPoint =
347 0.5F * (polygon.at(i) + polygon.at((i + 1) % n));
348 insertAt(i + 1, newPoint);
349
350 ARMARX_INFO << "Inserted a point after point " << i << " of room `"
351 << editable.room.name << "`.";
352 }
353 break;
354
355 case ContextMenuEntry::InsertPointBefore:
356 {
357 const Eigen::Vector2f newPoint =
358 0.5F * (polygon.at(i) + polygon.at((i + n - 1) % n));
359 insertAt(i, newPoint);
360
361 ARMARX_INFO << "Inserted a point before point " << i << " of room `"
362 << editable.room.name << "`.";
363 }
364 break;
365
366 case ContextMenuEntry::DeletePoint:
367 {
368 if (n <= MinPolygonSize)
369 {
370 ARMARX_WARNING << "Refusing to delete point " << i << " of room `"
371 << editable.room.name << "`: a polygon needs at least "
372 << MinPolygonSize << " points.";
373 return;
374 }
375
376 polygon.erase(polygon.begin() + static_cast<std::ptrdiff_t>(i));
377
378 ARMARX_INFO << "Deleted point " << i << " of room `" << editable.room.name
379 << "`.";
380 }
381 break;
382
383 default:
384 {
385 ARMARX_WARNING << "Unknown context menu entry " << entry << ".";
386 return;
387 }
388 }
389 }
390
391 notifyRoomChanged(handle.roomIndex);
392 redrawAll(stage);
393 }
394
395 void
397 {
398 handleByElement.clear();
399 handleLayer = arviz.layer(parameters.layerPrefix + "_Handles");
400
401 for (std::size_t roomIndex = 0; roomIndex < rooms_.size(); ++roomIndex)
402 {
403 const EditableRoom& editable = rooms_.at(roomIndex);
404 const std::vector<Eigen::Vector2f>& polygon = editable.room.polygon;
405
406 const float z = editable.room.zFloor + parameters.handleZOffset;
407
408 for (std::size_t i = 0; i < polygon.size(); ++i)
409 {
410 const Handle handle{
411 .type = Handle::Type::Vertex, .roomIndex = roomIndex, .pointIndex = i};
412 const std::string name = elementName(handle);
413 handleByElement[name] = handle;
414
415 handleLayer.add(viz::Sphere(name)
416 .position(to3D(polygon.at(i), z))
417 .radius(parameters.vertexHandleRadius)
418 .colorGlasbeyLUT(roomIndex)
420 .translation(viz::AXES_XY)
421 .contextMenu(VertexContextMenu)));
422 }
423
424 // One handle per edge, sitting on its midpoint. The last edge closes the polygon.
425 if (polygon.size() >= 2)
426 {
427 for (std::size_t i = 0; i < polygon.size(); ++i)
428 {
429 const Handle handle{
430 .type = Handle::Type::Edge, .roomIndex = roomIndex, .pointIndex = i};
431 const std::string name = elementName(handle);
432 handleByElement[name] = handle;
433
434 handleLayer.add(viz::Box(name)
435 .position(to3D(handlePosition(handle), z))
436 .size(parameters.edgeHandleSize)
437 .color(viz::Color::gray(160))
439 .translation(viz::AXES_XY)
440 .contextMenu(EdgeContextMenu)));
441 }
442 }
443
444 // One handle in the room center to translate/rotate the whole room in the XY plane.
445 if (polygon.size() >= MinPolygonSize)
446 {
447 const Handle handle{
448 .type = Handle::Type::Move, .roomIndex = roomIndex, .pointIndex = 0};
449 const std::string name = elementName(handle);
450 handleByElement[name] = handle;
451
452 handleLayer.add(viz::Cylinder(name)
453 .position(to3D(handlePosition(handle), z))
454 .radius(parameters.moveHandleRadius)
455 .height(parameters.moveHandleHeight)
456 .color(viz::Color::gray(200))
458 .translation(viz::AXES_XY)
459 .rotation(viz::AXES_Z)));
460 }
461 }
462
463 stage.add(handleLayer);
464
465 redrawRooms(stage);
466 }
467
468 void
469 RoomEditor::redrawRooms(viz::StagedCommit& stage)
470 {
471 roomLayer = arviz.layer(parameters.layerPrefix + "_Rooms");
472
473 for (std::size_t roomIndex = 0; roomIndex < rooms_.size(); ++roomIndex)
474 {
475 const EditableRoom& editable = rooms_.at(roomIndex);
476 const std::vector<Eigen::Vector2f> polygon = previewPolygon(roomIndex);
477
478 if (polygon.size() < 2)
479 {
480 continue;
481 }
482
483 const std::string prefix = editable.providerSegmentName + "/" + editable.room.name;
484 const float z = editable.room.zFloor;
485
486 viz::Path outline(prefix + "/outline");
487 for (const auto& point : polygon)
488 {
489 outline.addPoint(to3D(point, z));
490 }
491 // Close the outline.
492 outline.addPoint(to3D(polygon.front(), z));
493 outline.colorGlasbeyLUT(roomIndex).width(parameters.lineWidth);
494 roomLayer.add(outline);
495
496 if (polygon.size() >= MinPolygonSize)
497 {
498 viz::Polygon area(prefix + "/polygon");
499 for (const auto& point : polygon)
500 {
501 area.addPoint(to3D(point, z));
502 }
503 area.colorGlasbeyLUT(roomIndex, 60);
504 area.lineColorGlasbeyLUT(roomIndex);
505 area.lineWidth(0);
506 roomLayer.add(area);
507 }
508
509 roomLayer.add(viz::Text(prefix + "/label")
510 .text(editable.room.name)
511 .position(to3D(centroid(polygon), z))
512 .scale(parameters.labelScale));
513 }
514
515 stage.add(roomLayer);
516 }
517
518 std::vector<Eigen::Vector2f>
519 RoomEditor::previewPolygon(std::size_t roomIndex) const
520 {
521 std::vector<Eigen::Vector2f> polygon = rooms_.at(roomIndex).room.polygon;
522
523 if (not pendingTransform.has_value() or pendingTransform->handle.roomIndex != roomIndex)
524 {
525 return polygon;
526 }
527
528 const Handle& handle = pendingTransform->handle;
529
530 if (handle.type == Handle::Type::Move)
531 {
532 return applyRigidMotion(polygon,
533 centroid(polygon),
534 pendingTransform->rotation,
535 pendingTransform->translation);
536 }
537
538 if (handle.pointIndex >= polygon.size())
539 {
540 return polygon;
541 }
542
543 if (handle.type == Handle::Type::Vertex)
544 {
545 polygon.at(handle.pointIndex) = pendingTransform->position;
546 }
547 else
548 {
549 polygon.insert(polygon.begin() + static_cast<std::ptrdiff_t>(handle.pointIndex) + 1,
550 pendingTransform->position);
551 }
552
553 return polygon;
554 }
555
556 Eigen::Vector2f
557 RoomEditor::handlePosition(const Handle& handle) const
558 {
559 const std::vector<Eigen::Vector2f>& polygon = rooms_.at(handle.roomIndex).room.polygon;
560
561 if (handle.type == Handle::Type::Move)
562 {
563 return centroid(polygon);
564 }
565
566 if (handle.type == Handle::Type::Vertex)
567 {
568 return polygon.at(handle.pointIndex);
569 }
570
571 const std::size_t next = (handle.pointIndex + 1) % polygon.size();
572 return 0.5F * (polygon.at(handle.pointIndex) + polygon.at(next));
573 }
574
575 std::string
576 RoomEditor::elementName(const Handle& handle) const
577 {
578 const EditableRoom& editable = rooms_.at(handle.roomIndex);
579 const std::string prefix = editable.providerSegmentName + "/" + editable.room.name;
580
581 if (handle.type == Handle::Type::Move)
582 {
583 return prefix + "/move";
584 }
585
586 const std::string suffix = handle.type == Handle::Type::Vertex ? "/vertex_" : "/edge_";
587
588 return prefix + suffix + std::to_string(handle.pointIndex);
589 }
590
591 void
592 RoomEditor::notifyRoomChanged(std::size_t roomIndex) const
593 {
594 if (roomChangedCallback)
595 {
596 roomChangedCallback(rooms_.at(roomIndex));
597 }
598 }
599
600} // namespace armarx::navigation::components::room_editor
uint8_t index
bool isInteracting() const
True while the user is dragging a handle. Reloading should be paused meanwhile.
void update(const viz::CommitResult &result, viz::StagedCommit &stage)
Processes the interaction feedback of the previous commit and stages everything that has to be (re-)d...
const std::vector< EditableRoom > & rooms() const
void setRooms(const std::vector< EditableRoom > &rooms)
Replaces the edited rooms, e.g.
void setRoomChangedCallback(RoomChangedCallback callback)
std::function< void(const EditableRoom &)> RoomChangedCallback
Called whenever a room was modified by the user.
Definition RoomEditor.h:93
RoomEditor(viz::Client arviz, const Parameters &parameters)
void redrawAll(viz::StagedCommit &stage)
Stages a full redraw of all layers (rooms and handles).
virtual Layer layer(std::string const &name) const
Definition Client.cpp:80
DerivedT & enable(InteractionDescription const &interactionDescription)
Definition ElementOps.h:309
DerivedT & colorGlasbeyLUT(std::size_t id, int alpha=255)
Definition ElementOps.h:233
DerivedT & color(Color color)
Definition ElementOps.h:218
DerivedT & position(float x, float y, float z)
Definition ElementOps.h:136
#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 ARMARX_VERBOSE
The logging level for verbose information.
Definition Logging.h:185
std::vector< Eigen::Vector3f > to3D(const std::vector< Eigen::Vector2f > &v)
Definition eigen.cpp:11
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
const char * toString(InteractionFeedbackType type)
Definition Interaction.h:28
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
constexpr auto n() noexcept
double angle(const Point &a, const Point &b, const Point &c)
Definition point.hpp:109
std::vector< Eigen::Vector2f > polygon
Definition Room.h:37
A room together with the provider segment (i.e. the navigation graph) it belongs to.
Definition RoomEditor.h:45
std::string layerPrefix
Prefix of the ArViz layers created by the editor.
Definition RoomEditor.h:68
InteractionFeedbackRange interactions() const
Definition Client.h:85
Cylinder & height(float h)
Definition Elements.h:84
Self & rotation(AxesFlags const &axes=AXES_XYZ)
Definition ElementOps.h:73
data::InteractionDescription data_
Definition ElementOps.h:105
void add(ElementT const &element)
Definition Layer.h:31
Sphere & radius(float r)
Definition Elements.h:138
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