Files
dustinspace217andDustinspace217 499b3fb38b P4: qtFRED: rotate around the centre of a multi-selection (#7843)
* qtFRED: rotate around the centre of a multi-selection

Rotate Around Object (Alt+R) and the orbit pivot both used the current
object's position, so with several ships marked the camera still
circled whichever one happened to be current. Add
EditorViewport::selectionCenter(), which returns the centroid of the
marked objects when more than one is marked and the given object's
position otherwise, and use it at the three places that chose the
point: the look-at camera update, orbitCameraGetPivot(), and the Alt+R
handler that turns the camera toward the target when the mode is
switched on.

orbitCameraGetPivot() also serves middle-mouse orbit and wheel zoom in
normal mode, so those pivot on the selection centre as well. Every
marked type counts at full weight, including waypoints and jump nodes.
With a single object marked nothing changes.

Fixes #7724

* Make EditorViewport::selectionCenter const

It only reads the marked-object list and the editor's marked count;
clang-tidy (readability-make-member-function-const) flagged it in CI.

* Mirror the multi-selection orbit pivot to FRED, trim comments

FRED gets the same selection_center() as qtFRED's selectionCenter(),
used at the same three places: the look-at camera update, the orbit
pivot, and the Rotate Around Object toggle. Also removes the qtFRED
comments flagged in review.

* Make the selection-center pivot optional and share object bounds

Look-at and orbit use the current object again unless the new option is
on (qtFRED: Preferences > Controls; FRED: View menu), and the selection
center is now the bounding-box midpoint. view_universe in both editors,
the pivot, and qtFRED's selection radius all use get_object_bounds() in
missioneditor/common.

* Reset the cached orbit pivot when the selection-center option changes

Wheel zoom reuses the orbit pivot until the camera moves, so without this
the option only took effect for scrolling after the next camera move.

* Address review: move and rename the selection-centroid option

- FRED: the option moves from the View menu to Settings (and Misc in the
  classic layout), under Move Ships When Undocking, with hover help, and is
  renamed Rotate Around Selection Centroid.
- qtFRED: the checkbox moves next to Move Ships When Undocking in the
  preferences dialog, with the same name.
- setOrbitSelectionCenter only resets the orbit when the value changes.
- get_object_bounds iterates with list_range.
- The bounds test resets the object system with obj_init and takes objects
  off the free list, so it leaves a valid state for other tests.

* Rename the option to Selection Midpoint, move it to qtFRED's Controls tab, and document it

* Fix overlapping Appearance rows in qtFRED Preferences, realign a settings line

The last commit renumbered the Viewport label size row of the Appearance
group from 4 to 3, which is the Long list menu style row, so the label
size pair fell out of the layout and its spin box drew over the group
title. Put it back at row 4.

---------

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-04 01:17:00 -05:00

170 lines
8.5 KiB
C++

#pragma once
#include "globalincs/globals.h"
#include "mission/missionmessage.h"
#include "ship/anchor_t.h"
// Default AWACS range applied when the nebula intensity is unset or invalid
constexpr float DEFAULT_NEBULA_RANGE = 3000.0f;
// Smallest meaningful change in an orientation input field (degrees)
constexpr float ORIENT_INPUT_THRESHOLD = 0.01f;
// Normalize a degree value into the range [-180, 180]
float normalize_degrees(float deg);
// Voice acting manager
#define INVALID_MESSAGE ((MMessage*)SIZE_MAX) // was originally SIZE_T_MAX but that wasn't available outside fred. May need more research.
enum class PersonaSyncIndex : int {
Wingman = 0, // <Wingman Personas>
NonWingman = 1, // <Non-Wingman Personas>
PersonasStart = 2 // indices >= 2 map to specific persona
};
extern char Voice_abbrev_briefing[NAME_LENGTH];
extern char Voice_abbrev_campaign[NAME_LENGTH];
extern char Voice_abbrev_command_briefing[NAME_LENGTH];
extern char Voice_abbrev_debriefing[NAME_LENGTH];
extern char Voice_abbrev_message[NAME_LENGTH];
extern char Voice_abbrev_mission[NAME_LENGTH];
extern bool Voice_no_replace_filenames;
extern char Voice_script_entry_format[NOTES_LENGTH];
extern int Voice_export_selection;
extern bool Voice_group_messages;
extern SCP_string Voice_script_default_string;
extern SCP_string Voice_script_instructions_string;
void time_to_mission_info_string(const std::tm* src, char* dest, size_t dest_max_len);
void stuff_special_arrival_anchor_name(char* buf, int iff_index, int restrict_to_players, bool retail_format);
void stuff_special_arrival_anchor_name(char* buf, int anchor_num, bool retail_format);
int anchor_to_target(anchor_t anchor);
anchor_t target_to_anchor(int target);
// Rebuild Starting_wings[], Squadron_wings[], TVT_wings[] from their parallel
// name arrays via wing_name_lookup.
void update_custom_wing_indexes();
// The _team variants clear the map first; the _wing variants accumulate into it
// (so callers that build a total across several wings must clear the map themselves).
void generate_ship_usage_list_team(int team, SCP_map<int, int>& usage);
void generate_ship_usage_list_wing(int wing_num, SCP_map<int, int>& usage);
void generate_weaponry_usage_list_team(int team, SCP_map<int, int>& usage);
void generate_weaponry_usage_list_wing(int wing_num, SCP_map<int, int>& usage);
// If Player_start_shipnum no longer refers to a valid player start ship, repoint it to the
// first remaining player start in the mission (or -1 if there are none). Call this after
// changing a ship to or from an OBJ_START via demotion, deletion, etc.
void ensure_valid_player_start_shipnum();
// Make the given object the sole player start, per single-player semantics: promote it to
// OBJ_START, demote every other player start to OBJ_SHIP, adjust Player_starts to match,
// and repoint Player_start_shipnum. Returns true if anything changed.
bool set_single_player_start(int objnum);
// Check whether the given name is empty, starts with '<', or conflicts with an existing ship,
// wing, waypoint path, jump node, or target priority group.
// Returns an empty string if the name is valid, otherwise a self-contained reason string
// (e.g. "The name is already being used by a wing"). The exclude parameters prevent matching
// against the entity currently being renamed.
SCP_string check_name_conflict(const char *entity_type, const char *name, int exclude_ship = -1, int exclude_wing = -1, int exclude_waypoint_list = -1, int exclude_jump_node = -1);
// Free the engine data that the editors keep for their whole session. Call at shutdown, while the graphics
// system is still up, since models own GPU buffers and cursors own bitmaps.
void editor_free_engine_data();
// Bounding-box midpoint of all (or only marked) objects and the farthest distance from it; false if there are none.
bool get_object_bounds(bool marked_only, vec3d* center, float* radius);
// Resolve a mission filename to its index in Campaign.missions[], lazy-loading the mission's goal/event name lists from disk if the
// FRED_LOAD_PENDING flag is still set. Returns -1 if the mission isn't in the campaign.
int load_and_find_campaign_mission(const char *mission_filename);
struct FredShipSlotConfig
{
char (*fred_alt_names)[NAME_LENGTH + 1] = nullptr;
char (*fred_callsigns)[NAME_LENGTH + 1] = nullptr;
int *cur_ship = nullptr;
};
// Move the ship currently in Ships[from] into Ships[to], updating every
// back-reference (Objects, Ai_info, Wings, Player_start_shipnum, Ship_registry,
// and editor-side fields supplied via cfg). Leaves Ships[from] empty.
// Preconditions: from != to, Ships[from].objnum >= 0, Ships[to].objnum < 0.
// No caller may hold a ship* to either slot across this call.
// Fields in cfg whose pointers are nullptr are skipped.
void reassign_ship_slot(int from, int to, const FredShipSlotConfig& cfg, bool resort_obj_list = true);
// Swap the contents of two slots. Both must be valid (Ships[a].objnum >= 0
// and Ships[b].objnum >= 0). Implemented as three calls to reassign_ship_slot
// via a temporary empty slot.
void swap_ship_slots(int a, int b, const FredShipSlotConfig& cfg);
// Move the item at position from_pos in slots to position to_pos, shifting the
// items in between by one position.
void rotate_ship_slots(const SCP_vector<int>& slots, int from_pos, int to_pos, const FredShipSlotConfig& cfg);
struct FredWingSlotConfig
{
int (*wing_objects)[MAX_SHIPS_PER_WING] = nullptr;
int *cur_wing = nullptr;
};
// Move the wing currently in Wings[from] into Wings[to], updating every
// back-reference (Ships[i].wingnum, Starting/Squadron/TVT_wings caches, and
// editor-side fields supplied via cfg). Leaves Wings[from] empty.
// Preconditions: from != to, Wings[from].wave_count > 0, Wings[to].wave_count == 0.
// No caller may hold a wing* to either slot across this call.
// Fields in cfg whose pointers are nullptr are skipped.
void reassign_wing_slot(int from, int to, const FredWingSlotConfig& cfg, bool update_wing_indexes = true);
// Swap the contents of two slots. Both must be valid (Wings[a].wave_count > 0
// and Wings[b].wave_count > 0). Implemented as three calls to
// reassign_wing_slot via a temporary empty slot.
void swap_wing_slots(int a, int b, const FredWingSlotConfig& cfg);
// Move the item at position from_pos in slots to position to_pos, shifting the
// items in between by one position.
void rotate_wing_slots(const SCP_vector<int>& slots, int from_pos, int to_pos, const FredWingSlotConfig& cfg);
// Restore the obj_used_list invariant for the OBJ_SHIP/OBJ_START subset:
// among ship-type entries, list order matches Ships[] index order.
void resort_ships_in_obj_used_list();
// Same, for the OBJ_PROP subset: among prop entries, obj_used_list order matches
// Props[] index order. UI lists that walk obj_used_list, like the Scene Browser,
// depend on this to show props in their reordered Props[] order.
void resort_props_in_obj_used_list();
// Props and waypoint paths live in SCP_vectors rather than the fixed
// Ships[]/Wings[] arrays, and their only index-based back-reference is the
// owning object's instance (everything else refers to them by name). So unlike
// the ship/wing slot helpers above, these reorder the elements directly and just
// re-point the OBJ_PROP / OBJ_WAYPOINT object instances.
// Move the prop at display position from_pos to to_pos within `slots` (the
// occupied Props[] indices, in display order), shifting the props in between by
// one and preserving their relative order. Props[] may contain empty nullopt
// holes; those stay put while the occupants are permuted among their slots.
// Also resorts obj_used_list so Scene Browser matches.
void rotate_prop_slots(const SCP_vector<int>& slots, int from_pos, int to_pos);
// Move the waypoint list at index from_pos to to_pos within Waypoint_lists,
// shifting the lists in between by one and preserving their relative order.
// Waypoint_lists is kept compact, so positions are plain indices.
void rotate_waypoint_lists(int from_pos, int to_pos);
// Move the jump node at display position from_pos to to_pos within `slots` (the
// live Jump_nodes[] indices, in display order), shifting the nodes in between by
// one and preserving their relative order. Unlike ships/props there is no
// object-instance back-reference to re-point: a jump node links to its object via
// CJumpNode::m_objnum, which travels with the node, and is looked up by
// objnum/name, and the Scene Browser and mission save both iterate Jump_nodes
// directly so permuting the occupants among their slots is all that is needed.
void rotate_jump_nodes(const SCP_vector<int>& slots, int from_pos, int to_pos);