VSE: Text editing in preview

This commit implements most features needed for simple text editing.

Active text strip can be edited in preview by pressing tab key, which
enabled text editing mode. With this mode active, outline matches text
boundary box and cursor is drawn.

Cursor can be moved with usual keys. Pressing shift starts selection.
Selection and navigation works when text is scaled or rotated. Mirrored
text is not supported in this PR. it can be done, but the text is
unreadable that way, so I kept it simple.

Multi line text is supported. Pressing return key starts new line.

Copy/paste operator uses OS copy paste buffer, so text from other apps
can be pasted.

Text is still limited to 512 characters. Text string property still
exists in side panel and is limited to single line. Individual
characters can not be styled in different way like in 3D viewport, but
the code is mostly ready for such feature.

Ref: #126547

Pull Request: https://projects.blender.org/blender/blender/pulls/127239
This commit is contained in:
Richard Antalik
2024-12-19 15:48:06 +01:00
parent 9d86fada03
commit 8ce5356522
25 changed files with 1386 additions and 80 deletions

View File

@@ -702,6 +702,8 @@ const bTheme U_theme_default = {
.row_alternate = RGBA(0xffffff05),
.anim_preview_range = RGBA(0xa14d0066),
.metadatatext = RGBA(0xffffffff),
.text_strip_cursor = RGBA(0x71a8ffff),
.selected_text = RGBA(0xffffff4d),
},
.space_image = {
.back = RGBA(0x30303000),

View File

@@ -836,6 +836,8 @@
metadatatext="#ffffff"
preview_range="#a14d0066"
row_alternate="#ffffff0d"
text_strip_cursor="#71a8ff"
selected_text= "#19191a4d"
>
<space>
<ThemeSpaceGeneric

View File

@@ -2907,6 +2907,7 @@ def km_text(params):
# ------------------------------------------------------------------------------
# Editor (Sequencer)
def km_sequencercommon(params):
items = []
keymap = (
@@ -3075,6 +3076,39 @@ def km_sequencer(params):
return keymap
def _seq_preview_text_edit_cursor_move():
items = []
map = [
('LEFT_ARROW', None, ("type", 'PREVIOUS_CHARACTER')),
('RIGHT_ARROW', None, ("type", 'NEXT_CHARACTER')),
('UP_ARROW', None, ("type", 'PREVIOUS_LINE')),
('DOWN_ARROW', None, ("type", 'NEXT_LINE')),
('HOME', None, ("type", 'LINE_BEGIN')),
('END', None, ("type", 'LINE_END')),
('LEFT_ARROW', 'ctrl', ("type", 'PREVIOUS_WORD')),
('RIGHT_ARROW', 'ctrl', ("type", 'NEXT_WORD')),
('PAGE_UP', None, ("type", 'TEXT_BEGIN')),
('PAGE_DOWN', None, ("type", 'TEXT_END')),
]
for type, mod, prop in map:
if mod:
items.append(
("sequencer.text_cursor_move", {"type": type, "value": 'PRESS', **{mod: True}, "repeat": True},
{"properties": [prop]}))
items.append(
("sequencer.text_cursor_move", {"type": type, "value": 'PRESS', **{mod: True}, "shift": True, "repeat": True},
{"properties": [prop, ('select_text', True)]}))
else:
items.append(
("sequencer.text_cursor_move", {"type": type, "value": 'PRESS', "repeat": True},
{"properties": [prop]}))
items.append(
("sequencer.text_cursor_move", {"type": type, "value": 'PRESS', "shift": True, "repeat": True},
{"properties": [prop, ('select_text', True)]}))
return items
def km_sequencerpreview(params):
items = []
keymap = (
@@ -3084,6 +3118,22 @@ def km_sequencerpreview(params):
)
items.extend([
# Text editing.
*_seq_preview_text_edit_cursor_move(),
("sequencer.text_delete", {"type": 'DEL', "value": 'PRESS', "repeat": True},
{"properties": [("type", 'NEXT_OR_SELECTION')]}),
("sequencer.text_delete", {"type": 'BACK_SPACE', "value": 'PRESS', "repeat": True},
{"properties": [("type", 'PREVIOUS_OR_SELECTION')]}),
("sequencer.text_line_break", {"type": 'RET', "value": 'PRESS', "repeat": True}, None),
("sequencer.text_line_break", {"type": 'NUMPAD_ENTER', "value": 'PRESS', "repeat": True}, None),
("sequencer.text_select_all", {"type": 'A', "value": 'PRESS', "ctrl": True}, None),
("sequencer.text_deselect_all", {"type": 'ESC', "value": 'PRESS'}, None),
("sequencer.text_edit_mode_toggle", {"type": 'TAB', "value": 'PRESS'}, None),
("sequencer.text_edit_copy", {"type": 'C', "value": 'PRESS', "ctrl": True}, None),
("sequencer.text_edit_paste", {"type": 'V', "value": 'PRESS', "ctrl": True}, None),
("sequencer.text_edit_cut", {"type": 'X', "value": 'PRESS', "ctrl": True}, None),
("sequencer.text_insert", {"type": 'TEXTINPUT', "value": 'ANY', "any": True, "repeat": True}, None),
# Selection.
*_template_sequencer_preview_select(
type=params.select_mouse,
@@ -8022,6 +8072,8 @@ def km_sequencer_editor_tool_generic_select_preview(params, *, fallback):
_fallback_id("Sequencer Preview Tool: Tweak", fallback),
{"space_type": 'SEQUENCE_EDITOR', "region_type": 'WINDOW'},
{"items": [
("sequencer.text_cursor_set", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None),
("sequencer.text_cursor_set", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, None),
*([] if (fallback and (params.select_mouse == 'RIGHTMOUSE')) else _template_items_tool_select(
params, "sequencer.select", "sequencer.cursor_set", cursor_prioritize=True, fallback=fallback)),
@@ -8037,6 +8089,8 @@ def km_sequencer_editor_tool_generic_select_box_preview(params, *, fallback):
_fallback_id("Sequencer Preview Tool: Select Box", fallback),
{"space_type": 'SEQUENCE_EDITOR', "region_type": 'WINDOW'},
{"items": [
("sequencer.text_cursor_set", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None),
("sequencer.text_cursor_set", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, None),
# Don't use `tool_maybe_tweak_event`, see comment for this slot.
*([] if (fallback and not params.use_fallback_tool) else _template_items_tool_select_actions_simple(
"sequencer.select_box",

View File

@@ -889,6 +889,26 @@ class SEQUENCER_MT_strip_transform(Menu):
layout.operator("sequencer.gap_insert")
class SEQUENCER_MT_strip_text(Menu):
bl_label = "Text"
def draw(self, context):
layout = self.layout
layout.operator_context = 'INVOKE_REGION_PREVIEW'
layout.operator("sequencer.text_edit_mode_toggle")
layout.separator()
layout.operator("sequencer.text_edit_copy", icon='COPYDOWN')
layout.operator("sequencer.text_edit_paste", icon='PASTEDOWN')
layout.operator("sequencer.text_edit_cut")
layout.separator()
props = layout.operator("sequencer.text_delete")
props.type = 'PREVIOUS_OR_SELECTION'
layout.operator("sequencer.text_line_break")
layout.separator()
layout.operator("sequencer.text_select_all")
layout.operator("sequencer.text_deselect_all")
class SEQUENCER_MT_strip_input(Menu):
bl_label = "Inputs"
@@ -993,16 +1013,21 @@ class SEQUENCER_MT_strip(Menu):
st = context.space_data
has_sequencer, has_preview = _space_view_types(st)
layout.menu("SEQUENCER_MT_strip_transform")
if has_preview:
layout.operator_context = 'INVOKE_REGION_PREVIEW'
else:
layout.operator_context = 'INVOKE_REGION_WIN'
layout.menu("SEQUENCER_MT_strip_transform")
strip = context.active_sequence_strip
if has_preview:
layout.separator()
layout.operator("sequencer.preview_duplicate_move", text="Duplicate")
layout.separator()
if strip and strip.type == 'TEXT':
layout.menu("SEQUENCER_MT_strip_text")
if has_sequencer:
layout.menu("SEQUENCER_MT_strip_retiming")
@@ -1024,8 +1049,6 @@ class SEQUENCER_MT_strip(Menu):
layout.separator()
layout.operator("sequencer.delete", text="Delete")
strip = context.active_sequence_strip
if strip and strip.type == 'SCENE':
layout.operator("sequencer.delete", text="Delete Strip & Data").delete_data = True
layout.operator("sequencer.scene_frame_range_update")
@@ -3039,6 +3062,7 @@ classes = (
SEQUENCER_MT_strip,
SEQUENCER_MT_strip_transform,
SEQUENCER_MT_strip_retiming,
SEQUENCER_MT_strip_text,
SEQUENCER_MT_strip_input,
SEQUENCER_MT_strip_lock_mute,
SEQUENCER_MT_image,

View File

@@ -31,7 +31,7 @@ extern "C" {
/* Blender file format version. */
#define BLENDER_FILE_VERSION BLENDER_VERSION
#define BLENDER_FILE_SUBVERSION 14
#define BLENDER_FILE_SUBVERSION 15
/* Minimum Blender version that supports reading file written with the current
* version. Older Blender versions will test this and cancel loading the file, showing a warning to

View File

@@ -1040,6 +1040,12 @@ static bool versioning_convert_strip_speed_factor(Sequence *seq, void *user_data
return true;
}
static bool versioning_clear_strip_unused_flag(Sequence *seq, void * /*user_data*/)
{
seq->flag &= ~(1 << 6);
return true;
}
static bool all_scenes_use(Main *bmain, const blender::Span<const char *> engines)
{
if (!bmain->scenes.first) {
@@ -5304,6 +5310,15 @@ void blo_do_versions_400(FileData *fd, Library * /*lib*/, Main *bmain)
}
}
if (!MAIN_VERSION_FILE_ATLEAST(bmain, 404, 15)) {
LISTBASE_FOREACH (Scene *, scene, &bmain->scenes) {
Editing *ed = SEQ_editing_get(scene);
if (ed != nullptr) {
SEQ_for_each_callback(&ed->seqbase, versioning_clear_strip_unused_flag, scene);
}
}
}
/* Always run this versioning; meshes are written with the legacy format which always needs to
* be converted to the new format on file load. Can be moved to a subversion check in a larger
* breaking release. */

View File

@@ -214,6 +214,10 @@ static void do_versions_theme(const UserDef *userdef, bTheme *btheme)
FROM_DEFAULT_V4_UCHAR(space_view3d.face_front);
}
if (!USER_VERSION_ATLEAST(404, 12)) {
FROM_DEFAULT_V4_UCHAR(space_sequencer.text_strip_cursor);
FROM_DEFAULT_V4_UCHAR(space_sequencer.selected_text);
}
/**
* Always bump subversion in BKE_blender_version.h when adding versioning
* code here, and wrap it inside a USER_VERSION_ATLEAST check.

View File

@@ -219,6 +219,8 @@ enum ThemeColorID {
TH_SEQ_COLOR,
TH_SEQ_ACTIVE,
TH_SEQ_SELECTED,
TH_SEQ_TEXT_CURSOR,
TH_SEQ_SELECTED_TEXT,
TH_EDGE_SHARP,
TH_EDITMESH_ACTIVE,

View File

@@ -721,6 +721,12 @@ const uchar *UI_ThemeGetColorPtr(bTheme *btheme, int spacetype, int colorid)
case TH_SEQ_SELECTED:
cp = ts->selected_strip;
break;
case TH_SEQ_TEXT_CURSOR:
cp = ts->text_strip_cursor;
break;
case TH_SEQ_SELECTED_TEXT:
cp = ts->selected_text;
break;
case TH_CONSOLE_OUTPUT:
cp = ts->console_output;

View File

@@ -41,6 +41,7 @@ set(SRC
sequencer_scopes.cc
sequencer_select.cc
sequencer_strips_batch.cc
sequencer_text_edit.cc
sequencer_thumbnails.cc
sequencer_timeline_draw.cc
sequencer_view.cc

View File

@@ -281,6 +281,7 @@ Sequence *find_nearest_seq(const Scene *scene,
const View2D *v2d,
const int mval[2],
eSeqHandle *r_hand);
bool seq_point_image_isect(const Scene *scene, const Sequence *seq, float point_view[2]);
/* `sequencer_add.cc` */
@@ -380,6 +381,22 @@ int right_fake_key_frame_get(const bContext *C, const Sequence *seq);
bool retiming_keys_can_be_displayed(const SpaceSeq *sseq);
rctf seq_retiming_keys_box_get(const Scene *scene, const View2D *v2d, const Sequence *seq);
/* `sequencer_text_edit.cc` */
bool sequencer_text_editing_active_poll(bContext *C);
void SEQUENCER_OT_text_cursor_move(wmOperatorType *ot);
void SEQUENCER_OT_text_insert(wmOperatorType *ot);
void SEQUENCER_OT_text_delete(wmOperatorType *ot);
void SEQUENCER_OT_text_line_break(wmOperatorType *ot);
void SEQUENCER_OT_text_select_all(wmOperatorType *ot);
void SEQUENCER_OT_text_deselect_all(wmOperatorType *ot);
void SEQUENCER_OT_text_edit_mode_toggle(wmOperatorType *ot);
void SEQUENCER_OT_text_cursor_set(wmOperatorType *ot);
void SEQUENCER_OT_text_edit_copy(wmOperatorType *ot);
void SEQUENCER_OT_text_edit_paste(wmOperatorType *ot);
void SEQUENCER_OT_text_edit_cut(wmOperatorType *ot);
blender::int2 seq_text_cursor_offset_to_position(const TextVarsRuntime *text, int cursor_offset);
blender::IndexRange seq_text_selection_range_get(const TextVars *data);
/* `sequencer_timeline_draw.cc` */
blender::Vector<Sequence *> sequencer_visible_strips_get(const bContext *C);
blender::Vector<Sequence *> sequencer_visible_strips_get(const Scene *scene, const View2D *v2d);

View File

@@ -80,6 +80,19 @@ void sequencer_operatortypes()
WM_operatortype_append(SEQUENCER_OT_retiming_segment_speed_set);
WM_operatortype_append(SEQUENCER_OT_retiming_key_delete);
/* `sequencer_text_edit.cc` */
WM_operatortype_append(SEQUENCER_OT_text_cursor_move);
WM_operatortype_append(SEQUENCER_OT_text_insert);
WM_operatortype_append(SEQUENCER_OT_text_delete);
WM_operatortype_append(SEQUENCER_OT_text_line_break);
WM_operatortype_append(SEQUENCER_OT_text_select_all);
WM_operatortype_append(SEQUENCER_OT_text_deselect_all);
WM_operatortype_append(SEQUENCER_OT_text_edit_mode_toggle);
WM_operatortype_append(SEQUENCER_OT_text_cursor_set);
WM_operatortype_append(SEQUENCER_OT_text_edit_copy);
WM_operatortype_append(SEQUENCER_OT_text_edit_paste);
WM_operatortype_append(SEQUENCER_OT_text_edit_cut);
/* `sequencer_select.cc` */
WM_operatortype_append(SEQUENCER_OT_select_all);
WM_operatortype_append(SEQUENCER_OT_select);

View File

@@ -6,15 +6,24 @@
* \ingroup spseq
*/
#include <algorithm>
#include <cmath>
#include <cstring>
#include "BLF_api.hh"
#include "BLI_blenlib.h"
#include "BLI_index_range.hh"
#include "BLI_math_matrix.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_math_rotation.h"
#include "BLI_math_vector_types.hh"
#include "BLI_rect.h"
#include "BLI_utildefines.h"
#include "BLI_vector.hh"
#include "DNA_view2d_types.h"
#include "GPU_primitive.hh"
#include "IMB_imbuf_types.hh"
#include "DNA_scene_types.h"
@@ -43,6 +52,7 @@
#include "BIF_glutil.hh"
#include "SEQ_channels.hh"
#include "SEQ_effects.hh"
#include "SEQ_iterator.hh"
#include "SEQ_prefetch.hh"
#include "SEQ_proxy.hh"
@@ -1070,6 +1080,187 @@ static void seq_draw_image_origin_and_outline(const bContext *C, Sequence *seq,
GPU_line_smooth(false);
}
static void text_selection_draw(const bContext *C, const Sequence *seq, uint pos)
{
const TextVars *data = static_cast<TextVars *>(seq->effectdata);
const TextVarsRuntime *text = data->runtime;
const Scene *scene = CTX_data_scene(C);
if (data->selection_start_offset == -1 || seq_text_selection_range_get(data).is_empty()) {
return;
}
const blender::IndexRange sel_range = seq_text_selection_range_get(data);
const blender::int2 selection_start = seq_text_cursor_offset_to_position(text,
sel_range.first());
const blender::int2 selection_end = seq_text_cursor_offset_to_position(text, sel_range.last());
const int line_start = selection_start.y;
const int line_end = selection_end.y;
for (int line_index = line_start; line_index <= line_end; line_index++) {
const blender::seq::LineInfo line = text->lines[line_index];
blender::seq::CharInfo character_start = line.characters.first();
blender::seq::CharInfo character_end = line.characters.last();
if (line_index == selection_start.y) {
character_start = line.characters[selection_start.x];
}
if (line_index == selection_end.y) {
character_end = line.characters[selection_end.x];
}
const float line_y = character_start.position.y + text->font_descender;
const blender::float3 view_offs{-scene->r.xsch / 2.0f, -scene->r.ysch / 2.0f, 0.0f};
const float view_aspect = scene->r.xasp / scene->r.yasp;
blender::float4x4 transform_mat;
SEQ_image_transform_matrix_get(scene, seq, transform_mat.ptr());
blender::float4x3 selection_quad{
{character_start.position.x, line_y, 0.0f},
{character_start.position.x, line_y + text->line_height, 0.0f},
{character_end.position.x + character_end.advance_x, line_y + text->line_height, 0.0f},
{character_end.position.x + character_end.advance_x, line_y, 0.0f},
};
immBegin(GPU_PRIM_TRIS, 6);
immUniformThemeColor(TH_SEQ_SELECTED_TEXT);
for (int i : blender::IndexRange(0, 4)) {
selection_quad[i] += view_offs;
selection_quad[i] = blender::math::transform_point(transform_mat, selection_quad[i]);
selection_quad[i].x *= view_aspect;
}
for (int i : blender::Vector<int>{0, 1, 2, 2, 3, 0}) {
immVertex2f(pos, selection_quad[i][0], selection_quad[i][1]);
}
immEnd();
}
}
static blender::float2 coords_region_view_align(const View2D *v2d, const blender::float2 coords)
{
blender::int2 coords_view;
UI_view2d_view_to_region(v2d, coords.x, coords.y, &coords_view.x, &coords_view.y);
coords_view.x = std::round(coords_view.x);
coords_view.y = std::round(coords_view.y);
blender::float2 coords_region_aligned;
UI_view2d_region_to_view(
v2d, coords_view.x, coords_view.y, &coords_region_aligned.x, &coords_region_aligned.y);
return coords_region_aligned;
}
static void text_edit_draw_cursor(const bContext *C, const Sequence *seq, uint pos)
{
const TextVars *data = static_cast<TextVars *>(seq->effectdata);
const TextVarsRuntime *text = data->runtime;
const Scene *scene = CTX_data_scene(C);
const blender::float3 view_offs{-scene->r.xsch / 2.0f, -scene->r.ysch / 2.0f, 0.0f};
const float view_aspect = scene->r.xasp / scene->r.yasp;
blender::float4x4 transform_mat;
SEQ_image_transform_matrix_get(scene, seq, transform_mat.ptr());
const blender::int2 cursor_position = seq_text_cursor_offset_to_position(text,
data->cursor_offset);
const float cursor_width = 10;
blender::float2 cursor_coords =
text->lines[cursor_position.y].characters[cursor_position.x].position;
/* Clamp cursor coords to be inside of text boundbox. Compensate for cursor width, but also line
* width hardcoded in shader. */
rcti text_boundbox = text->text_boundbox;
text_boundbox.xmax -= cursor_width + U.pixelsize;
text_boundbox.xmin += U.pixelsize;
cursor_coords.x = std::clamp(
cursor_coords.x, float(text_boundbox.xmin), float(text_boundbox.xmax));
cursor_coords = coords_region_view_align(UI_view2d_fromcontext(C), cursor_coords);
blender::float4x3 cursor_quad{
{cursor_coords.x, cursor_coords.y, 0.0f},
{cursor_coords.x, cursor_coords.y + text->line_height, 0.0f},
{cursor_coords.x + cursor_width, cursor_coords.y + text->line_height, 0.0f},
{cursor_coords.x + cursor_width, cursor_coords.y, 0.0f},
};
const blender::float3 descender_offs{0.0f, float(text->font_descender), 0.0f};
immBegin(GPU_PRIM_TRIS, 6);
immUniformThemeColor(TH_SEQ_TEXT_CURSOR);
for (int i : blender::IndexRange(0, 4)) {
cursor_quad[i] += descender_offs + view_offs;
cursor_quad[i] = blender::math::transform_point(transform_mat, cursor_quad[i]);
cursor_quad[i].x *= view_aspect;
}
for (int i : blender::Vector<int>{0, 1, 2, 2, 3, 0}) {
immVertex2f(pos, cursor_quad[i][0], cursor_quad[i][1]);
}
immEnd();
}
static void text_edit_draw_box(const bContext *C, const Sequence *seq, uint pos)
{
const TextVars *data = static_cast<TextVars *>(seq->effectdata);
const TextVarsRuntime *text = data->runtime;
const Scene *scene = CTX_data_scene(C);
const blender::float3 view_offs{-scene->r.xsch / 2.0f, -scene->r.ysch / 2.0f, 0.0f};
const float view_aspect = scene->r.xasp / scene->r.yasp;
blender::float4x4 transform_mat;
SEQ_image_transform_matrix_get(CTX_data_scene(C), seq, transform_mat.ptr());
blender::float4x3 box_quad{
{float(text->text_boundbox.xmin), float(text->text_boundbox.ymin), 0.0f},
{float(text->text_boundbox.xmin), float(text->text_boundbox.ymax), 0.0f},
{float(text->text_boundbox.xmax), float(text->text_boundbox.ymax), 0.0f},
{float(text->text_boundbox.xmax), float(text->text_boundbox.ymin), 0.0f},
};
GPU_blend(GPU_BLEND_NONE);
immBindBuiltinProgram(GPU_SHADER_3D_LINE_DASHED_UNIFORM_COLOR);
blender::float3 col;
UI_GetThemeColorShade3fv(TH_SEQ_ACTIVE, -50, col);
immUniformColor3fv(col);
immUniform1f("lineWidth", U.pixelsize);
immUniform1f("dash_width", 10.0f);
immBegin(GPU_PRIM_LINE_LOOP, 4);
for (int i : blender::IndexRange(0, 4)) {
box_quad[i] += view_offs;
box_quad[i] = blender::math::transform_point(transform_mat, box_quad[i]);
box_quad[i].x *= view_aspect;
immVertex2f(pos, box_quad[i][0], box_quad[i][1]);
}
immEnd();
immUnbindProgram();
}
static void text_edit_draw(const bContext *C)
{
if (!sequencer_text_editing_active_poll(const_cast<bContext *>(C))) {
return;
}
const Sequence *seq = SEQ_select_active_get(CTX_data_scene(C));
if (!SEQ_effects_can_render_text(seq)) {
return;
}
GPUVertFormat *format = immVertexFormat();
const uint pos = GPU_vertformat_attr_add(format, "pos", GPU_COMP_F32, 2, GPU_FETCH_FLOAT);
GPU_line_smooth(true);
GPU_blend(GPU_BLEND_ALPHA);
immBindBuiltinProgram(GPU_SHADER_3D_UNIFORM_COLOR);
text_selection_draw(C, seq, pos);
text_edit_draw_cursor(C, seq, pos);
immUnbindProgram();
GPU_blend(GPU_BLEND_NONE);
GPU_line_smooth(false);
text_edit_draw_box(C, seq, pos);
}
void sequencer_draw_preview(const bContext *C,
Scene *scene,
ARegion *region,
@@ -1156,6 +1347,7 @@ void sequencer_draw_preview(const bContext *C,
Sequence *active_seq = SEQ_select_active_get(scene);
for (Sequence *seq : strips) {
seq_draw_image_origin_and_outline(C, seq, seq == active_seq);
text_edit_draw(C);
}
}

View File

@@ -438,12 +438,12 @@ void recurs_sel_seq(Sequence *seq_meta)
}
}
static bool seq_point_image_isect(const Scene *scene, const Sequence *seq, float point[2])
bool seq_point_image_isect(const Scene *scene, const Sequence *seq, float point_view[2])
{
float seq_image_quad[4][2];
SEQ_image_transform_final_quad_get(scene, seq, seq_image_quad);
return isect_point_quad_v2(
point, seq_image_quad[0], seq_image_quad[1], seq_image_quad[2], seq_image_quad[3]);
point_view, seq_image_quad[0], seq_image_quad[1], seq_image_quad[2], seq_image_quad[3]);
}
static void sequencer_select_do_updates(bContext *C, Scene *scene)

View File

@@ -0,0 +1,883 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup spseq
*/
#include <cstddef>
#include "DNA_sequence_types.h"
#include "BLI_math_matrix.hh"
#include "BLI_math_vector.hh"
#include "BLI_string.h"
#include "BLI_string_utf8.h"
#include "BKE_context.hh"
#include "BKE_scene.hh"
#include "SEQ_effects.hh"
#include "SEQ_relations.hh"
#include "SEQ_select.hh"
#include "SEQ_time.hh"
#include "SEQ_transform.hh"
#include "WM_api.hh"
#include "RNA_define.hh"
#include "UI_view2d.hh"
#include "ED_screen.hh"
/* Own include. */
#include "sequencer_intern.hh"
using namespace blender;
static bool sequencer_text_editing_poll(bContext *C)
{
if (!sequencer_editing_initialized_and_active(C)) {
return false;
}
const Sequence *seq = SEQ_select_active_get(CTX_data_scene(C));
if (seq == nullptr || seq->type != SEQ_TYPE_TEXT || !SEQ_effects_can_render_text(seq)) {
return false;
}
const TextVars *data = static_cast<TextVars *>(seq->effectdata);
if (data == nullptr || data->runtime == nullptr) {
return false;
}
return true;
}
bool sequencer_text_editing_active_poll(bContext *C)
{
const Sequence *seq = SEQ_select_active_get(CTX_data_scene(C));
if (seq == nullptr || !sequencer_text_editing_poll(C)) {
return false;
}
if (ED_screen_animation_no_scrub(CTX_wm_manager(C))) {
return false;
}
const Scene *scene = CTX_data_scene(C);
if (!SEQ_time_strip_intersects_frame(scene, seq, BKE_scene_frame_get(scene))) {
return false;
}
return (seq->flag & SEQ_FLAG_TEXT_EDITING_ACTIVE) != 0;
}
int2 seq_text_cursor_offset_to_position(const TextVarsRuntime *text, int cursor_offset)
{
cursor_offset = std::clamp(cursor_offset, 0, text->character_count);
int2 cursor_position{0, 0};
for (const seq::LineInfo &line : text->lines) {
if (cursor_offset < line.characters.size()) {
cursor_position.x = cursor_offset;
break;
}
cursor_offset -= line.characters.size();
cursor_position.y += 1;
}
cursor_position.y = std::clamp(cursor_position.y, 0, int(text->lines.size() - 1));
cursor_position.x = std::clamp(
cursor_position.x, 0, int(text->lines[cursor_position.y].characters.size() - 1));
return cursor_position;
}
static const seq::CharInfo &character_at_cursor_pos_get(const TextVarsRuntime *text,
const int2 cursor_pos)
{
return text->lines[cursor_pos.y].characters[cursor_pos.x];
}
static const seq::CharInfo &character_at_cursor_offset_get(const TextVarsRuntime *text,
const int cursor_offset)
{
const int2 cursor_pos = seq_text_cursor_offset_to_position(text, cursor_offset);
return character_at_cursor_pos_get(text, cursor_pos);
}
static int cursor_position_to_offset(const TextVarsRuntime *text, int2 cursor_position)
{
return character_at_cursor_pos_get(text, cursor_position).index;
}
static void text_selection_cancel(TextVars *data)
{
data->selection_start_offset = 0;
data->selection_end_offset = 0;
}
IndexRange seq_text_selection_range_get(const TextVars *data)
{
/* Ensure, that selection start < selection end. */
int sel_start_offset = data->selection_start_offset;
int sel_end_offset = data->selection_end_offset;
if (sel_start_offset > sel_end_offset) {
std::swap(sel_start_offset, sel_end_offset);
}
return IndexRange(sel_start_offset, sel_end_offset - sel_start_offset);
}
static bool text_has_selection(const TextVars *data)
{
return !seq_text_selection_range_get(data).is_empty();
}
static void delete_selected_text(TextVars *data)
{
if (!text_has_selection(data)) {
return;
}
TextVarsRuntime *text = data->runtime;
IndexRange sel_range = seq_text_selection_range_get(data);
seq::CharInfo char_start = character_at_cursor_offset_get(text, sel_range.first());
seq::CharInfo char_end = character_at_cursor_offset_get(text, sel_range.last());
char *addr_start = const_cast<char *>(char_start.str_ptr);
char *addr_end = const_cast<char *>(char_end.str_ptr) + char_end.byte_length;
std::memmove(addr_start, addr_end, BLI_strnlen(addr_end, sizeof(data->text)) + 1);
const int2 sel_start = seq_text_cursor_offset_to_position(text, sel_range.first());
data->cursor_offset = cursor_position_to_offset(text, sel_start);
text_selection_cancel(data);
}
static void text_editing_update(const bContext *C)
{
Sequence *seq = SEQ_select_active_get(CTX_data_scene(C));
SEQ_relations_invalidate_cache_raw(CTX_data_scene(C), seq);
WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER, CTX_data_scene(C));
}
enum {
LINE_BEGIN,
LINE_END,
TEXT_BEGIN,
TEXT_END,
PREV_CHAR,
NEXT_CHAR,
PREV_WORD,
NEXT_WORD,
PREV_LINE,
NEXT_LINE,
};
static const EnumPropertyItem move_type_items[] = {
{LINE_BEGIN, "LINE_BEGIN", 0, "Line Begin", ""},
{LINE_END, "LINE_END", 0, "Line End", ""},
{TEXT_BEGIN, "TEXT_BEGIN", 0, "Text Begin", ""},
{TEXT_END, "TEXT_END", 0, "Text End", ""},
{PREV_CHAR, "PREVIOUS_CHARACTER", 0, "Previous Character", ""},
{NEXT_CHAR, "NEXT_CHARACTER", 0, "Next Character", ""},
{PREV_WORD, "PREVIOUS_WORD", 0, "Previous Word", ""},
{NEXT_WORD, "NEXT_WORD", 0, "Next Word", ""},
{PREV_LINE, "PREVIOUS_LINE", 0, "Previous Line", ""},
{NEXT_LINE, "NEXT_LINE", 0, "Next Line", ""},
{0, nullptr, 0, nullptr, nullptr},
};
static int2 cursor_move_by_character(int2 cursor_position, const TextVarsRuntime *text, int offset)
{
const seq::LineInfo &cur_line = text->lines[cursor_position.y];
/* Move to next line. */
if (cursor_position.x + offset > cur_line.characters.size() - 1 &&
cursor_position.y < text->lines.size() - 1)
{
cursor_position.x = 0;
cursor_position.y++;
}
/* Move to previous line. */
else if (cursor_position.x + offset < 0 && cursor_position.y > 0) {
cursor_position.y--;
cursor_position.x = text->lines[cursor_position.y].characters.size() - 1;
}
else {
cursor_position.x += offset;
const int position_max = text->lines[cursor_position.y].characters.size() - 1;
cursor_position.x = std::clamp(cursor_position.x, 0, position_max);
}
return cursor_position;
}
static int2 cursor_move_by_line(int2 cursor_position, const TextVarsRuntime *text, int offset)
{
const seq::LineInfo &cur_line = text->lines[cursor_position.y];
const int cur_pos_x = cur_line.characters[cursor_position.x].position.x;
const int line_max = text->lines.size() - 1;
const int new_line_index = std::clamp(cursor_position.y + offset, 0, line_max);
const seq::LineInfo &new_line = text->lines[new_line_index];
if (cursor_position.y == new_line_index) {
return cursor_position;
}
/* Find character in another line closest to current position. */
int best_distance = std::numeric_limits<int>::max();
int best_character_index = 0;
for (int i : new_line.characters.index_range()) {
seq::CharInfo character = new_line.characters[i];
const int distance = std::abs(character.position.x - cur_pos_x);
if (distance < best_distance) {
best_distance = distance;
best_character_index = i;
}
}
cursor_position.x = best_character_index;
cursor_position.y = new_line_index;
return cursor_position;
}
static int2 cursor_move_line_end(int2 cursor_position, const TextVarsRuntime *text)
{
const seq::LineInfo &cur_line = text->lines[cursor_position.y];
cursor_position.x = cur_line.characters.size() - 1;
return cursor_position;
}
static bool is_whitespace_transition(char chr1, char chr2)
{
return ELEM(chr1, ' ', '\t', '\n') && !ELEM(chr2, ' ', '\t', '\n');
}
static int2 cursor_move_prev_word(int2 cursor_position, const TextVarsRuntime *text)
{
cursor_position = cursor_move_by_character(cursor_position, text, -1);
while (cursor_position.x > 0 || cursor_position.y > 0) {
const seq::CharInfo character = character_at_cursor_pos_get(text, cursor_position);
const int2 prev_cursor_pos = cursor_move_by_character(cursor_position, text, -1);
const seq::CharInfo prev_character = character_at_cursor_pos_get(text, prev_cursor_pos);
if (is_whitespace_transition(prev_character.str_ptr[0], character.str_ptr[0])) {
break;
}
cursor_position = prev_cursor_pos;
}
return cursor_position;
}
static int2 cursor_move_next_word(int2 cursor_position, const TextVarsRuntime *text)
{
const int maxline = text->lines.size() - 1;
const int maxchar = text->lines.last().characters.size() - 1;
while ((cursor_position.x < maxchar) || (cursor_position.y < maxline)) {
const seq::CharInfo character = character_at_cursor_pos_get(text, cursor_position);
cursor_position = cursor_move_by_character(cursor_position, text, 1);
const seq::CharInfo next_character = character_at_cursor_pos_get(text, cursor_position);
if (is_whitespace_transition(next_character.str_ptr[0], character.str_ptr[0])) {
break;
}
}
return cursor_position;
}
static int sequencer_text_cursor_move_exec(bContext *C, wmOperator *op)
{
const Sequence *seq = SEQ_select_active_get(CTX_data_scene(C));
TextVars *data = static_cast<TextVars *>(seq->effectdata);
const TextVarsRuntime *text = data->runtime;
if (RNA_boolean_get(op->ptr, "select_text") && !text_has_selection(data)) {
data->selection_start_offset = data->cursor_offset;
}
int2 cursor_position = seq_text_cursor_offset_to_position(text, data->cursor_offset);
switch (RNA_enum_get(op->ptr, "type")) {
case PREV_CHAR:
cursor_position = cursor_move_by_character(cursor_position, text, -1);
break;
case NEXT_CHAR:
cursor_position = cursor_move_by_character(cursor_position, text, 1);
break;
case PREV_LINE:
cursor_position = cursor_move_by_line(cursor_position, text, -1);
break;
case NEXT_LINE:
cursor_position = cursor_move_by_line(cursor_position, text, 1);
break;
case LINE_BEGIN:
cursor_position.x = 0;
break;
case LINE_END:
cursor_position = cursor_move_line_end(cursor_position, text);
break;
case TEXT_BEGIN:
cursor_position = {0, 0};
break;
case TEXT_END:
cursor_position.y = text->lines.size() - 1;
cursor_position = cursor_move_line_end(cursor_position, text);
break;
case PREV_WORD:
cursor_position = cursor_move_prev_word(cursor_position, text);
break;
case NEXT_WORD:
cursor_position = cursor_move_next_word(cursor_position, text);
break;
}
data->cursor_offset = cursor_position_to_offset(text, cursor_position);
if (RNA_boolean_get(op->ptr, "select_text")) {
data->selection_end_offset = data->cursor_offset;
}
if (!RNA_boolean_get(op->ptr, "select_text") ||
data->cursor_offset == data->selection_start_offset)
{
text_selection_cancel(data);
}
WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER, CTX_data_scene(C));
return OPERATOR_FINISHED;
}
void SEQUENCER_OT_text_cursor_move(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Move cursor";
ot->description = "Move cursor in text";
ot->idname = "SEQUENCER_OT_text_cursor_move";
/* api callbacks */
ot->exec = sequencer_text_cursor_move_exec;
ot->poll = sequencer_text_editing_active_poll;
/* flags */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
/* properties */
RNA_def_enum(ot->srna,
"type",
move_type_items,
LINE_BEGIN,
"Type",
"Where to move cursor to, to make a selection");
PropertyRNA *prop = RNA_def_boolean(
ot->srna, "select_text", false, "Select Text", "Select text while moving cursor");
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
}
static bool text_insert(TextVars *data, const char *buf)
{
const TextVarsRuntime *text = data->runtime;
delete_selected_text(data);
const size_t in_str_len = BLI_strnlen(buf, sizeof(buf));
const size_t text_str_len = BLI_strnlen(data->text, sizeof(data->text));
if (text_str_len + in_str_len + 1 > sizeof(data->text)) {
return false;
}
const seq::CharInfo cur_char = character_at_cursor_offset_get(text, data->cursor_offset);
char *cursor_addr = const_cast<char *>(cur_char.str_ptr);
const size_t move_str_len = BLI_strnlen(cursor_addr, sizeof(data->text)) + 1;
std::memmove(cursor_addr + in_str_len, cursor_addr, move_str_len);
std::memcpy(cursor_addr, buf, in_str_len);
data->cursor_offset += 1;
return true;
}
static int sequencer_text_insert_exec(bContext *C, wmOperator *op)
{
const Sequence *seq = SEQ_select_active_get(CTX_data_scene(C));
TextVars *data = static_cast<TextVars *>(seq->effectdata);
char str[512];
RNA_string_get(op->ptr, "string", str);
const size_t in_buf_len = BLI_strnlen(str, sizeof(str));
if (in_buf_len == 0) {
return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH;
}
if (!text_insert(data, str)) {
return OPERATOR_CANCELLED;
}
text_editing_update(C);
return OPERATOR_FINISHED;
}
static int sequencer_text_insert_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
char str[6];
BLI_strncpy(str, event->utf8_buf, BLI_str_utf8_size_safe(event->utf8_buf) + 1);
RNA_string_set(op->ptr, "string", str);
return sequencer_text_insert_exec(C, op);
}
void SEQUENCER_OT_text_insert(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Insert Character";
ot->description = "Insert text at cursor position";
ot->idname = "SEQUENCER_OT_text_insert";
/* api callbacks */
ot->exec = sequencer_text_insert_exec;
ot->invoke = sequencer_text_insert_invoke;
ot->poll = sequencer_text_editing_active_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
/* properties */
RNA_def_string(
ot->srna, "string", nullptr, 512, "String", "String to be inserted at cursor position");
}
enum { DEL_NEXT_SEL, DEL_PREV_SEL };
static const EnumPropertyItem delete_type_items[] = {
{DEL_NEXT_SEL, "NEXT_OR_SELECTION", 0, "Next or Selection", ""},
{DEL_PREV_SEL, "PREVIOUS_OR_SELECTION", 0, "Previous or Selection", ""},
{0, nullptr, 0, nullptr, nullptr},
};
static void delete_character(const seq::CharInfo character, const TextVars *data)
{
char *cursor_addr = const_cast<char *>(character.str_ptr);
char *next_char_addr = cursor_addr + character.byte_length;
std::memmove(cursor_addr, next_char_addr, BLI_strnlen(next_char_addr, sizeof(data->text)) + 1);
}
static int sequencer_text_delete_exec(bContext *C, wmOperator *op)
{
const Sequence *seq = SEQ_select_active_get(CTX_data_scene(C));
TextVars *data = static_cast<TextVars *>(seq->effectdata);
const TextVarsRuntime *text = data->runtime;
const int type = RNA_enum_get(op->ptr, "type");
if (text_has_selection(data)) {
delete_selected_text(data);
text_editing_update(C);
return OPERATOR_FINISHED;
}
if (type == DEL_NEXT_SEL) {
if (data->cursor_offset >= text->character_count) {
return OPERATOR_CANCELLED;
}
delete_character(character_at_cursor_offset_get(text, data->cursor_offset), data);
}
if (type == DEL_PREV_SEL) {
if (data->cursor_offset == 0) {
return OPERATOR_CANCELLED;
}
delete_character(character_at_cursor_offset_get(text, data->cursor_offset - 1), data);
data->cursor_offset -= 1;
}
text_editing_update(C);
return OPERATOR_FINISHED;
}
void SEQUENCER_OT_text_delete(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Delete Character";
ot->description = "Delete text at cursor position";
ot->idname = "SEQUENCER_OT_text_delete";
/* api callbacks */
ot->exec = sequencer_text_delete_exec;
ot->poll = sequencer_text_editing_active_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
/* properties */
RNA_def_enum(ot->srna,
"type",
delete_type_items,
DEL_NEXT_SEL,
"Type",
"Which part of the text to delete");
}
static int sequencer_text_line_break_exec(bContext *C, wmOperator * /*op*/)
{
const Sequence *seq = SEQ_select_active_get(CTX_data_scene(C));
TextVars *data = static_cast<TextVars *>(seq->effectdata);
if (!text_insert(data, "\n")) {
return OPERATOR_CANCELLED;
}
text_editing_update(C);
return OPERATOR_FINISHED;
}
void SEQUENCER_OT_text_line_break(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Insert Line Break";
ot->description = "Insert line break at cursor position";
ot->idname = "SEQUENCER_OT_text_line_break";
/* api callbacks */
ot->exec = sequencer_text_line_break_exec;
ot->poll = sequencer_text_editing_active_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
}
static int sequencer_text_select_all(bContext *C, wmOperator * /*op*/)
{
const Sequence *seq = SEQ_select_active_get(CTX_data_scene(C));
TextVars *data = static_cast<TextVars *>(seq->effectdata);
data->selection_start_offset = 0;
data->selection_end_offset = data->runtime->character_count;
text_editing_update(C);
return OPERATOR_FINISHED;
}
void SEQUENCER_OT_text_select_all(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Select All";
ot->description = "Select all characters";
ot->idname = "SEQUENCER_OT_text_select_all";
/* api callbacks */
ot->exec = sequencer_text_select_all;
ot->poll = sequencer_text_editing_active_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
}
static int sequencer_text_deselect_all(bContext *C, wmOperator * /*op*/)
{
Sequence *seq = SEQ_select_active_get(CTX_data_scene(C));
TextVars *data = static_cast<TextVars *>(seq->effectdata);
if (!text_has_selection(data)) {
/* Exit edit mode, so text can be translated by mouse. */
seq->flag &= ~SEQ_FLAG_TEXT_EDITING_ACTIVE;
}
else {
text_selection_cancel(data);
}
text_editing_update(C);
return OPERATOR_FINISHED;
}
void SEQUENCER_OT_text_deselect_all(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Deselect All";
ot->description = "Deselect all characters";
ot->idname = "SEQUENCER_OT_text_deselect_all";
/* api callbacks */
ot->exec = sequencer_text_deselect_all;
ot->poll = sequencer_text_editing_active_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
}
static int sequencer_text_edit_mode_toggle(bContext *C, wmOperator * /*op*/)
{
Sequence *seq = SEQ_select_active_get(CTX_data_scene(C));
if (sequencer_text_editing_active_poll(C)) {
seq->flag &= ~SEQ_FLAG_TEXT_EDITING_ACTIVE;
}
else {
seq->flag |= SEQ_FLAG_TEXT_EDITING_ACTIVE;
}
WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER, CTX_data_scene(C));
return OPERATOR_FINISHED;
}
void SEQUENCER_OT_text_edit_mode_toggle(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Edit Text";
ot->description = "Toggle text editing";
ot->idname = "SEQUENCER_OT_text_edit_mode_toggle";
/* api callbacks */
ot->exec = sequencer_text_edit_mode_toggle;
ot->poll = sequencer_text_editing_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
}
static int find_closest_cursor_offset(const TextVars *data, float2 mouse_loc)
{
const TextVarsRuntime *text = data->runtime;
int best_cursor_offset = 0;
float best_distance = std::numeric_limits<float>::max();
for (const seq::LineInfo &line : text->lines) {
for (const seq::CharInfo &character : line.characters) {
const float distance = math::distance(mouse_loc, character.position);
if (distance < best_distance) {
best_distance = distance;
best_cursor_offset = character.index;
}
}
}
return best_cursor_offset;
}
static void cursor_set_by_mouse_position(const bContext *C, const wmEvent *event)
{
const Scene *scene = CTX_data_scene(C);
const Sequence *seq = SEQ_select_active_get(scene);
TextVars *data = static_cast<TextVars *>(seq->effectdata);
const View2D *v2d = UI_view2d_fromcontext(C);
int2 mval_region;
WM_event_drag_start_mval(event, CTX_wm_region(C), mval_region);
float3 mouse_loc;
UI_view2d_region_to_view(v2d, mval_region.x, mval_region.y, &mouse_loc.x, &mouse_loc.y);
/* Convert cursor coordinates to domain of CharInfo::position. */
const blender::float3 view_offs{-scene->r.xsch / 2.0f, -scene->r.ysch / 2.0f, 0.0f};
const float view_aspect = scene->r.xasp / scene->r.yasp;
blender::float4x4 transform_mat;
SEQ_image_transform_matrix_get(CTX_data_scene(C), seq, transform_mat.ptr());
transform_mat = blender::math::invert(transform_mat);
mouse_loc.x /= view_aspect;
mouse_loc = blender::math::transform_point(transform_mat, mouse_loc);
mouse_loc -= view_offs;
data->cursor_offset = find_closest_cursor_offset(data, float2(mouse_loc));
}
static int sequencer_text_cursor_set_modal(bContext *C, wmOperator * /*op*/, const wmEvent *event)
{
const Scene *scene = CTX_data_scene(C);
const Sequence *seq = SEQ_select_active_get(scene);
TextVars *data = static_cast<TextVars *>(seq->effectdata);
bool make_selection = false;
switch (event->type) {
case LEFTMOUSE:
if (event->val == KM_RELEASE) {
cursor_set_by_mouse_position(C, event);
if (make_selection) {
data->selection_end_offset = data->cursor_offset;
}
return OPERATOR_FINISHED;
}
break;
case MIDDLEMOUSE:
case RIGHTMOUSE:
return OPERATOR_FINISHED;
case MOUSEMOVE:
make_selection = true;
if (!text_has_selection(data)) {
data->selection_start_offset = data->cursor_offset;
}
cursor_set_by_mouse_position(C, event);
data->selection_end_offset = data->cursor_offset;
break;
}
WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER, CTX_data_scene(C));
return OPERATOR_RUNNING_MODAL;
}
static int sequencer_text_cursor_set_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
const Scene *scene = CTX_data_scene(C);
Sequence *seq = SEQ_select_active_get(scene);
TextVars *data = static_cast<TextVars *>(seq->effectdata);
const View2D *v2d = UI_view2d_fromcontext(C);
int2 mval_region;
WM_event_drag_start_mval(event, CTX_wm_region(C), mval_region);
float2 mouse_loc;
UI_view2d_region_to_view(v2d, mval_region.x, mval_region.y, &mouse_loc.x, &mouse_loc.y);
if (!seq_point_image_isect(scene, seq, mouse_loc)) {
seq->flag &= ~SEQ_FLAG_TEXT_EDITING_ACTIVE;
return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH;
}
text_selection_cancel(data);
cursor_set_by_mouse_position(C, event);
WM_event_add_modal_handler(C, op);
WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER, CTX_data_scene(C));
return OPERATOR_RUNNING_MODAL;
}
void SEQUENCER_OT_text_cursor_set(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Set Cursor";
ot->description = "Set cursor position in text";
ot->idname = "SEQUENCER_OT_text_cursor_set";
/* api callbacks */
ot->invoke = sequencer_text_cursor_set_invoke;
ot->modal = sequencer_text_cursor_set_modal;
ot->poll = sequencer_text_editing_active_poll;
/* flags */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
/* properties */
PropertyRNA *prop = RNA_def_boolean(
ot->srna, "select_text", false, "Select Text", "Select text while moving cursor");
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
}
static void text_edit_copy(const TextVars *data)
{
const TextVarsRuntime *text = data->runtime;
const IndexRange selection_range = seq_text_selection_range_get(data);
const seq::CharInfo start = character_at_cursor_offset_get(text, selection_range.first());
const seq::CharInfo end = character_at_cursor_offset_get(text, selection_range.last());
const size_t len = end.str_ptr + end.byte_length - start.str_ptr;
char clipboard_buf[sizeof(data->text)] = {0};
memcpy(clipboard_buf, start.str_ptr, math::min(len, sizeof(clipboard_buf)));
WM_clipboard_text_set(clipboard_buf, false);
}
static int sequencer_text_edit_copy_exec(bContext *C, wmOperator * /*op*/)
{
const Sequence *seq = SEQ_select_active_get(CTX_data_scene(C));
const TextVars *data = static_cast<TextVars *>(seq->effectdata);
if (!text_has_selection(data)) {
return OPERATOR_CANCELLED;
}
text_edit_copy(data);
return OPERATOR_FINISHED;
}
void SEQUENCER_OT_text_edit_copy(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Copy Text";
ot->description = "Copy text to clipboard";
ot->idname = "SEQUENCER_OT_text_edit_copy";
/* api callbacks */
ot->exec = sequencer_text_edit_copy_exec;
ot->poll = sequencer_text_editing_active_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
}
static int sequencer_text_edit_paste_exec(bContext *C, wmOperator * /*op*/)
{
const Sequence *seq = SEQ_select_active_get(CTX_data_scene(C));
TextVars *data = static_cast<TextVars *>(seq->effectdata);
const TextVarsRuntime *text = data->runtime;
delete_selected_text(data);
int clipboard_len;
char *clipboard_buf = WM_clipboard_text_get(false, true, &clipboard_len);
if (clipboard_len == 0) {
return OPERATOR_CANCELLED;
}
const int max_str_len = sizeof(data->text) - (BLI_strnlen(data->text, sizeof(data->text)) + 1);
clipboard_len = std::min(clipboard_len, max_str_len);
const seq::CharInfo cur_char = character_at_cursor_offset_get(text, data->cursor_offset);
char *cursor_addr = const_cast<char *>(cur_char.str_ptr);
const size_t move_str_len = BLI_strnlen(cursor_addr, sizeof(data->text)) + 1;
std::memmove(cursor_addr + clipboard_len, cursor_addr, move_str_len);
std::memcpy(cursor_addr, clipboard_buf, clipboard_len);
data->cursor_offset += BLI_strlen_utf8(clipboard_buf);
MEM_freeN(clipboard_buf);
text_editing_update(C);
return OPERATOR_FINISHED;
}
void SEQUENCER_OT_text_edit_paste(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Paste Text";
ot->description = "Paste text to clipboard";
ot->idname = "SEQUENCER_OT_text_edit_paste";
/* api callbacks */
ot->exec = sequencer_text_edit_paste_exec;
ot->poll = sequencer_text_editing_active_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
}
static int sequencer_text_edit_cut_exec(bContext *C, wmOperator * /*op*/)
{
const Sequence *seq = SEQ_select_active_get(CTX_data_scene(C));
TextVars *data = static_cast<TextVars *>(seq->effectdata);
if (!text_has_selection(data)) {
return OPERATOR_CANCELLED;
}
text_edit_copy(data);
delete_selected_text(data);
text_editing_update(C);
return OPERATOR_FINISHED;
}
void SEQUENCER_OT_text_edit_cut(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Cut Text";
ot->description = "Cut text to clipboard";
ot->idname = "SEQUENCER_OT_text_edit_cut";
/* api callbacks */
ot->exec = sequencer_text_edit_cut_exec;
ot->poll = sequencer_text_editing_active_poll;
/* flags */
ot->flag = OPTYPE_UNDO;
}

View File

@@ -788,11 +788,16 @@ static void sequencer_preview_region_init(wmWindowManager *wm, ARegion *region)
WM_event_add_keymap_handler_v2d_mask(&region->runtime->handlers, keymap);
#endif
/* Own keymap. */
keymap = WM_keymap_ensure(wm->defaultconf, "SequencerPreview", SPACE_SEQ, RGN_TYPE_WINDOW);
WM_event_add_keymap_handler_v2d_mask(&region->runtime->handlers, keymap);
keymap = WM_keymap_ensure(wm->defaultconf, "SequencerCommon", SPACE_SEQ, RGN_TYPE_WINDOW);
WM_event_add_keymap_handler_v2d_mask(&region->runtime->handlers, keymap);
/* Own keymap. */
keymap = WM_keymap_ensure(wm->defaultconf, "SequencerPreview", SPACE_SEQ, RGN_TYPE_WINDOW);
/* Do this instead of adding V2D keymap flag to `art->keymapflag` text editing keymap conflicts
* with V2D keymap. This seems to be only way to define order of evaluation. */
keymap = WM_keymap_ensure(wm->defaultconf, "View2D", SPACE_EMPTY, RGN_TYPE_WINDOW);
WM_event_add_keymap_handler_v2d_mask(&region->runtime->handlers, keymap);
ListBase *lb = WM_dropboxmap_find("Sequencer", SPACE_SEQ, RGN_TYPE_PREVIEW);
@@ -1122,8 +1127,7 @@ void ED_spacetype_sequencer()
art->on_view2d_changed = sequencer_preview_region_view2d_changed;
art->draw = sequencer_preview_region_draw;
art->listener = sequencer_preview_region_listener;
art->keymapflag = ED_KEYMAP_TOOL | ED_KEYMAP_GIZMO | ED_KEYMAP_VIEW2D | ED_KEYMAP_FRAMES |
ED_KEYMAP_GPENCIL;
art->keymapflag = ED_KEYMAP_TOOL | ED_KEYMAP_GIZMO | ED_KEYMAP_GPENCIL;
BLI_addhead(&st->regiontypes, art);
/* List-view/buttons. */

View File

@@ -32,12 +32,15 @@ struct bSound;
namespace blender::seq {
struct MediaPresence;
struct ThumbnailCache;
struct TextVarsRuntime;
} // namespace blender::seq
using MediaPresence = blender::seq::MediaPresence;
using ThumbnailCache = blender::seq::ThumbnailCache;
using TextVarsRuntime = blender::seq::TextVarsRuntime;
#else
typedef struct MediaPresence MediaPresence;
typedef struct ThumbnailCache ThumbnailCache;
typedef struct TextVarsRuntime TextVarsRuntime;
#endif
/* -------------------------------------------------------------------- */
@@ -453,9 +456,17 @@ typedef struct TextVars {
float outline_width;
char flag;
char align;
char _pad[2];
/* Ofssets in bytes relative to #TextVars::text. */
int cursor_offset;
int selection_start_offset;
int selection_end_offset;
char align_y DNA_DEPRECATED /* Only used for versioning. */;
char anchor_x, anchor_y;
char _pad[7];
char _pad1;
TextVarsRuntime *runtime;
} TextVars;
/** #TextVars.flag */
@@ -627,7 +638,7 @@ enum {
SEQ_OVERLAP = (1 << 3),
SEQ_FILTERY = (1 << 4),
SEQ_MUTE = (1 << 5),
/* SEQ_FLAG_SKIP_THUMBNAILS = (1 << 6), */ /* no longer used */
SEQ_FLAG_TEXT_EDITING_ACTIVE = (1 << 6),
SEQ_REVERSE_FRAMES = (1 << 7),
SEQ_IPO_FRAME_LOCKED = (1 << 8),
SEQ_EFFECT_NOT_LOADED = (1 << 9),

View File

@@ -360,7 +360,7 @@ typedef struct ThemeSpace {
/** For sequence editor. */
unsigned char movie[4], movieclip[4], mask[4], image[4], scene[4], audio[4];
unsigned char effect[4], transition[4], meta[4], text_strip[4], color_strip[4];
unsigned char active_strip[4], selected_strip[4];
unsigned char active_strip[4], selected_strip[4], text_strip_cursor[4], selected_text[4];
/** For dopesheet - scale factor for size of keyframes (i.e. height of channels). */
float keyframe_scale_fac;

View File

@@ -3869,6 +3869,18 @@ static void rna_def_userdef_theme_space_seq(BlenderRNA *brna)
RNA_def_property_array(prop, 4);
RNA_def_property_ui_text(prop, "Alternate Rows", "Overlay color on every other row");
RNA_def_property_update(prop, 0, "rna_userdef_theme_update");
prop = RNA_def_property(srna, "text_strip_cursor", PROP_FLOAT, PROP_COLOR_GAMMA);
RNA_def_property_float_sdna(prop, nullptr, "text_strip_cursor");
RNA_def_property_array(prop, 4);
RNA_def_property_ui_text(prop, "Text Strip Cursor", "Text strip editing cursor");
RNA_def_property_update(prop, 0, "rna_userdef_theme_update");
prop = RNA_def_property(srna, "selected_text", PROP_FLOAT, PROP_COLOR_GAMMA);
RNA_def_property_float_sdna(prop, nullptr, "selected_text");
RNA_def_property_array(prop, 4);
RNA_def_property_ui_text(prop, "Selected text", "Text strip editing selection");
RNA_def_property_update(prop, 0, "rna_userdef_theme_update");
}
static void rna_def_userdef_theme_space_action(BlenderRNA *brna)

View File

@@ -92,10 +92,12 @@ SeqEffectHandle SEQ_effect_handle_get(Sequence *seq);
int SEQ_effect_get_num_inputs(int seq_type);
void SEQ_effect_text_font_unload(TextVars *data, bool do_id_user);
void SEQ_effect_text_font_load(TextVars *data, bool do_id_user);
bool SEQ_effects_can_render_text(const Sequence *seq);
namespace blender::seq {
struct CharInfo {
int index = 0;
const char *str_ptr = nullptr;
int byte_length = 0;
float2 position{0.0f, 0.0f};
@@ -111,11 +113,12 @@ struct LineInfo {
struct TextVarsRuntime {
Vector<LineInfo> lines;
rcti text_boundbox;
rcti text_boundbox; /* Boundbox used for box drawing and selection. */
int line_height;
int font_descender;
int character_count;
int font;
bool editing_is_active; /* UI uses this to differentiate behavior. */
};
} // namespace blender::seq

View File

@@ -129,3 +129,15 @@ void SEQ_image_transform_bounding_box_from_collection(Scene *scene,
bool apply_rotation,
float r_min[2],
float r_max[2]);
/**
* Get strip image transformation matrix. Pivot point is set to correspond with viewport coordinate
* system
*
* \param scene: Scene in which strips are located
* \param seq: Strip that is used to construct the matrix
* \param r_transform_matrix: Return value
*/
void SEQ_image_transform_matrix_get(const Scene *scene,
const Sequence *seq,
float r_transform_matrix[4][4]);

View File

@@ -9,6 +9,7 @@
*/
#include <cmath>
#include <cstddef>
#include <cstdlib>
#include <cstring>
#include <mutex>
@@ -2612,6 +2613,21 @@ static ImBuf *do_gaussian_blur_effect(const SeqRenderData *context,
/** \name Text Effect
* \{ */
/* `data->text[0] == 0` is ignored on purpose in order to make it possible to edit */
bool SEQ_effects_can_render_text(const Sequence *seq)
{
TextVars *data = static_cast<TextVars *>(seq->effectdata);
if (data->text_size < 1.0f ||
((data->color[3] == 0.0f) &&
(data->shadow_color[3] == 0.0f || (data->flag & SEQ_TEXT_SHADOW) == 0) &&
(data->outline_color[3] == 0.0f || data->outline_width <= 0.0f ||
(data->flag & SEQ_TEXT_OUTLINE) == 0)))
{
return false;
}
return true;
}
static void init_text_effect(Sequence *seq)
{
if (seq->effectdata) {
@@ -2706,6 +2722,7 @@ static void free_text_effect(Sequence *seq, const bool do_id_user)
SEQ_effect_text_font_unload(data, do_id_user);
if (data) {
MEM_delete(data->runtime);
MEM_freeN(data);
seq->effectdata = nullptr;
}
@@ -2722,6 +2739,7 @@ static void copy_text_effect(Sequence *dst, const Sequence *src, const int flag)
dst->effectdata = MEM_dupallocN(src->effectdata);
TextVars *data = static_cast<TextVars *>(dst->effectdata);
data->runtime = nullptr;
data->text_blf_id = -1;
SEQ_effect_text_font_load(data, (flag & LIB_ID_CREATE_NO_USER_REFCOUNT) == 0);
}
@@ -2733,13 +2751,7 @@ static int num_inputs_text()
static StripEarlyOut early_out_text(const Sequence *seq, float /*fac*/)
{
TextVars *data = static_cast<TextVars *>(seq->effectdata);
if (data->text[0] == 0 || data->text_size < 1.0f ||
((data->color[3] == 0.0f) &&
(data->shadow_color[3] == 0.0f || (data->flag & SEQ_TEXT_SHADOW) == 0) &&
(data->outline_color[3] == 0.0f || data->outline_width <= 0.0f ||
(data->flag & SEQ_TEXT_OUTLINE) == 0)))
{
if (!SEQ_effects_can_render_text(seq)) {
return StripEarlyOut::UseInput1;
}
return StripEarlyOut::NoInput;
@@ -2999,29 +3011,29 @@ static void jump_flooding_pass(Span<JFACoord> input,
}
namespace blender::seq {
static void text_draw(const TextVarsRuntime &runtime, float color[4])
static void text_draw(const TextVarsRuntime *runtime, float color[4])
{
for (const LineInfo &line : runtime.lines) {
for (const LineInfo &line : runtime->lines) {
for (const CharInfo &character : line.characters) {
BLF_position(runtime.font, character.position.x, character.position.y, 0.0f);
BLF_buffer_col(runtime.font, color);
BLF_draw_buffer(runtime.font, character.str_ptr, character.byte_length);
BLF_position(runtime->font, character.position.x, character.position.y, 0.0f);
BLF_buffer_col(runtime->font, color);
BLF_draw_buffer(runtime->font, character.str_ptr, character.byte_length);
}
}
}
static rcti draw_text_outline(const SeqRenderData *context,
const TextVars *data,
const TextVarsRuntime &runtime,
const TextVarsRuntime *runtime,
ColorManagedDisplay *display,
ImBuf *out)
{
/* Outline width of 1.0 maps to half of text line height. */
const int outline_width = int(runtime.line_height * 0.5f * data->outline_width);
const int outline_width = int(runtime->line_height * 0.5f * data->outline_width);
if (outline_width < 1 || data->outline_color[3] <= 0.0f ||
((data->flag & SEQ_TEXT_OUTLINE) == 0))
{
return runtime.text_boundbox;
return runtime->text_boundbox;
}
const int2 size = int2(context->rectx, context->recty);
@@ -3029,11 +3041,11 @@ static rcti draw_text_outline(const SeqRenderData *context,
/* Draw white text into temporary buffer. */
const size_t pixel_count = size_t(size.x) * size.y;
Array<uchar4> tmp_buf(pixel_count, uchar4(0));
BLF_buffer(runtime.font, nullptr, (uchar *)tmp_buf.data(), size.x, size.y, display);
BLF_buffer(runtime->font, nullptr, (uchar *)tmp_buf.data(), size.x, size.y, display);
text_draw(runtime, float4(1.0f));
rcti outline_rect = runtime.text_boundbox;
rcti outline_rect = runtime->text_boundbox;
BLI_rcti_pad(&outline_rect, outline_width + 1, outline_width + 1);
outline_rect.xmin = clamp_i(outline_rect.xmin, 0, size.x - 1);
outline_rect.xmax = clamp_i(outline_rect.xmax, 0, size.x - 1);
@@ -3129,7 +3141,7 @@ static rcti draw_text_outline(const SeqRenderData *context,
}
}
});
BLF_buffer(runtime.font, nullptr, out->byte_buffer.data, size.x, size.y, display);
BLF_buffer(runtime->font, nullptr, out->byte_buffer.data, size.x, size.y, display);
return outline_rect;
}
@@ -3252,17 +3264,20 @@ static blender::Vector<CharInfo> build_character_info(const TextVars *data, int
blender::Vector<CharInfo> characters;
const size_t len_max = BLI_strnlen(data->text, sizeof(data->text));
int byte_offset = 0;
int char_index = 0;
while (byte_offset <= len_max) {
const char *str = data->text + byte_offset;
const int char_length = BLI_str_utf8_size_safe(str);
CharInfo char_info;
char_info.index = char_index;
char_info.str_ptr = str;
char_info.byte_length = char_length;
char_info.advance_x = BLF_glyph_advance(font, str);
characters.append(char_info);
byte_offset += char_length;
char_index++;
}
return characters;
}
@@ -3277,7 +3292,7 @@ static int wrap_width_get(const TextVars *data, const int2 image_size)
/* Lines must contain CharInfo for newlines and \0, as UI must know where they begin. */
static void apply_word_wrapping(const TextVars *data,
TextVarsRuntime &runtime,
TextVarsRuntime *runtime,
const int2 image_size,
blender::Vector<CharInfo> &characters)
{
@@ -3305,18 +3320,18 @@ static void apply_word_wrapping(const TextVars *data,
/* Second pass: Fill lines with characters. */
char_position = {0.0f, 0.0f};
runtime.lines.append(LineInfo());
runtime->lines.append(LineInfo());
for (CharInfo &character : characters) {
character.position = char_position;
runtime.lines.last().characters.append(character);
runtime.lines.last().width = char_position.x;
runtime->lines.last().characters.append(character);
runtime->lines.last().width = char_position.x;
char_position.x += character.advance_x;
if (character.do_wrap || character.str_ptr[0] == '\n') {
runtime.lines.append(LineInfo());
runtime->lines.append(LineInfo());
char_position.x = 0;
char_position.y -= runtime.line_height;
char_position.y -= runtime->line_height;
}
}
}
@@ -3377,59 +3392,67 @@ static float2 anchor_offset_get(const TextVars *data, int width_max, int text_he
return anchor_offset;
}
static void apply_text_alignment(const TextVars *data,
TextVarsRuntime &runtime,
const int2 image_size)
static void calc_boundbox(const TextVars *data, TextVarsRuntime *runtime, const int2 image_size)
{
const int width_max = text_box_width_get(runtime.lines);
const int text_height = runtime.lines.size() * runtime.line_height;
const int text_height = runtime->lines.size() * runtime->line_height;
int width_max = text_box_width_get(runtime->lines);
/* Add width to empty text, so there is something to draw or select. */
if (width_max == 0) {
width_max = text_height * 2;
}
const float2 image_center{data->loc[0] * image_size.x, data->loc[1] * image_size.y};
const float2 line_height_offset{0.0f, float(-runtime.line_height - BLF_descender(runtime.font))};
const float2 anchor = anchor_offset_get(data, width_max, text_height);
Vector<rcti> line_boxes;
runtime->text_boundbox.xmin = anchor.x + image_center.x;
runtime->text_boundbox.xmax = anchor.x + image_center.x + width_max;
runtime->text_boundbox.ymin = anchor.y + image_center.y - text_height;
runtime->text_boundbox.ymax = runtime->text_boundbox.ymin + text_height;
}
for (LineInfo &line : runtime.lines) {
static void apply_text_alignment(const TextVars *data,
TextVarsRuntime *runtime,
const int2 image_size)
{
const int width_max = text_box_width_get(runtime->lines);
const int text_height = runtime->lines.size() * runtime->line_height;
const float2 image_center{data->loc[0] * image_size.x, data->loc[1] * image_size.y};
const float2 line_height_offset{0.0f,
float(-runtime->line_height - BLF_descender(runtime->font))};
const float2 anchor = anchor_offset_get(data, width_max, text_height);
for (LineInfo &line : runtime->lines) {
const float2 alignment_x = horizontal_alignment_offset_get(data, line.width, width_max);
const float2 alignment = math::round(image_center + line_height_offset + alignment_x + anchor);
for (CharInfo &character : line.characters) {
character.position += alignment;
}
/* Get text box for line.
* This has to be done, because some fonts do not define a descender value,
* but define their height. In that case, box has unwanted offset in Y axis. */
rcti line_box;
size_t str_len = line.characters.last().str_ptr - line.characters.first().str_ptr;
BLF_boundbox(runtime.font, line.characters.first().str_ptr, str_len, &line_box, nullptr);
BLI_rcti_translate(
&line_box, line.characters.first().position.x, line.characters.first().position.y);
line_boxes.append(line_box);
}
runtime.text_boundbox = line_boxes.first();
for (const rcti &box : line_boxes) {
BLI_rcti_union(&runtime.text_boundbox, &box);
}
}
static void calc_text_runtime(const Sequence *seq,
int font,
const int2 image_size,
TextVarsRuntime &r_runtime)
static void calc_text_runtime(const Sequence *seq, int font, const int2 image_size)
{
const TextVars *data = static_cast<TextVars *>(seq->effectdata);
TextVars *data = static_cast<TextVars *>(seq->effectdata);
r_runtime.font = font;
r_runtime.line_height = BLF_height_max(font);
r_runtime.font_descender = BLF_descender(font);
r_runtime.character_count = BLI_strlen_utf8(data->text);
if (data->runtime != nullptr) {
MEM_delete(data->runtime);
}
data->runtime = MEM_new<TextVarsRuntime>(__func__);
TextVarsRuntime *runtime = data->runtime;
runtime->font = font;
runtime->line_height = BLF_height_max(font);
runtime->font_descender = BLF_descender(font);
runtime->character_count = BLI_strlen_utf8(data->text);
blender::Vector<CharInfo> characters_temp = build_character_info(data, font);
apply_word_wrapping(data, r_runtime, image_size, characters_temp);
apply_text_alignment(data, r_runtime, image_size);
apply_word_wrapping(data, runtime, image_size, characters_temp);
apply_text_alignment(data, runtime, image_size);
calc_boundbox(data, runtime, image_size);
}
static ImBuf *do_text_effect(const SeqRenderData *context,
@@ -3454,8 +3477,8 @@ static ImBuf *do_text_effect(const SeqRenderData *context,
const int font = text_effect_font_init(context, seq, font_flags);
TextVarsRuntime runtime;
calc_text_runtime(seq, font, {out->x, out->y}, runtime);
calc_text_runtime(seq, font, {out->x, out->y});
TextVarsRuntime *runtime = data->runtime;
rcti outline_rect = draw_text_outline(context, data, runtime, display, out);
BLF_buffer(font, nullptr, out->byte_buffer.data, out->x, out->y, display);
@@ -3465,17 +3488,17 @@ static ImBuf *do_text_effect(const SeqRenderData *context,
/* Draw shadow. */
if (data->flag & SEQ_TEXT_SHADOW) {
draw_text_shadow(context, data, runtime.line_height, outline_rect, out);
draw_text_shadow(context, data, runtime->line_height, outline_rect, out);
}
/* Draw box under text. */
if (data->flag & SEQ_TEXT_BOX) {
if (out->byte_buffer.data) {
const int margin = data->box_margin * out->x;
const int minx = runtime.text_boundbox.xmin - margin;
const int maxx = runtime.text_boundbox.xmax + margin;
const int miny = runtime.text_boundbox.ymin - margin;
const int maxy = runtime.text_boundbox.ymax + margin;
const int minx = runtime->text_boundbox.xmin - margin;
const int maxx = runtime->text_boundbox.xmax + margin;
const int miny = runtime->text_boundbox.ymin - margin;
const int maxy = runtime->text_boundbox.ymax + margin;
float corner_radius = data->box_roundness * (maxy - miny) / 2.0f;
fill_rect_alpha_under(out, data->box_color, minx, miny, maxx, maxy, corner_radius);
}

View File

@@ -871,6 +871,7 @@ static bool seq_read_data_cb(Sequence *seq, void *user_data)
if (seq->type == SEQ_TYPE_TEXT) {
TextVars *t = static_cast<TextVars *>(seq->effectdata);
t->text_blf_id = SEQ_FONT_NOT_LOADED;
t->runtime = nullptr;
}
BLO_read_struct(reader, IDProperty, &seq->prop);

View File

@@ -8,6 +8,7 @@
* \ingroup bke
*/
#include "BLI_math_matrix_types.hh"
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
@@ -16,6 +17,7 @@
#include "BLI_math_rotation.h"
#include "BLI_math_vector.h"
#include "BLI_math_vector_types.hh"
#include "BLI_rect.h"
#include "SEQ_animation.hh"
#include "SEQ_channels.hh"
@@ -642,6 +644,29 @@ void SEQ_image_transform_origin_offset_pixelspace_get(const Scene *scene,
mul_v2_v2(r_origin, viewport_pixel_aspect);
}
void SEQ_image_transform_matrix_get(const Scene *scene,
const Sequence *seq,
float r_transform_matrix[4][4])
{
float image_size[2] = {float(scene->r.xsch), float(scene->r.ysch)};
if (ELEM(seq->type, SEQ_TYPE_MOVIE, SEQ_TYPE_IMAGE)) {
image_size[0] = seq->strip->stripdata->orig_width;
image_size[1] = seq->strip->stripdata->orig_height;
}
StripTransform *transform = seq->strip->transform;
float rotation_matrix[3][3];
axis_angle_to_mat3_single(rotation_matrix, 'Z', transform->rotation);
loc_rot_size_to_mat4(r_transform_matrix,
blender::float3{transform->xofs, transform->yofs, 0.0f},
rotation_matrix,
blender::float3{transform->scale_x, transform->scale_y, 1.0f});
const float origin[2] = {image_size[0] * transform->origin[0],
image_size[1] * transform->origin[1]};
const float pivot[3] = {origin[0] - (image_size[0] / 2), origin[1] - (image_size[1] / 2), 0.0f};
transform_pivot_set_m4(r_transform_matrix, pivot);
}
static void seq_image_transform_quad_get_ex(const Scene *scene,
const Sequence *seq,
bool apply_rotation,