Files
tsullivan13andClaude Opus 5.5 d9bac524db Park editor: saved object sets; duplicate selections; group local axes
Sets: save a selection under a name and place it again anywhere, on any map.

- The library gets an OBJECTS / SETS switch. SETS saves the selected
  objects as a set and lists the saved sets to place or delete (Delete asks
  for a second press).
- A set is a park file in <profile>/object_sets/<name>.park.json, so it
  shares the park format, its 2 MB limit and its name rules. Positions are
  relative to the set's anchor: the middle of its footprint, at its lowest
  origin.
- Placing follows the cursor as one wire cube per object, on surfaces or
  the grid plane; the mouse wheel turns it by the angle step. A click
  pastes the whole set: one undo step, under the paste path's object and
  session limits. Sets with objects missing from the catalog cannot be
  placed.
- A placed or pasted group is then selected with the move gizmo.

Editing groups:

- Local axes work for multi-selections. A placed set's axes are the
  rotation it was placed with, plus every later turn; any other group uses
  its last selected object's rotation. Applying a moved group does not snap
  its own rotation to the angle grid.
- Duplicate (button or Ctrl+D) copies every selected object at its saved
  position, rotation and scale, one grid step along X, as one paste.
- On a server with object scaling off, pastes (sets, duplicates, Ctrl+V)
  are made at 1x.

Tests cover set storage (anchor, round trip, replace, names, damaged files,
delete), saving and placing a set through the UI (surface, wheel, one
paste, no live preview), selecting it with its own axes, moving it along
them, and duplicating with and without scaling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:02:41 -07:00

66 lines
3.1 KiB
C++

#include "park_object_sets.h"
#include "park_document.h"
#include <algorithm>
#include <stdexcept>
namespace dingosdk::editor {
profile::ObjectLayout anchored_set(profile::ObjectLayout objects) {
if (objects.empty())
return objects;
std::array<float, 3> low = objects.front().position, high = low;
for (const auto &object : objects)
for (unsigned i = 0; i < 3; ++i) {
low[i] = std::min(low[i], object.position[i]);
high[i] = std::max(high[i], object.position[i]);
}
const std::array<float, 3> anchor{(low[0] + high[0]) * .5f, low[1], (low[2] + high[2]) * .5f};
std::uint64_t id = 1;
for (auto &object : objects) {
object.id = id++; // Ids are the set's own; placing gives every object a new one.
for (unsigned i = 0; i < 3; ++i)
object.position[i] -= anchor[i];
}
return objects;
}
void save_object_set(const std::filesystem::path &directory, std::string_view name, std::string_view map,
const profile::ObjectLayout &objects) {
if (!valid_park_name(name))
throw std::runtime_error("Use 1-64 letters, numbers, spaces, underscores or hyphens for the set name.");
if (objects.empty())
throw std::runtime_error("Select objects to save as a set.");
if (objects.size() > 1024)
throw std::runtime_error("A set holds at most 1024 objects.");
const auto existing = list_parks(directory);
if (existing.size() >= max_object_sets && std::find(existing.begin(), existing.end(), name) == existing.end())
throw std::runtime_error("You have " + std::to_string(max_object_sets) + " sets. Delete one first.");
const auto anchored = anchored_set(objects);
for (const auto &object : anchored)
if (!profile::valid_placed_object(object))
throw std::runtime_error("The selection has an object that cannot be saved.");
save_park(directory, name, {map.empty() ? std::string("unknown") : std::string(map), anchored});
}
void remove_object_set(const std::filesystem::path &directory, std::string_view name) {
if (!valid_park_name(name))
throw std::runtime_error("That set does not exist.");
std::error_code error;
if (!std::filesystem::remove(directory / (std::string(name) + ".park.json"), error) || error)
throw std::runtime_error("Could not delete the set " + std::string(name) + ".");
}
std::vector<EditorObjectSet> load_object_sets(const std::filesystem::path &directory) {
std::vector<EditorObjectSet> sets;
if (directory.empty())
return sets;
for (const auto &name : list_parks(directory)) {
try {
const auto park = load_park(directory, name);
EditorObjectSet set{name, park.map, {}};
for (const auto &object : park.objects)
set.objects.push_back({object.id, object.item, object.position, object.rotation, true, object.scale});
if (!set.objects.empty())
sets.push_back(std::move(set));
} catch (...) { /* A damaged or foreign file is not listed. */ }
}
return sets;
}
} // namespace dingosdk::editor