reckfellENGINE

3D-гизмо: ручка длины и Undo/Redo

Полный C#-плагин: локальная ось, перетаскивание, отмена и история.

На этой странице

Гизмо — вспомогательная геометрия редактора: линии, точки и ручки, связанные с выбранной Entity3D. Здесь одна ручка меняет Length вдоль локальной оси +X. Полный пример поддерживает отмену перетаскивания и Undo/Redo; он не создаёт игровую геометрию или физический коллайдер.

Нужен C#-редактор Reckfell 1.0.0 и собирающийся проект. Если ещё не включали плагины, начните с плагина редактора.

1. Создайте четыре файла

Все файлы положите в asset://addons/docs_length/. Имена файлов C# должны совпадать с классами.

plugin.cfg:

Конфигурация
[plugin]
name="Length Marker Gizmo"
description="Edit a local X-axis length with Undo/Redo."
author="Reckfell"
version="1.0.0"
script="LengthEditorPlugin.cs"

LengthMarker3D.cs хранит редактируемое значение. Подсказка [Export] задаёт интерфейс Inspector, а setter действительно ограничивает диапазон и отклоняет NaN/Infinity. Вызов UpdateGizmos() просит редактор обновить визуализацию после изменения свойства.

C#
using Reckfell;

[Tool, GlobalClass]
public partial class LengthMarker3D : Entity3D
{
    public const float MinimumLength = 0.1f;
    public const float MaximumLength = 100.0f;
    private float _length = 2.0f;

    [Export(PropertyHint.Range, "0.1,100,0.1")]
    public float Length
    {
        get => _length;
        set
        {
            if (!float.IsFinite(value)) return;
            _length = Mathf.Clamp(value, MinimumLength, MaximumLength);
            if (Engine.IsEditorHint() && IsInsideTree()) UpdateGizmos();
        }
    }
}

LengthEditorPlugin.cs регистрирует обработчик и передаёт ему настоящий менеджер Undo/Redo редактора. Для отмены действий здесь не создаётся отдельный UndoRedo: изменения должны попасть в историю редактируемой сцены.

C#
using Reckfell;

[Tool]
public partial class LengthEditorPlugin : EditorPlugin
{
    private LengthGizmoPlugin? _gizmoPlugin;
    public override void _EnterTree()
    {
        _gizmoPlugin = new LengthGizmoPlugin(GetUndoRedo());
        AddEntity3DGizmoPlugin(_gizmoPlugin);
    }
    public override void _ExitTree()
    {
        if (_gizmoPlugin is null) return;
        RemoveEntity3DGizmoPlugin(_gizmoPlugin);
        _gizmoPlugin.Dispose();
        _gizmoPlugin = null;
    }
}

LengthGizmoPlugin.cs рисует отрезок, добавляет одну ручку с ID 0 и обрабатывает изменение:

C#
using Reckfell;

[Tool]
public partial class LengthGizmoPlugin : EditorEntity3DGizmoPlugin
{
    private readonly EditorUndoRedoManager _undo;

    public LengthGizmoPlugin(EditorUndoRedoManager undo)
    {
        _undo = undo;
        CreateMaterial("line", new Color(0.25f, 0.45f, 1.0f));
        CreateHandleMaterial("handle");
    }

    public override string _GetGizmoName() => "LengthMarker3D";
    public override bool _HasGizmo(Entity3D forEntity3D) => forEntity3D is LengthMarker3D;

    public override void _Redraw(EditorEntity3DGizmo gizmo)
    {
        gizmo.Clear();
        if (gizmo.GetEntity3D() is not LengthMarker3D marker) return;
        Vector3 end = Vector3.Right * marker.Length;
        Vector3[] line = [Vector3.Zero, end];
        gizmo.AddLines(line, GetMaterial("line", gizmo));
        gizmo.AddCollisionSegments(line);
        gizmo.AddHandles([end], GetMaterial("handle", gizmo), [0]);
    }

    public override string _GetHandleName(EditorEntity3DGizmo gizmo, int handleId, bool secondary) =>
        handleId == 0 && !secondary ? "Length" : "";

    public override Variant _GetHandleValue(EditorEntity3DGizmo gizmo, int handleId, bool secondary) =>
        handleId == 0 && !secondary && gizmo.GetEntity3D() is LengthMarker3D marker ? marker.Length : default;

    public override void _SetHandle(EditorEntity3DGizmo gizmo, int handleId, bool secondary,
        Camera3D camera, Vector2 screenPos)
    {
        if (handleId != 0 || secondary || !screenPos.IsFinite() ||
            gizmo.GetEntity3D() is not LengthMarker3D marker) return;
        if (Mathf.Abs(marker.GlobalBasis.Determinant()) < 0.000001f) return;
        Vector3 origin = camera.ProjectRayOrigin(screenPos);
        Vector3 from = marker.ToLocal(origin);
        Vector3 to = marker.ToLocal(origin + camera.ProjectRayNormal(screenPos) * 10000);
        Vector3 direction = (to - from).Normalized();
        // Looking along the axis makes its length ambiguous; preserve the old value.
        if (Mathf.Abs(direction.Dot(Vector3.Right)) > 0.9999f) return;
        Vector3[] closest = Geometry3D.GetClosestPointsBetweenSegments(
            Vector3.Zero, Vector3.Right * LengthMarker3D.MaximumLength, from, to);
        marker.Length = closest[0].X;
    }

    public override void _CommitHandle(EditorEntity3DGizmo gizmo, int handleId, bool secondary,
        Variant restore, bool cancel)
    {
        if (handleId != 0 || secondary || gizmo.GetEntity3D() is not LengthMarker3D marker) return;
        float previous = restore.AsSingle();
        if (cancel) { marker.Length = previous; return; }
        if (Mathf.IsEqualApprox(marker.Length, previous)) return;
        _undo.CreateAction("Change marker length", UndoRedo.MergeMode.Disable, marker);
        _undo.AddDoProperty(marker, LengthMarker3D.PropertyName.Length, marker.Length);
        _undo.AddUndoProperty(marker, LengthMarker3D.PropertyName.Length, previous);
        _undo.CommitAction();
    }
}

Ссылка на менеджер Undo/Redo заимствована у редактора; не вызывайте для неё Dispose(). Сам обработчик гизмо снимается с регистрации и освобождается владельцем при выключении плагина.

2. Проверьте в редакторе

  1. Соберите C#-проект. В Project → Project Settings → Plugins включите Length Marker Gizmo.
  2. Создайте сцену с корнем Entity3D, добавьте дочернюю LengthMarker3D через создание Entity либо прикрепите LengthMarker3D.cs к дочерней Entity3D. Сохраните сцену как .rscene.
  3. Если сцена уже была открыта до регистрации плагина, сохраните и переоткройте её. В 1.0.0 само добавление обработчика не гарантирует пересоздание гизмо всех уже открытых Entity.
  4. Откройте рабочую область 3D и выберите маркер. Включите гизмо в меню View → Gizmos, если они скрыты. Должен появиться отрезок вдоль локальной +X с ручкой на конце.
  5. Перетащите ручку: Length в Inspector меняется в пределах 0.1…100. Отпустите кнопку, затем выполните Undo и Redo — должны восстановиться начальная и конечная длина.
  6. Начните другое перетаскивание и нажмите Esc либо правую кнопку мыши, пока ручка активна. Начальное значение восстанавливается без новой записи в истории.
  7. Измените Length через Inspector. Setter запросит перерисовку гизмо; исходник сцены меняется только после сохранения.

Код не подключает шаг сетки или клавиши snapping редактора. Не ожидайте привязку к сетке: ограничивается только диапазон. При взгляде почти строго вдоль редактируемой оси длина неоднозначна, поэтому обработчик оставляет прежнее значение; поверните камеру.

Координаты и масштаб

AddLines() и AddHandles() получают локальные координаты маркера. Точки (0,0,0) и (Length,0,0) задают отрезок; Transform Entity переводит его в мир. Поэтому Length = 2 при масштабе X = 3 даёт мировую длину 6, а не 2.

В _SetHandle() экранная позиция преобразуется камерой в луч. Оба конца луча переводятся через ToLocal(), после чего ищутся ближайшие точки луча и локальной оси. Это сохраняет правильное значение при перемещении, повороте и обычном ненулевом масштабе Entity. Нельзя напрямую записывать мировую координату X в локальное свойство.

При почти вырожденном базисе изменение отвергается. Это не универсальный инструмент для нулевого масштаба или любой величины мира: пример использует луч длиной 10 000 мировых единиц и диапазон длины до 100 локальных единиц. Для других масштабов проекта выберите подходящие пределы и проверьте точность отдельно.

Жизненный цикл ручки

МетодНазначение
_HasGizmo()Применяет обработчик только к LengthMarker3D. Обычная Entity3D его не получает.
_Redraw()Сначала очищает прежнюю геометрию, затем добавляет линии, область выбора и ручку. Без Clear() повторное обновление накапливало бы геометрию.
_GetHandleValue()Возвращает значение до перетаскивания; редактор передаёт его в restore при завершении.
_SetHandle()Меняет текущее значение во время движения; не создаёт отдельное действие на каждый кадр.
_CommitHandle()При отмене сразу восстанавливает restore; при подтверждении записывает одну пару Do/Undo. Если длина не изменилась, история не пополняется.

AddCollisionSegments() задаёт геометрию выбора гизмо в редакторе. Она не добавляет столкновения в физический мир. AddHandles() задаёт интерактивные точки; его вызов имеет смысл для выбранного и доступного для редактирования гизмо. ID ручки должен совпадать с ID, который обрабатывают callback-методы.

GetMaterial("line", gizmo) выбирает вариант материала с учётом выбранности и редактируемости объекта. Не создавайте новый материал при каждом _Redraw() и не освобождайте используемый материал внутри этого метода.

Когда нужен отдельный класс гизмо

Для одной длины достаточно EditorEntity3DGizmoPlugin. Если нужно отдельное состояние на каждый объект или собственная логика subgizmo, можно переопределить _CreateGizmo() и вернуть наследника EditorEntity3DGizmo. Возвращайте null для неподходящих Entity. В таком варианте redraw/handle-методы можно реализовать у самого экземпляра; пример выше намеренно использует один подход целиком, без смешивания двух наборов callback.

Диагностика

ПроблемаЧто проверить
Нет отрезкаC#-сборка, включённый плагин, скрипт LengthMarker3D на Entity, открытая редактируемая сцена, меню гизмо. После включения сохраните и переоткройте сцену.
Есть линия, нет ручкиВыбран ли сам маркер? Доступен ли объект для редактирования? AddHandles() не добавляет ручки невыбранному или нередактируемому гизмо.
Ручка не меняет значениеСверьте ID 0, флаг secondary, угол камеры и ненулевой масштаб.
Длина прыгает после поворотаНе смешивайте мировой луч с локальной осью; переводите обе точки луча в одну систему координат.
Undo возвращает не тудаСохраняйте restore, полученный при начале перетаскивания, а не значение последнего кадра. Используйте GetUndoRedo() редактора.
Сцена заполнена копиями гизмоНе регистрируйте один обработчик несколько раз; при выключении снимайте тот же экземпляр. Начинайте redraw с Clear().
В игре нет отрезкаЭто ожидаемо: гизмо визуализирует данные в редакторе. Для игрового эффекта нужна отдельная игровая геометрия/логика.

Границы проверки

В установленном headless-редакторе 1.0.0 проверены регистрация и выбор гизмо, повторный redraw без ошибок API, перспективная и ортографическая проекция ручки, локальная ось повёрнутой/масштабированной Entity, диапазон и неверные входы, реальная история Undo/Redo, отмена без записи, выключение и повторное включение с переоткрытием сцены. События перетаскивания вызываются программно с координатами камеры: физический выбор ручки мышью, экранное изображение, snapping, subgizmo, вырожденные Transform, крайние масштабы мира и экспорт этим тестом не подтверждены.

Документация Reckfell EngineНаверх ↑
БЫСТРЫЙ ПЕРЕХОДНачало работыEntity и иерархияC# & .NET 10Сцены и префабы
Руководства и полный API Reckfell K
reckfellENGINE
RECKFELL ACCOUNT

Вход в аккаунт

Войдите в аккаунт Reckfell.

Один аккаунт для всей платформы Reckfell.