/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later */

/** \file
 * \ingroup edtransform
 */

#include <math.h>

#include "MEM_guardedalloc.h"

#include "DNA_gpencil_legacy_types.h"

#include "BLI_blenlib.h"
#include "BLI_math.h"
#include "BLI_rand.h"

#include "PIL_time.h"

#include "BLT_translation.h"

#include "RNA_access.h"

#include "GPU_immediate.h"
#include "GPU_matrix.h"

#include "BKE_context.h"
#include "BKE_layer.h"
#include "BKE_mask.h"
#include "BKE_modifier.h"
#include "BKE_paint.h"

#include "SEQ_transform.h"

#include "ED_clip.h"
#include "ED_image.h"
#include "ED_object.h"
#include "ED_screen.h"
#include "ED_space_api.h"
#include "ED_uvedit.h"

#include "WM_api.h"
#include "WM_types.h"

#include "UI_resources.h"
#include "UI_view2d.h"

#include "SEQ_sequencer.h"

#include "transform.hh"
#include "transform_convert.hh"
#include "transform_gizmo.hh"
#include "transform_mode.hh"
#include "transform_orientations.hh"
#include "transform_snap.hh"

/* ************************** GENERICS **************************** */

void resetTransModal(TransInfo *t)
{
  freeTransCustomDataForMode(t);
}

void resetTransRestrictions(TransInfo *t)
{
  t->flag &= ~T_ALL_RESTRICTIONS;
}

static void *t_view_get(TransInfo *t)
{
  if (t->spacetype == SPACE_VIEW3D) {
    View3D *v3d = static_cast<View3D *>(t->area->spacedata.first);
    return (void *)v3d;
  }
  if (t->region) {
    return (void *)&t->region->v2d;
  }
  return nullptr;
}

static int t_around_get(TransInfo *t)
{
  if (t->flag & T_OVERRIDE_CENTER) {
    /* Avoid initialization of individual origins (#V3D_AROUND_LOCAL_ORIGINS). */
    return V3D_AROUND_CENTER_BOUNDS;
  }

  ScrArea *area = t->area;
  switch (t->spacetype) {
    case SPACE_VIEW3D: {
      if (t->mode == TFM_BEND) {
        /* Bend always uses the cursor. */
        return V3D_AROUND_CURSOR;
      }
      return t->settings->transform_pivot_point;
    }
    case SPACE_IMAGE: {
      SpaceImage *sima = static_cast<SpaceImage *>(area->spacedata.first);
      return sima->around;
    }
    case SPACE_GRAPH: {
      SpaceGraph *sipo = static_cast<SpaceGraph *>(area->spacedata.first);
      return sipo->around;
    }
    case SPACE_CLIP: {
      SpaceClip *sclip = static_cast<SpaceClip *>(area->spacedata.first);
      return sclip->around;
    }
    case SPACE_SEQ: {
      if (t->region->regiontype == RGN_TYPE_PREVIEW) {
        return SEQ_tool_settings_pivot_point_get(t->scene);
      }
      break;
    }
    default:
      break;
  }

  return V3D_AROUND_CENTER_BOUNDS;
}

void initTransInfo(bContext *C, TransInfo *t, wmOperator *op, const wmEvent *event)
{
  Scene *sce = CTX_data_scene(C);
  ViewLayer *view_layer = CTX_data_view_layer(C);
  BKE_view_layer_synced_ensure(sce, view_layer);
  Object *obact = BKE_view_layer_active_object_get(view_layer);
  const eObjectMode object_mode = eObjectMode(obact ? obact->mode : OB_MODE_OBJECT);
  ToolSettings *ts = CTX_data_tool_settings(C);
  ARegion *region = CTX_wm_region(C);
  ScrArea *area = CTX_wm_area(C);

  bGPdata *gpd = CTX_data_gpencil_data(C);
  PropertyRNA *prop;

  t->mbus = CTX_wm_message_bus(C);
  t->depsgraph = CTX_data_depsgraph_pointer(C);
  t->scene = sce;
  t->view_layer = view_layer;
  t->area = area;
  t->region = region;
  t->settings = ts;
  t->reports = op ? op->reports : nullptr;

  t->helpline = HLP_NONE;

  t->flag = eTFlag(0);

  if (obact && !(t->options & (CTX_CURSOR | CTX_TEXTURE_SPACE)) &&
      ELEM(object_mode, OB_MODE_EDIT, OB_MODE_EDIT_GPENCIL_LEGACY))
  {
    t->obedit_type = obact->type;
  }
  else {
    t->obedit_type = -1;
  }

  if (t->options & CTX_CURSOR) {
    /* Cursor should always use the drag start as the combination of click-drag to place & move
     * doesn't work well if the click location isn't used when transforming. */
    t->flag |= T_EVENT_DRAG_START;
  }

  /* Many kinds of transform only use a single handle. */
  if (t->data_container == nullptr) {
    t->data_container = static_cast<TransDataContainer *>(
        MEM_callocN(sizeof(*t->data_container), __func__));
    t->data_container_len = 1;
  }

  t->redraw = TREDRAW_HARD; /* redraw first time */

  int mval[2];
  if (event) {
    if (t->flag & T_EVENT_DRAG_START) {
      WM_event_drag_start_mval(event, region, mval);
    }
    else {
      copy_v2_v2_int(mval, event->mval);
    }
  }
  else {
    zero_v2_int(mval);
  }
  copy_v2_v2_int(t->mval, mval);
  copy_v2_v2_int(t->mouse.imval, mval);

  t->mode_info = nullptr;

  t->data_len_all = 0;

  zero_v3(t->center_global);

  unit_m3(t->mat);

  /* Default to rotate on the Z axis. */
  t->orient_axis = 2;
  t->orient_axis_ortho = 1;

  /* if there's an event, we're modal */
  if (event) {
    t->flag |= T_MODAL;
  }

  /* Crease needs edge flag */
  if (ELEM(t->mode, TFM_EDGE_CREASE, TFM_BWEIGHT)) {
    t->options |= CTX_EDGE_DATA;
  }

  t->remove_on_cancel = false;

  if (op && (prop = RNA_struct_find_property(op->ptr, "remove_on_cancel")) &&
      RNA_property_is_set(op->ptr, prop))
  {
    if (RNA_property_boolean_get(op->ptr, prop)) {
      t->remove_on_cancel = true;
    }
  }

  /* GPencil editing context */
  if (GPENCIL_EDIT_MODE(gpd)) {
    t->options |= CTX_GPENCIL_STROKES;
  }

  /* Assign the space type, some exceptions for running in different mode */
  if (area == nullptr) {
    /* background mode */
    t->spacetype = SPACE_EMPTY;
  }
  else if ((region == nullptr) && (area->spacetype == SPACE_VIEW3D)) {
    /* running in the text editor */
    t->spacetype = SPACE_EMPTY;
  }
  else {
    /* normal operation */
    t->spacetype = area->spacetype;
  }

  /* handle T_ALT_TRANSFORM initialization, we may use for different operators */
  if (op) {
    const char *prop_id = nullptr;
    if (t->mode == TFM_SHRINKFATTEN) {
      prop_id = "use_even_offset";
    }

    if (prop_id && (prop = RNA_struct_find_property(op->ptr, prop_id))) {
      SET_FLAG_FROM_TEST(t->flag, RNA_property_boolean_get(op->ptr, prop), T_ALT_TRANSFORM);
    }
  }

  if (t->spacetype == SPACE_VIEW3D) {
    bScreen *animscreen = ED_screen_animation_playing(CTX_wm_manager(C));

    t->animtimer = (animscreen) ? animscreen->animtimer : nullptr;

    if (t->scene->toolsettings->transform_flag & SCE_XFORM_AXIS_ALIGN) {
      t->flag |= T_V3D_ALIGN;
    }

    if ((object_mode & OB_MODE_ALL_PAINT) || (object_mode & OB_MODE_SCULPT_CURVES)) {
      Paint *p = BKE_paint_get_active_from_context(C);
      if (p && p->brush && (p->brush->flag & BRUSH_CURVE)) {
        t->options |= CTX_PAINT_CURVE;
      }
    }

    /* initialize UV transform from */
    if (op && (prop = RNA_struct_find_property(op->ptr, "correct_uv"))) {
      if (RNA_property_is_set(op->ptr, prop)) {
        if (RNA_property_boolean_get(op->ptr, prop)) {
          t->settings->uvcalc_flag |= UVCALC_TRANSFORM_CORRECT_SLIDE;
        }
        else {
          t->settings->uvcalc_flag &= ~UVCALC_TRANSFORM_CORRECT_SLIDE;
        }
      }
      else {
        RNA_property_boolean_set(
            op->ptr, prop, (t->settings->uvcalc_flag & UVCALC_TRANSFORM_CORRECT_SLIDE) != 0);
      }
    }
  }
  else if (t->spacetype == SPACE_IMAGE) {
    SpaceImage *sima = static_cast<SpaceImage *>(area->spacedata.first);
    BKE_view_layer_synced_ensure(t->scene, t->view_layer);
    if (ED_space_image_show_uvedit(sima, BKE_view_layer_active_object_get(t->view_layer))) {
      /* UV transform */
    }
    else if (sima->mode == SI_MODE_MASK) {
      t->options |= CTX_MASK;
    }
    else if (sima->mode == SI_MODE_PAINT) {
      Paint *p = &sce->toolsettings->imapaint.paint;
      if (p->brush && (p->brush->flag & BRUSH_CURVE)) {
        t->options |= CTX_PAINT_CURVE;
      }
    }
    /* image not in uv edit, nor in mask mode, can happen for some tools */
  }
  else if (t->spacetype == SPACE_CLIP) {
    SpaceClip *sclip = static_cast<SpaceClip *>(area->spacedata.first);
    if (ED_space_clip_check_show_trackedit(sclip)) {
      t->options |= CTX_MOVIECLIP;
    }
    else if (ED_space_clip_check_show_maskedit(sclip)) {
      t->options |= CTX_MASK;
    }
  }
  else if (t->spacetype == SPACE_SEQ && region->regiontype == RGN_TYPE_PREVIEW) {
    t->options |= CTX_SEQUENCER_IMAGE;

    /* Needed for autokeying transforms in preview during playback. */
    bScreen *animscreen = ED_screen_animation_playing(CTX_wm_manager(C));
    t->animtimer = (animscreen) ? animscreen->animtimer : nullptr;
  }

  setTransformViewAspect(t, t->aspect);

  if (op && (prop = RNA_struct_find_property(op->ptr, "center_override")) &&
      RNA_property_is_set(op->ptr, prop))
  {
    RNA_property_float_get_array(op->ptr, prop, t->center_global);
    mul_v3_v3(t->center_global, t->aspect);
    t->flag |= T_OVERRIDE_CENTER;
  }

  t->view = t_view_get(t);
  t->around = t_around_get(t);

  /* Exceptional case. */
  if (t->around == V3D_AROUND_LOCAL_ORIGINS) {
    if (ELEM(t->mode, TFM_ROTATION, TFM_RESIZE, TFM_TRACKBALL)) {
      const bool use_island = transdata_check_local_islands(t, t->around);

      if ((t->obedit_type != -1) && !use_island) {
        t->options |= CTX_NO_PET;
      }
    }
  }

  bool t_values_set_is_array = false;

  if (op && (prop = RNA_struct_find_property(op->ptr, "value")) &&
      RNA_property_is_set(op->ptr, prop))
  {
    float values[4] = {0}; /* in case value isn't length 4, avoid uninitialized memory. */
    if (RNA_property_array_check(prop)) {
      RNA_property_float_get_array(op->ptr, prop, values);
      t_values_set_is_array = true;
    }
    else {
      values[0] = RNA_property_float_get(op->ptr, prop);
    }

    if (t->flag & T_MODAL) {
      /* Run before init functions so 'values_modal_offset' can be applied on mouse input. */
      copy_v4_v4(t->values_modal_offset, values);
    }
    else {
      copy_v4_v4(t->values, values);
      t->flag |= T_INPUT_IS_VALUES_FINAL;
    }
  }

  if (op && (prop = RNA_struct_find_property(op->ptr, "constraint_axis"))) {
    bool constraint_axis[3] = {false, false, false};
    if (t_values_set_is_array && t->flag & T_INPUT_IS_VALUES_FINAL) {
      /* For operators whose `t->values` is array (as Move and Scale), set constraint so that the
       * orientation is more intuitive in the Redo Panel. */
      constraint_axis[0] = constraint_axis[1] = constraint_axis[2] = true;
    }
    else if (RNA_property_is_set(op->ptr, prop)) {
      RNA_property_boolean_get_array(op->ptr, prop, constraint_axis);
    }

    if (constraint_axis[0] || constraint_axis[1] || constraint_axis[2]) {
      t->con.mode |= CON_APPLY;

      if (constraint_axis[0]) {
        t->con.mode |= CON_AXIS0;
      }
      if (constraint_axis[1]) {
        t->con.mode |= CON_AXIS1;
      }
      if (constraint_axis[2]) {
        t->con.mode |= CON_AXIS2;
      }
    }
  }

  {
    short orient_types[3];
    short orient_type_apply = O_DEFAULT;
    float custom_matrix[3][3];

    int orient_type_scene = V3D_ORIENT_GLOBAL;
    int orient_type_default = -1;
    int orient_type_set = -1;
    int orient_type_matrix_set = -1;

    if ((t->spacetype == SPACE_VIEW3D) && (t->region->regiontype == RGN_TYPE_WINDOW)) {
      TransformOrientationSlot *orient_slot = &t->scene->orientation_slots[SCE_ORIENT_DEFAULT];
      orient_type_scene = orient_slot->type;
      if (orient_type_scene == V3D_ORIENT_CUSTOM) {
        const int index_custom = orient_slot->index_custom;
        orient_type_scene += index_custom;
      }
    }

    if (op && ((prop = RNA_struct_find_property(op->ptr, "orient_type")) &&
               RNA_property_is_set(op->ptr, prop)))
    {
      orient_type_set = RNA_property_enum_get(op->ptr, prop);
      if (orient_type_set >= V3D_ORIENT_CUSTOM + BIF_countTransformOrientation(C)) {
        orient_type_set = V3D_ORIENT_GLOBAL;
      }
    }

    if (op && (prop = RNA_struct_find_property(op->ptr, "orient_axis"))) {
      t->orient_axis = RNA_property_enum_get(op->ptr, prop);
    }

    if (op && (prop = RNA_struct_find_property(op->ptr, "orient_axis_ortho"))) {
      t->orient_axis_ortho = RNA_property_enum_get(op->ptr, prop);
    }

    /* The properties "orient_matrix" and "orient_matrix_type" are used to store the orientation
     * calculated in the first operator call. This allows for reuse of the orientation during
     * subsequent calls of the same operator. When making adjustments through the Redo panel
     * (#OP_IS_REPEAT), reusing the orientation prevents unpredictable changes that can occur when
     * using #V3D_ORIENT_VIEW. However, when activated by #SCREEN_OT_repeat_last
     * (#OP_IS_REPEAT_LAST), it's best to avoid reusing the orientation to prevent unintended
     * changes. */
    if (op && !(op->flag & OP_IS_REPEAT_LAST) &&
        ((prop = RNA_struct_find_property(op->ptr, "orient_matrix")) &&
         RNA_property_is_set(op->ptr, prop)))
    {
      RNA_property_float_get_array(op->ptr, prop, &custom_matrix[0][0]);

      if ((prop = RNA_struct_find_property(op->ptr, "orient_matrix_type")) &&
          RNA_property_is_set(op->ptr, prop))
      {
        orient_type_matrix_set = RNA_property_enum_get(op->ptr, prop);
      }
      else if (orient_type_set == -1) {
        orient_type_set = V3D_ORIENT_CUSTOM_MATRIX;
      }
    }

    orient_type_default = orient_type_scene;

    if (orient_type_set != -1) {
      if (!(t->con.mode & CON_APPLY)) {
        /* Only overwrite default if not constrained. */
        orient_type_default = orient_type_set;
        t->is_orient_default_overwrite = true;
      }
    }
    else if (orient_type_matrix_set != -1) {
      orient_type_set = orient_type_matrix_set;
      if (!(t->con.mode & CON_APPLY)) {
        /* Only overwrite default if not constrained. */
        orient_type_default = orient_type_set;
        t->is_orient_default_overwrite = true;
      }
    }

    if (orient_type_set == -1) {
      if (orient_type_scene == V3D_ORIENT_GLOBAL) {
        orient_type_set = V3D_ORIENT_LOCAL;
      }
      else {
        orient_type_set = V3D_ORIENT_GLOBAL;
      }

      if (t->con.mode & CON_APPLY) {
        orient_type_apply = O_SCENE;
      }
    }
    else {
      if (t->con.mode & CON_APPLY) {
        orient_type_apply = O_SET;
      }
    }

    BLI_assert(!ELEM(-1, orient_type_default, orient_type_set));
    if (orient_type_matrix_set == orient_type_set) {
      /* Constraints are forced to use the custom matrix when redoing. */
      orient_type_set = V3D_ORIENT_CUSTOM_MATRIX;
    }

    orient_types[O_DEFAULT] = short(orient_type_default);
    orient_types[O_SCENE] = short(orient_type_scene);
    orient_types[O_SET] = short(orient_type_set);

    for (int i = 0; i < 3; i++) {
      /* For efficiency, avoid calculating the same orientation twice. */
      int j;
      for (j = 0; j < i; j++) {
        if (orient_types[j] == orient_types[i]) {
          memcpy(&t->orient[i], &t->orient[j], sizeof(*t->orient));
          break;
        }
      }
      if (j == i) {
        t->orient[i].type = transform_orientation_matrix_get(
            C, t, orient_types[i], custom_matrix, t->orient[i].matrix);
      }
    }

    t->orient_type_mask = 0;
    for (int i = 0; i < 3; i++) {
      const int type = t->orient[i].type;
      if (type < V3D_ORIENT_CUSTOM_MATRIX) {
        BLI_assert(type < 32);
        t->orient_type_mask |= (1 << type);
      }
    }

    transform_orientations_current_set(t, orient_type_apply);
  }

  if (op && ((prop = RNA_struct_find_property(op->ptr, "release_confirm")) &&
             RNA_property_is_set(op->ptr, prop)))
  {
    if (RNA_property_boolean_get(op->ptr, prop)) {
      t->flag |= T_RELEASE_CONFIRM;
    }
  }
  else {
    /* Release confirms preference should not affect node editor (#69288, #70504). */
    if (ISMOUSE_BUTTON(t->launch_event) &&
        ((U.flag & USER_RELEASECONFIRM) || (t->spacetype == SPACE_NODE)))
    {
      /* Global "release confirm" on mouse bindings */
      t->flag |= T_RELEASE_CONFIRM;
    }
  }

  if (op &&
      ((prop = RNA_struct_find_property(op->ptr, "mirror")) && RNA_property_is_set(op->ptr, prop)))
  {
    if (!RNA_property_boolean_get(op->ptr, prop)) {
      t->flag |= T_NO_MIRROR;
    }
  }
  else if ((t->spacetype == SPACE_VIEW3D) && (t->obedit_type == OB_MESH)) {
    /* pass */
  }
  else {
    /* Avoid mirroring for unsupported contexts. */
    t->flag |= T_NO_MIRROR;
  }

  /* Setting proportional editing flag only if property exist in operator. Otherwise, assume it's
   * not supported. */
  if (op && (prop = RNA_struct_find_property(op->ptr, "use_proportional_edit"))) {
    if (RNA_property_is_set(op->ptr, prop)) {
      if (RNA_property_boolean_get(op->ptr, prop)) {
        t->flag |= T_PROP_EDIT;
        if (RNA_boolean_get(op->ptr, "use_proportional_connected")) {
          t->flag |= T_PROP_CONNECTED;
        }
        if (RNA_boolean_get(op->ptr, "use_proportional_projected")) {
          t->flag |= T_PROP_PROJECTED;
        }
      }
    }
    else {
      /* use settings from scene only if modal */
      if (t->flag & T_MODAL) {
        if ((t->options & CTX_NO_PET) == 0) {
          bool use_prop_edit = false;
          if (t->spacetype == SPACE_GRAPH) {
            use_prop_edit = ts->proportional_fcurve;
          }
          else if (t->spacetype == SPACE_ACTION) {
            use_prop_edit = ts->proportional_action;
          }
          else if (t->options & CTX_MASK) {
            use_prop_edit = ts->proportional_mask;
          }
          else if (obact && obact->mode == OB_MODE_OBJECT) {
            use_prop_edit = ts->proportional_objects;
          }
          else {
            use_prop_edit = (ts->proportional_edit & PROP_EDIT_USE) != 0;
          }

          if (use_prop_edit) {
            t->flag |= T_PROP_EDIT;
            if (ts->proportional_edit & PROP_EDIT_CONNECTED) {
              t->flag |= T_PROP_CONNECTED;
            }
            if (ts->proportional_edit & PROP_EDIT_PROJECTED) {
              t->flag |= T_PROP_PROJECTED;
            }
          }
        }
      }
    }

    if (op && ((prop = RNA_struct_find_property(op->ptr, "proportional_size")) &&
               RNA_property_is_set(op->ptr, prop)))
    {
      t->prop_size = RNA_property_float_get(op->ptr, prop);
    }
    else {
      t->prop_size = ts->proportional_size;
    }

    /* TRANSFORM_FIX_ME rna restrictions */
    if (t->prop_size <= 0.00001f) {
      printf("Proportional size (%f) under 0.00001, resetting to 1!\n", t->prop_size);
      t->prop_size = 1.0f;
    }

    if (op && ((prop = RNA_struct_find_property(op->ptr, "proportional_edit_falloff")) &&
               RNA_property_is_set(op->ptr, prop)))
    {
      t->prop_mode = RNA_property_enum_get(op->ptr, prop);
    }
    else {
      t->prop_mode = ts->prop_mode;
    }
  }
  else { /* add not pet option to context when not available */
    t->options |= CTX_NO_PET;
  }

  if (op && (prop = RNA_struct_find_property(op->ptr, "use_automerge_and_split")) &&
      RNA_property_is_set(op->ptr, prop))
  {
    if (RNA_property_boolean_get(op->ptr, prop)) {
      t->flag |= T_AUTOMERGE | T_AUTOSPLIT;
    }
  }
  else if (t->obedit_type == OB_MESH) {
    char automerge = t->scene->toolsettings->automerge;
    if (automerge & AUTO_MERGE) {
      t->flag |= T_AUTOMERGE;
      if (automerge & AUTO_MERGE_AND_SPLIT) {
        t->flag |= T_AUTOSPLIT;
      }
    }
  }

/* Mirror is not supported with proportional editing, turn it off. */
#if 0
  if (t->flag & T_PROP_EDIT) {
    t->flag &= ~T_MIRROR;
  }
#endif

  /* Disable cursor wrap when edge panning is enabled. */
  if (t->options & CTX_VIEW2D_EDGE_PAN) {
    t->flag |= T_NO_CURSOR_WRAP;
  }

  if (op && (t->flag & T_MODAL) &&
      (prop = RNA_struct_find_property(op->ptr, "allow_navigation")) &&
      RNA_property_boolean_get(op->ptr, prop))
  {
    t->vod = ED_view3d_navigation_init(C);
  }

  setTransformViewMatrices(t);
  calculateCenter2D(t);
  calculateCenterLocal(t, t->center_global);
  initNumInput(&t->num);

  transform_gizmo_3d_model_from_constraint_and_mode_init(t);
}

static void freeTransCustomData(TransInfo *t, TransDataContainer *tc, TransCustomData *custom_data)
{
  if (custom_data->free_cb) {
    /* Can take over freeing t->data and data_2d etc... */
    custom_data->free_cb(t, tc, custom_data);
    BLI_assert(custom_data->data == nullptr);
  }
  else if ((custom_data->data != nullptr) && custom_data->use_free) {
    MEM_freeN(custom_data->data);
    custom_data->data = nullptr;
  }
  /* In case modes are switched in the same transform session. */
  custom_data->free_cb = nullptr;
  custom_data->use_free = false;
}

static void freeTransCustomDataContainer(TransInfo *t,
                                         TransDataContainer *tc,
                                         TransCustomDataContainer *tcdc)
{
  TransCustomData *custom_data = &tcdc->first_elem;
  for (int i = 0; i < TRANS_CUSTOM_DATA_ELEM_MAX; i++, custom_data++) {
    freeTransCustomData(t, tc, custom_data);
  }
}

void freeTransCustomDataForMode(TransInfo *t)
{
  freeTransCustomData(t, nullptr, &t->custom.mode);
  FOREACH_TRANS_DATA_CONTAINER (t, tc) {
    freeTransCustomData(t, tc, &tc->custom.mode);
  }
}

void postTrans(bContext *C, TransInfo *t)
{
  if (t->draw_handle_view) {
    ED_region_draw_cb_exit(t->region->type, t->draw_handle_view);
  }
  if (t->draw_handle_pixel) {
    ED_region_draw_cb_exit(t->region->type, t->draw_handle_pixel);
  }
  if (t->draw_handle_cursor) {
    WM_paint_cursor_end(static_cast<wmPaintCursor *>(t->draw_handle_cursor));
  }

  if (t->flag & T_MODAL_CURSOR_SET) {
    WM_cursor_modal_restore(CTX_wm_window(C));
  }

  /* Free all custom-data */
  freeTransCustomDataContainer(t, nullptr, &t->custom);
  FOREACH_TRANS_DATA_CONTAINER (t, tc) {
    freeTransCustomDataContainer(t, tc, &tc->custom);
  }

  /* postTrans can be called when nothing is selected, so data is nullptr already */
  if (t->data_len_all != 0) {
    FOREACH_TRANS_DATA_CONTAINER (t, tc) {
      /* free data malloced per trans-data */
      if (ELEM(t->obedit_type, OB_CURVES_LEGACY, OB_SURF, OB_GPENCIL_LEGACY) ||
          (t->spacetype == SPACE_GRAPH))
      {
        TransData *td = tc->data;
        for (int a = 0; a < tc->data_len; a++, td++) {
          if (td->flag & TD_BEZTRIPLE) {
            MEM_freeN(td->hdata);
          }
        }
      }
      MEM_freeN(tc->data);

      MEM_SAFE_FREE(tc->data_mirror);
      MEM_SAFE_FREE(tc->data_ext);
      MEM_SAFE_FREE(tc->data_2d);
    }
  }

  MEM_SAFE_FREE(t->data_container);
  t->data_container = nullptr;

  BLI_freelistN(&t->tsnap.points);

  if (t->spacetype == SPACE_IMAGE) {
    if (t->options & (CTX_MASK | CTX_PAINT_CURVE)) {
      /* pass */
    }
    else {
      SpaceImage *sima = static_cast<SpaceImage *>(t->area->spacedata.first);
      if (sima->flag & SI_LIVE_UNWRAP) {
        ED_uvedit_live_unwrap_end(t->state == TRANS_CANCEL);
      }
    }
  }

  if (t->mouse.data) {
    MEM_freeN(t->mouse.data);
  }

  if (t->rng != nullptr) {
    BLI_rng_free(t->rng);
  }

  freeSnapping(t);

  if (t->vod) {
    ED_view3d_navigation_free(C, t->vod);
  }
}

void applyTransObjects(TransInfo *t)
{
  TransDataContainer *tc = TRANS_DATA_CONTAINER_FIRST_SINGLE(t);

  TransData *td;

  for (td = tc->data; td < tc->data + tc->data_len; td++) {
    copy_v3_v3(td->iloc, td->loc);
    if (td->ext->rot) {
      copy_v3_v3(td->ext->irot, td->ext->rot);
    }
    if (td->ext->size) {
      copy_v3_v3(td->ext->isize, td->ext->size);
    }
  }
  recalcData(t);
}

static void transdata_restore_basic(TransDataBasic *td_basic)
{
  if (td_basic->val) {
    *td_basic->val = td_basic->ival;
  }

  /* TODO(mano-wii): Only use 3D or larger vectors in `td->loc`.
   * If `loc` and `val` point to the same address, it may indicate that `loc` is not 3D which is
   * not safe for `copy_v3_v3`. */
  if (td_basic->loc && td_basic->val != td_basic->loc) {
    copy_v3_v3(td_basic->loc, td_basic->iloc);
  }
}

static void restoreElement(TransData *td)
{
  transdata_restore_basic((TransDataBasic *)td);

  if (td->ext && (td->flag & TD_NO_EXT) == 0) {
    if (td->ext->rot) {
      copy_v3_v3(td->ext->rot, td->ext->irot);
    }
    if (td->ext->rotAngle) {
      *td->ext->rotAngle = td->ext->irotAngle;
    }
    if (td->ext->rotAxis) {
      copy_v3_v3(td->ext->rotAxis, td->ext->irotAxis);
    }
    /* XXX, drotAngle & drotAxis not used yet */
    if (td->ext->size) {
      copy_v3_v3(td->ext->size, td->ext->isize);
    }
    if (td->ext->quat) {
      copy_qt_qt(td->ext->quat, td->ext->iquat);
    }
  }

  if (td->flag & TD_BEZTRIPLE) {
    *(td->hdata->h1) = td->hdata->ih1;
    *(td->hdata->h2) = td->hdata->ih2;
  }
}

void restoreTransObjects(TransInfo *t)
{
  FOREACH_TRANS_DATA_CONTAINER (t, tc) {

    TransData *td;
    TransData2D *td2d;
    TransDataMirror *tdm;

    for (td = tc->data; td < tc->data + tc->data_len; td++) {
      restoreElement(td);
    }

    for (tdm = tc->data_mirror; tdm < tc->data_mirror + tc->data_mirror_len; tdm++) {
      transdata_restore_basic((TransDataBasic *)tdm);
    }

    for (td2d = tc->data_2d; tc->data_2d && td2d < tc->data_2d + tc->data_len; td2d++) {
      if (td2d->h1) {
        td2d->h1[0] = td2d->ih1[0];
        td2d->h1[1] = td2d->ih1[1];
      }
      if (td2d->h2) {
        td2d->h2[0] = td2d->ih2[0];
        td2d->h2[1] = td2d->ih2[1];
      }
    }

    unit_m3(t->mat);
  }

  recalcData(t);
}

void calculateCenter2D(TransInfo *t)
{
  BLI_assert(!is_zero_v3(t->aspect));
  projectFloatView(t, t->center_global, t->center2d);
}

void calculateCenterLocal(TransInfo *t, const float center_global[3])
{
  /* Setting constraint center. */
  /* NOTE: init functions may over-ride `t->center`. */
  FOREACH_TRANS_DATA_CONTAINER (t, tc) {
    if (tc->use_local_mat) {
      mul_v3_m4v3(tc->center_local, tc->imat, center_global);
    }
    else {
      copy_v3_v3(tc->center_local, center_global);
    }
  }
}

void calculateCenterCursor(TransInfo *t, float r_center[3])
{
  const float *cursor = t->scene->cursor.location;
  copy_v3_v3(r_center, cursor);

  /* If edit or pose mode, move cursor in local space */
  if (t->options & CTX_PAINT_CURVE) {
    if (ED_view3d_project_float_global(t->region, cursor, r_center, V3D_PROJ_TEST_NOP) !=
        V3D_PROJ_RET_OK)
    {
      r_center[0] = t->region->winx / 2.0f;
      r_center[1] = t->region->winy / 2.0f;
    }
    r_center[2] = 0.0f;
  }
}

void calculateCenterCursor2D(TransInfo *t, float r_center[2])
{
  float cursor_local_buf[2];
  const float *cursor = nullptr;

  if (t->spacetype == SPACE_IMAGE) {
    SpaceImage *sima = (SpaceImage *)t->area->spacedata.first;
    cursor = sima->cursor;
  }
  if (t->spacetype == SPACE_SEQ) {
    SpaceSeq *sseq = (SpaceSeq *)t->area->spacedata.first;
    SEQ_image_preview_unit_to_px(t->scene, sseq->cursor, cursor_local_buf);
    cursor = cursor_local_buf;
  }
  else if (t->spacetype == SPACE_CLIP) {
    SpaceClip *space_clip = (SpaceClip *)t->area->spacedata.first;
    cursor = space_clip->cursor;
  }

  if (cursor) {
    if (t->options & CTX_MASK) {
      float co[2];

      if (t->spacetype == SPACE_IMAGE) {
        SpaceImage *sima = (SpaceImage *)t->area->spacedata.first;
        BKE_mask_coord_from_image(sima->image, &sima->iuser, co, cursor);
      }
      else if (t->spacetype == SPACE_CLIP) {
        SpaceClip *space_clip = (SpaceClip *)t->area->spacedata.first;
        BKE_mask_coord_from_movieclip(space_clip->clip, &space_clip->user, co, cursor);
      }
      else {
        BLI_assert_msg(0, "Shall not happen");
      }

      r_center[0] = co[0] * t->aspect[0];
      r_center[1] = co[1] * t->aspect[1];
    }
    else if (t->options & CTX_PAINT_CURVE) {
      if (t->spacetype == SPACE_IMAGE) {
        r_center[0] = UI_view2d_view_to_region_x(&t->region->v2d, cursor[0]);
        r_center[1] = UI_view2d_view_to_region_y(&t->region->v2d, cursor[1]);
      }
    }
    else {
      r_center[0] = cursor[0] * t->aspect[0];
      r_center[1] = cursor[1] * t->aspect[1];
    }
  }
}

void calculateCenterCursorGraph2D(TransInfo *t, float r_center[2])
{
  SpaceGraph *sipo = (SpaceGraph *)t->area->spacedata.first;
  Scene *scene = t->scene;

  /* cursor is combination of current frame, and graph-editor cursor value */
  if (sipo->mode == SIPO_MODE_DRIVERS) {
    r_center[0] = sipo->cursorTime;
    r_center[1] = sipo->cursorVal;
  }
  else {
    r_center[0] = float(scene->r.cfra);
    r_center[1] = sipo->cursorVal;
  }
}

static bool transdata_center_global_get(const TransDataContainer *tc,
                                        const TransDataBasic *td_basic,
                                        float r_vec[3])
{
  if (td_basic->flag & TD_SELECTED) {
    if (!(td_basic->flag & TD_NOCENTER)) {
      if (tc->use_local_mat) {
        mul_v3_m4v3(r_vec, tc->mat, td_basic->center);
      }
      else {
        copy_v3_v3(r_vec, td_basic->center);
      }
      return true;
    }
  }
  return false;
}

void calculateCenterMedian(TransInfo *t, float r_center[3])
{
  float partial[3] = {0.0f, 0.0f, 0.0f};
  int total = 0;

  FOREACH_TRANS_DATA_CONTAINER (t, tc) {
    float center[3];
    for (int i = 0; i < tc->data_len; i++) {
      if (transdata_center_global_get(tc, (TransDataBasic *)&tc->data[i], center)) {
        add_v3_v3(partial, center);
        total++;
      }
    }
    for (int i = 0; i < tc->data_mirror_len; i++) {
      if (transdata_center_global_get(tc, (TransDataBasic *)&tc->data_mirror[i], center)) {
        add_v3_v3(partial, center);
        total++;
      }
    }
  }
  if (total) {
    mul_v3_fl(partial, 1.0f / float(total));
  }
  copy_v3_v3(r_center, partial);
}

void calculateCenterBound(TransInfo *t, float r_center[3])
{
  float max[3], min[3];
  bool changed = false;
  INIT_MINMAX(min, max);
  FOREACH_TRANS_DATA_CONTAINER (t, tc) {
    float center[3];
    for (int i = 0; i < tc->data_len; i++) {
      if (transdata_center_global_get(tc, (TransDataBasic *)&tc->data[i], center)) {
        minmax_v3v3_v3(min, max, center);
        changed = true;
      }
    }
    for (int i = 0; i < tc->data_mirror_len; i++) {
      if (transdata_center_global_get(tc, (TransDataBasic *)&tc->data_mirror[i], center)) {
        minmax_v3v3_v3(min, max, center);
        changed = true;
      }
    }
  }
  if (changed) {
    mid_v3_v3v3(r_center, min, max);
  }
}

bool calculateCenterActive(TransInfo *t, bool select_only, float r_center[3])
{
  TransDataContainer *tc = TRANS_DATA_CONTAINER_FIRST_OK(t);

  if (t->spacetype != SPACE_VIEW3D) {
    return false;
  }
  if (tc->obedit) {
    if (ED_object_calc_active_center_for_editmode(tc->obedit, select_only, r_center)) {
      mul_m4_v3(tc->obedit->object_to_world, r_center);
      return true;
    }
  }
  else if (t->options & CTX_POSE_BONE) {
    BKE_view_layer_synced_ensure(t->scene, t->view_layer);
    Object *ob = BKE_view_layer_active_object_get(t->view_layer);
    if (ED_object_calc_active_center_for_posemode(ob, select_only, r_center)) {
      mul_m4_v3(ob->object_to_world, r_center);
      return true;
    }
  }
  else if (t->options & CTX_PAINT_CURVE) {
    Paint *p = BKE_paint_get_active(t->scene, t->view_layer);
    Brush *br = p->brush;
    PaintCurve *pc = br->paint_curve;
    copy_v3_v3(r_center, pc->points[pc->add_index - 1].bez.vec[1]);
    r_center[2] = 0.0f;
    return true;
  }
  else {
    /* object mode */
    BKE_view_layer_synced_ensure(t->scene, t->view_layer);
    Base *base = BKE_view_layer_active_base_get(t->view_layer);
    if (base && ((!select_only) || ((base->flag & BASE_SELECTED) != 0))) {
      copy_v3_v3(r_center, base->object->object_to_world[3]);
      return true;
    }
  }

  return false;
}

static void calculateCenter_FromAround(TransInfo *t, int around, float r_center[3])
{
  switch (around) {
    case V3D_AROUND_CENTER_BOUNDS:
      calculateCenterBound(t, r_center);
      break;
    case V3D_AROUND_CENTER_MEDIAN:
      calculateCenterMedian(t, r_center);
      break;
    case V3D_AROUND_CURSOR:
      if (ELEM(t->spacetype, SPACE_IMAGE, SPACE_SEQ, SPACE_CLIP)) {
        calculateCenterCursor2D(t, r_center);
      }
      else if (t->spacetype == SPACE_GRAPH) {
        calculateCenterCursorGraph2D(t, r_center);
      }
      else {
        calculateCenterCursor(t, r_center);
      }
      break;
    case V3D_AROUND_LOCAL_ORIGINS:
      /* Individual element center uses median center for helpline and such */
      calculateCenterMedian(t, r_center);
      break;
    case V3D_AROUND_ACTIVE: {
      if (calculateCenterActive(t, false, r_center)) {
        /* pass */
      }
      else {
        /* fallback */
        calculateCenterMedian(t, r_center);
      }
      break;
    }
  }
}

static void calculateZfac(TransInfo *t)
{
  /* ED_view3d_calc_zfac() defines a factor for perspective depth correction,
   * used in ED_view3d_win_to_delta() */

  /* zfac is only used convertViewVec only in cases operator was invoked in RGN_TYPE_WINDOW
   * and never used in other cases.
   *
   * We need special case here as well, since ED_view3d_calc_zfac will crash when called
   * for a region different from RGN_TYPE_WINDOW.
   */
  if ((t->spacetype == SPACE_VIEW3D) && (t->region->regiontype == RGN_TYPE_WINDOW)) {
    t->zfac = ED_view3d_calc_zfac(static_cast<const RegionView3D *>(t->region->regiondata),
                                  t->center_global);
  }
  else if (t->spacetype == SPACE_IMAGE) {
    SpaceImage *sima = static_cast<SpaceImage *>(t->area->spacedata.first);
    t->zfac = 1.0f / sima->zoom;
  }
  else if (t->region) {
    View2D *v2d = &t->region->v2d;
    /* Get zoom fac the same way as in
     * `ui_view2d_curRect_validate_resize` - better keep in sync! */
    const float zoomx = float(BLI_rcti_size_x(&v2d->mask) + 1) / BLI_rctf_size_x(&v2d->cur);
    t->zfac = 1.0f / zoomx;
  }
}

void calculateCenter(TransInfo *t)
{
  if ((t->flag & T_OVERRIDE_CENTER) == 0) {
    calculateCenter_FromAround(t, t->around, t->center_global);
  }
  calculateCenterLocal(t, t->center_global);

  calculateCenter2D(t);

  /* For panning from the camera-view. */
  if ((t->options & CTX_OBJECT) && (t->flag & T_OVERRIDE_CENTER) == 0) {
    if (t->spacetype == SPACE_VIEW3D && t->region && t->region->regiontype == RGN_TYPE_WINDOW) {

      if (t->options & CTX_CAMERA) {
        float axis[3];
        /* persinv is nasty, use viewinv instead, always right */
        copy_v3_v3(axis, t->viewinv[2]);
        normalize_v3(axis);

        /* 6.0 = 6 grid units */
        axis[0] = t->center_global[0] - 6.0f * axis[0];
        axis[1] = t->center_global[1] - 6.0f * axis[1];
        axis[2] = t->center_global[2] - 6.0f * axis[2];

        projectFloatView(t, axis, t->center2d);

        /* Rotate only needs correct 2d center, grab needs #ED_view3d_calc_zfac() value. */
        if (t->mode == TFM_TRANSLATION) {
          copy_v3_v3(t->center_global, axis);
        }
      }
    }
  }

  calculateZfac(t);
}

void tranformViewUpdate(TransInfo *t)
{
  float zoom_prev = t->zfac;
  float zoom_new;
  if ((t->spacetype == SPACE_VIEW3D) && (t->region->regiontype == RGN_TYPE_WINDOW)) {
    if (!t->persp) {
      zoom_prev *= len_v3(t->persinv[0]);
    }

    setTransformViewMatrices(t);
    calculateZfac(t);

    zoom_new = t->zfac;
    if (!t->persp) {
      zoom_new *= len_v3(t->persinv[0]);
    }

    for (int i = 0; i < ARRAY_SIZE(t->orient); i++) {
      if (t->orient[i].type == V3D_ORIENT_VIEW) {
        copy_m3_m4(t->orient[i].matrix, t->viewinv);
        normalize_m3(t->orient[i].matrix);
        if (t->orient_curr == i) {
          copy_m3_m3(t->spacemtx, t->orient[i].matrix);
          invert_m3_m3_safe_ortho(t->spacemtx_inv, t->spacemtx);
        }
      }
    }
  }
  else {
    calculateZfac(t);
    zoom_new = t->zfac;
  }

  calculateCenter2D(t);
  transform_input_update(t, zoom_prev / zoom_new);
}

void calculatePropRatio(TransInfo *t)
{
  int i;
  float dist;
  const bool connected = (t->flag & T_PROP_CONNECTED) != 0;

  t->proptext[0] = '\0';

  if (t->flag & T_PROP_EDIT) {
    const char *pet_id = nullptr;
    FOREACH_TRANS_DATA_CONTAINER (t, tc) {
      TransData *td = tc->data;
      for (i = 0; i < tc->data_len; i++, td++) {
        if (td->flag & TD_SELECTED) {
          td->factor = 1.0f;
        }
        else if ((connected && (td->flag & TD_NOTCONNECTED || td->dist > t->prop_size)) ||
                 (connected == 0 && td->rdist > t->prop_size))
        {
          td->factor = 0.0f;
          restoreElement(td);
        }
        else {
          /* Use rdist for falloff calculations, it is the real distance */
          if (connected) {
            dist = (t->prop_size - td->dist) / t->prop_size;
          }
          else {
            dist = (t->prop_size - td->rdist) / t->prop_size;
          }

          /*
           * Clamp to positive numbers.
           * Certain corner cases with connectivity and individual centers
           * can give values of rdist larger than propsize.
           */
          if (dist < 0.0f) {
            dist = 0.0f;
          }

          switch (t->prop_mode) {
            case PROP_SHARP:
              td->factor = dist * dist;
              break;
            case PROP_SMOOTH:
              td->factor = 3.0f * dist * dist - 2.0f * dist * dist * dist;
              break;
            case PROP_ROOT:
              td->factor = sqrtf(dist);
              break;
            case PROP_LIN:
              td->factor = dist;
              break;
            case PROP_CONST:
              td->factor = 1.0f;
              break;
            case PROP_SPHERE:
              td->factor = sqrtf(2 * dist - dist * dist);
              break;
            case PROP_RANDOM:
              if (t->rng == nullptr) {
                /* Lazy initialization. */
                uint rng_seed = uint(PIL_check_seconds_timer_i() & UINT_MAX);
                t->rng = BLI_rng_new(rng_seed);
              }
              td->factor = BLI_rng_get_float(t->rng) * dist;
              break;
            case PROP_INVSQUARE:
              td->factor = dist * (2.0f - dist);
              break;
            default:
              td->factor = 1;
              break;
          }
        }
      }
    }

    switch (t->prop_mode) {
      case PROP_SHARP:
        pet_id = N_("(Sharp)");
        break;
      case PROP_SMOOTH:
        pet_id = N_("(Smooth)");
        break;
      case PROP_ROOT:
        pet_id = N_("(Root)");
        break;
      case PROP_LIN:
        pet_id = N_("(Linear)");
        break;
      case PROP_CONST:
        pet_id = N_("(Constant)");
        break;
      case PROP_SPHERE:
        pet_id = N_("(Sphere)");
        break;
      case PROP_RANDOM:
        pet_id = N_("(Random)");
        break;
      case PROP_INVSQUARE:
        pet_id = N_("(InvSquare)");
        break;
      default:
        break;
    }

    if (pet_id) {
      STRNCPY_UTF8(t->proptext, IFACE_(pet_id));
    }
  }
  else {
    FOREACH_TRANS_DATA_CONTAINER (t, tc) {
      TransData *td = tc->data;
      for (i = 0; i < tc->data_len; i++, td++) {
        td->factor = 1.0;
      }
    }
  }
}

void transform_data_ext_rotate(TransData *td, float mat[3][3], bool use_drot)
{
  float totmat[3][3];
  float smat[3][3];
  float fmat[3][3];
  float obmat[3][3];

  float dmat[3][3]; /* delta rotation */
  float dmat_inv[3][3];

  mul_m3_m3m3(totmat, mat, td->mtx);
  mul_m3_m3m3(smat, td->smtx, mat);

  /* logic from BKE_object_rot_to_mat3 */
  if (use_drot) {
    if (td->ext->rotOrder > 0) {
      eulO_to_mat3(dmat, td->ext->drot, td->ext->rotOrder);
    }
    else if (td->ext->rotOrder == ROT_MODE_AXISANGLE) {
#if 0
      axis_angle_to_mat3(dmat, td->ext->drotAxis, td->ext->drotAngle);
#else
      unit_m3(dmat);
#endif
    }
    else {
      float tquat[4];
      normalize_qt_qt(tquat, td->ext->dquat);
      quat_to_mat3(dmat, tquat);
    }

    invert_m3_m3(dmat_inv, dmat);
  }

  if (td->ext->rotOrder == ROT_MODE_QUAT) {
    float quat[4];

    /* Calculate the total rotation. */
    quat_to_mat3(obmat, td->ext->iquat);
    if (use_drot) {
      mul_m3_m3m3(obmat, dmat, obmat);
    }

    /* mat = transform, obmat = object rotation */
    mul_m3_m3m3(fmat, smat, obmat);

    if (use_drot) {
      mul_m3_m3m3(fmat, dmat_inv, fmat);
    }

    mat3_to_quat(quat, fmat);

    /* apply */
    copy_qt_qt(td->ext->quat, quat);
  }
  else if (td->ext->rotOrder == ROT_MODE_AXISANGLE) {
    float axis[3], angle;

    /* Calculate the total rotation. */
    axis_angle_to_mat3(obmat, td->ext->irotAxis, td->ext->irotAngle);
    if (use_drot) {
      mul_m3_m3m3(obmat, dmat, obmat);
    }

    /* mat = transform, obmat = object rotation */
    mul_m3_m3m3(fmat, smat, obmat);

    if (use_drot) {
      mul_m3_m3m3(fmat, dmat_inv, fmat);
    }

    mat3_to_axis_angle(axis, &angle, fmat);

    /* apply */
    copy_v3_v3(td->ext->rotAxis, axis);
    *td->ext->rotAngle = angle;
  }
  else {
    float eul[3];

    /* Calculate the total rotation. */
    eulO_to_mat3(obmat, td->ext->irot, td->ext->rotOrder);
    if (use_drot) {
      mul_m3_m3m3(obmat, dmat, obmat);
    }

    /* mat = transform, obmat = object rotation */
    mul_m3_m3m3(fmat, smat, obmat);

    if (use_drot) {
      mul_m3_m3m3(fmat, dmat_inv, fmat);
    }

    mat3_to_compatible_eulO(eul, td->ext->rot, td->ext->rotOrder, fmat);

    /* apply */
    copy_v3_v3(td->ext->rot, eul);
  }
}

Object *transform_object_deform_pose_armature_get(const TransInfo *t, Object *ob)
{
  if (!(ob->mode & OB_MODE_ALL_WEIGHT_PAINT)) {
    return nullptr;
  }
  /* Important that ob_armature can be set even when its not selected #23412.
   * Lines below just check is also visible. */
  Object *ob_armature = BKE_modifiers_is_deformed_by_armature(ob);
  if (ob_armature && ob_armature->mode & OB_MODE_POSE) {
    BKE_view_layer_synced_ensure(t->scene, t->view_layer);
    Base *base_arm = BKE_view_layer_base_find(t->view_layer, ob_armature);
    if (base_arm) {
      View3D *v3d = static_cast<View3D *>(t->view);
      if (BASE_VISIBLE(v3d, base_arm)) {
        return ob_armature;
      }
    }
  }
  return nullptr;
}
