diff options
33 files changed, 2087 insertions, 685 deletions
diff --git a/doc/doxygen/Doxyfile b/doc/doxygen/Doxyfile index c07a80bf0d5..ecd60957f2b 100644 --- a/doc/doxygen/Doxyfile +++ b/doc/doxygen/Doxyfile @@ -38,7 +38,7 @@ PROJECT_NAME = Blender # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = "V2.82" +PROJECT_NUMBER = "V2.83" # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/intern/cycles/device/device_optix.cpp b/intern/cycles/device/device_optix.cpp index 979ea7dba23..0a79bd1232b 100644 --- a/intern/cycles/device/device_optix.cpp +++ b/intern/cycles/device/device_optix.cpp @@ -1188,8 +1188,8 @@ class OptiXDevice : public Device { out_data, sizes.outputSizeInBytes, &out_handle, - &compacted_size_prop, - 1)); + background ? &compacted_size_prop : NULL, + background ? 1 : 0)); // Wait for all operations to finish check_result_cuda_ret(cuStreamSynchronize(NULL)); diff --git a/release/scripts/modules/rna_prop_ui.py b/release/scripts/modules/rna_prop_ui.py index 202fd865723..8dda8c90f85 100644 --- a/release/scripts/modules/rna_prop_ui.py +++ b/release/scripts/modules/rna_prop_ui.py @@ -277,14 +277,15 @@ def draw(layout, context, context_member, property_type, use_edit=True): else: val_draw = val - row = flow.row(align=True) + row = layout.row(align=True) box = row.box() if use_edit: split = box.split(factor=0.75) row = split.row(align=True) else: - row = box.row(align=True) + split = box.split(factor=1.00) + row = split.row(align=True) row.alignment = 'RIGHT' diff --git a/source/blender/blenkernel/BKE_animsys.h b/source/blender/blenkernel/BKE_animsys.h index 963e3158d46..9da17d777cd 100644 --- a/source/blender/blenkernel/BKE_animsys.h +++ b/source/blender/blenkernel/BKE_animsys.h @@ -249,6 +249,10 @@ typedef enum eAnimData_Recalc { ADT_RECALC_ALL = (ADT_RECALC_DRIVERS | ADT_RECALC_ANIM), } eAnimData_Recalc; +bool BKE_animsys_store_rna_setting(struct PointerRNA *ptr, + const char *rna_path, + const int array_index, + struct PathResolvedRNA *r_result); bool BKE_animsys_read_rna_setting(struct PathResolvedRNA *anim_rna, float *r_value); bool BKE_animsys_write_rna_setting(struct PathResolvedRNA *anim_rna, const float value); diff --git a/source/blender/blenkernel/BKE_blender_version.h b/source/blender/blenkernel/BKE_blender_version.h index 75e14b7efca..dd3e381ef5d 100644 --- a/source/blender/blenkernel/BKE_blender_version.h +++ b/source/blender/blenkernel/BKE_blender_version.h @@ -26,8 +26,8 @@ * * \note Use #STRINGIFY() rather than defining with quotes. */ -#define BLENDER_VERSION 282 -#define BLENDER_SUBVERSION 6 +#define BLENDER_VERSION 283 +#define BLENDER_SUBVERSION 0 /** Several breakages with 280, e.g. collections vs layers. */ #define BLENDER_MINVERSION 280 #define BLENDER_MINSUBVERSION 0 @@ -36,7 +36,7 @@ /** Can be left blank, otherwise a,b,c... etc with no quotes. */ #define BLENDER_VERSION_CHAR /** alpha/beta/rc/release, docs use this. */ -#define BLENDER_VERSION_CYCLE beta +#define BLENDER_VERSION_CYCLE alpha /** Optionally set to 1,2,... for example to get alpha1 or rc2. */ #define BLENDER_VERSION_CYCLE_NUMBER diff --git a/source/blender/blenkernel/intern/anim_sys.c b/source/blender/blenkernel/intern/anim_sys.c index 32420e2e894..be6622e5d42 100644 --- a/source/blender/blenkernel/intern/anim_sys.c +++ b/source/blender/blenkernel/intern/anim_sys.c @@ -1665,11 +1665,11 @@ void BKE_keyingsets_free(ListBase *list) /* ***************************************** */ /* Evaluation Data-Setting Backend */ -static bool animsys_store_rna_setting(PointerRNA *ptr, - /* typically 'fcu->rna_path', 'fcu->array_index' */ - const char *rna_path, - const int array_index, - PathResolvedRNA *r_result) +bool BKE_animsys_store_rna_setting(PointerRNA *ptr, + /* typically 'fcu->rna_path', 'fcu->array_index' */ + const char *rna_path, + const int array_index, + PathResolvedRNA *r_result) { bool success = false; const char *path = rna_path; @@ -1880,7 +1880,7 @@ static void animsys_write_orig_anim_rna(PointerRNA *ptr, } PathResolvedRNA orig_anim_rna; /* TODO(sergey): Should be possible to cache resolved path in dependency graph somehow. */ - if (animsys_store_rna_setting(&ptr_orig, rna_path, array_index, &orig_anim_rna)) { + if (BKE_animsys_store_rna_setting(&ptr_orig, rna_path, array_index, &orig_anim_rna)) { BKE_animsys_write_rna_setting(&orig_anim_rna, value); } } @@ -1910,7 +1910,7 @@ static void animsys_evaluate_fcurves(PointerRNA *ptr, continue; } PathResolvedRNA anim_rna; - if (animsys_store_rna_setting(ptr, fcu->rna_path, fcu->array_index, &anim_rna)) { + if (BKE_animsys_store_rna_setting(ptr, fcu->rna_path, fcu->array_index, &anim_rna)) { const float curval = calculate_fcurve(&anim_rna, fcu, ctime); BKE_animsys_write_rna_setting(&anim_rna, curval); if (flush_to_original) { @@ -1944,7 +1944,7 @@ static void animsys_evaluate_drivers(PointerRNA *ptr, AnimData *adt, float ctime * NOTE: for 'layering' option later on, we should check if we should remove old value * before adding new to only be done when drivers only changed. */ PathResolvedRNA anim_rna; - if (animsys_store_rna_setting(ptr, fcu->rna_path, fcu->array_index, &anim_rna)) { + if (BKE_animsys_store_rna_setting(ptr, fcu->rna_path, fcu->array_index, &anim_rna)) { const float curval = calculate_fcurve(&anim_rna, fcu, ctime); ok = BKE_animsys_write_rna_setting(&anim_rna, curval); } @@ -2023,7 +2023,7 @@ void animsys_evaluate_action_group(PointerRNA *ptr, bAction *act, bActionGroup * /* check if this curve should be skipped */ if ((fcu->flag & (FCURVE_MUTED | FCURVE_DISABLED)) == 0 && !BKE_fcurve_is_empty(fcu)) { PathResolvedRNA anim_rna; - if (animsys_store_rna_setting(ptr, fcu->rna_path, fcu->array_index, &anim_rna)) { + if (BKE_animsys_store_rna_setting(ptr, fcu->rna_path, fcu->array_index, &anim_rna)) { const float curval = calculate_fcurve(&anim_rna, fcu, ctime); BKE_animsys_write_rna_setting(&anim_rna, curval); } @@ -3803,7 +3803,7 @@ static void animsys_evaluate_overrides(PointerRNA *ptr, AnimData *adt) /* for each override, simply execute... */ for (aor = adt->overrides.first; aor; aor = aor->next) { PathResolvedRNA anim_rna; - if (animsys_store_rna_setting(ptr, aor->rna_path, aor->array_index, &anim_rna)) { + if (BKE_animsys_store_rna_setting(ptr, aor->rna_path, aor->array_index, &anim_rna)) { BKE_animsys_write_rna_setting(&anim_rna, aor->value); } } @@ -4125,7 +4125,7 @@ void BKE_animsys_eval_driver(Depsgraph *depsgraph, ID *id, int driver_index, FCu // printf("\told val = %f\n", fcu->curval); PathResolvedRNA anim_rna; - if (animsys_store_rna_setting(&id_ptr, fcu->rna_path, fcu->array_index, &anim_rna)) { + if (BKE_animsys_store_rna_setting(&id_ptr, fcu->rna_path, fcu->array_index, &anim_rna)) { /* Evaluate driver, and write results to COW-domain destination */ const float ctime = DEG_get_ctime(depsgraph); const float curval = calculate_fcurve(&anim_rna, fcu, ctime); diff --git a/source/blender/depsgraph/CMakeLists.txt b/source/blender/depsgraph/CMakeLists.txt index 4abeec19645..7f3c7d5043f 100644 --- a/source/blender/depsgraph/CMakeLists.txt +++ b/source/blender/depsgraph/CMakeLists.txt @@ -59,6 +59,7 @@ set(SRC intern/eval/deg_eval_copy_on_write.cc intern/eval/deg_eval_flush.cc intern/eval/deg_eval_runtime_backup.cc + intern/eval/deg_eval_runtime_backup_animation.cc intern/eval/deg_eval_runtime_backup_modifier.cc intern/eval/deg_eval_runtime_backup_movieclip.cc intern/eval/deg_eval_runtime_backup_object.cc @@ -108,6 +109,7 @@ set(SRC intern/eval/deg_eval_copy_on_write.h intern/eval/deg_eval_flush.h intern/eval/deg_eval_runtime_backup.h + intern/eval/deg_eval_runtime_backup_animation.h intern/eval/deg_eval_runtime_backup_modifier.h intern/eval/deg_eval_runtime_backup_movieclip.h intern/eval/deg_eval_runtime_backup_object.h diff --git a/source/blender/depsgraph/intern/builder/deg_builder_relations.cc b/source/blender/depsgraph/intern/builder/deg_builder_relations.cc index 3e0ab9684da..31c1b0361f8 100644 --- a/source/blender/depsgraph/intern/builder/deg_builder_relations.cc +++ b/source/blender/depsgraph/intern/builder/deg_builder_relations.cc @@ -2674,6 +2674,26 @@ void DepsgraphRelationBuilder::build_copy_on_write_relations(IDNode *id_node) BLI_assert(object->type == OB_EMPTY); } } + +#if 0 + /* NOTE: Relation is disabled since AnimationBackup() is disabled. + * See comment in AnimationBackup:init_from_id(). */ + + /* Copy-on-write of write will iterate over f-curves to store current values corresponding + * to their RNA path. This means that action must be copied prior to the ID's copy-on-write, + * otherwise depsgraph might try to access freed data. */ + AnimData *animation_data = BKE_animdata_from_id(id_orig); + if (animation_data != NULL) { + if (animation_data->action != NULL) { + OperationKey action_copy_on_write_key( + &animation_data->action->id, NodeType::COPY_ON_WRITE, OperationCode::COPY_ON_WRITE); + add_relation(action_copy_on_write_key, + copy_on_write_key, + "Eval Order", + RELATION_FLAG_GODMODE | RELATION_FLAG_NO_FLUSH); + } + } +#endif } /* **** ID traversal callbacks functions **** */ diff --git a/source/blender/depsgraph/intern/eval/deg_eval_runtime_backup.cc b/source/blender/depsgraph/intern/eval/deg_eval_runtime_backup.cc index 88390ab412f..4da5ca77fb8 100644 --- a/source/blender/depsgraph/intern/eval/deg_eval_runtime_backup.cc +++ b/source/blender/depsgraph/intern/eval/deg_eval_runtime_backup.cc @@ -32,7 +32,8 @@ namespace DEG { RuntimeBackup::RuntimeBackup(const Depsgraph *depsgraph) - : scene_backup(depsgraph), + : animation_backup(depsgraph), + scene_backup(depsgraph), sound_backup(depsgraph), object_backup(depsgraph), drawdata_ptr(NULL), @@ -47,6 +48,8 @@ void RuntimeBackup::init_from_id(ID *id) return; } + animation_backup.init_from_id(id); + const ID_Type id_type = GS(id->name); switch (id_type) { case ID_OB: @@ -76,6 +79,8 @@ void RuntimeBackup::init_from_id(ID *id) void RuntimeBackup::restore_to_id(ID *id) { + animation_backup.restore_to_id(id); + const ID_Type id_type = GS(id->name); switch (id_type) { case ID_OB: diff --git a/source/blender/depsgraph/intern/eval/deg_eval_runtime_backup.h b/source/blender/depsgraph/intern/eval/deg_eval_runtime_backup.h index 31ae3164e37..892cc88002f 100644 --- a/source/blender/depsgraph/intern/eval/deg_eval_runtime_backup.h +++ b/source/blender/depsgraph/intern/eval/deg_eval_runtime_backup.h @@ -25,6 +25,7 @@ #include "DNA_ID.h" +#include "intern/eval/deg_eval_runtime_backup_animation.h" #include "intern/eval/deg_eval_runtime_backup_movieclip.h" #include "intern/eval/deg_eval_runtime_backup_object.h" #include "intern/eval/deg_eval_runtime_backup_scene.h" @@ -44,6 +45,7 @@ class RuntimeBackup { /* Restore fields to the given ID. */ void restore_to_id(ID *id); + AnimationBackup animation_backup; SceneBackup scene_backup; SoundBackup sound_backup; ObjectRuntimeBackup object_backup; diff --git a/source/blender/depsgraph/intern/eval/deg_eval_runtime_backup_animation.cc b/source/blender/depsgraph/intern/eval/deg_eval_runtime_backup_animation.cc new file mode 100644 index 00000000000..cc4935431d1 --- /dev/null +++ b/source/blender/depsgraph/intern/eval/deg_eval_runtime_backup_animation.cc @@ -0,0 +1,144 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup depsgraph + */ + +#include "intern/eval/deg_eval_runtime_backup_animation.h" + +#include "DNA_anim_types.h" + +#include "BKE_animsys.h" + +#include "RNA_access.h" +#include "RNA_types.h" + +#include "intern/depsgraph.h" + +namespace DEG { + +namespace { + +struct AnimatedPropertyStoreCalbackData { + AnimationBackup *backup; + + /* ID which needs to be stored. + * Is used to check possibly nested IDs which f-curves are pointing to. */ + ID *id; + + PointerRNA id_pointer_rna; +}; + +void animated_property_store_cb(ID *id, FCurve *fcurve, void *data_v) +{ + AnimatedPropertyStoreCalbackData *data = reinterpret_cast<AnimatedPropertyStoreCalbackData *>( + data_v); + if (fcurve->rna_path == NULL || fcurve->rna_path[0] == '\0') { + return; + } + if (id != data->id) { + return; + } + + /* Resolve path to the property. */ + PathResolvedRNA resolved_rna; + if (!BKE_animsys_store_rna_setting( + &data->id_pointer_rna, fcurve->rna_path, fcurve->array_index, &resolved_rna)) { + return; + } + + /* Read property value. */ + float value; + if (!BKE_animsys_read_rna_setting(&resolved_rna, &value)) { + return; + } + + data->backup->values_backup.emplace_back(fcurve->rna_path, fcurve->array_index, value); +} + +} // namespace + +AnimationValueBackup::AnimationValueBackup() +{ +} + +AnimationValueBackup::AnimationValueBackup(const string &rna_path, int array_index, float value) + : rna_path(rna_path), array_index(array_index), value(value) +{ +} + +AnimationValueBackup::~AnimationValueBackup() +{ +} + +AnimationBackup::AnimationBackup(const Depsgraph *depsgraph) +{ + meed_value_backup = !depsgraph->is_active; + reset(); +} + +void AnimationBackup::reset() +{ +} + +void AnimationBackup::init_from_id(ID *id) +{ + /* NOTE: This animation backup nicely preserves values which are animated and + * are not touched by frame/depsgraph post_update handler. + * + * But it makes it impossible to have user edits to animated properties: for + * example, translation of object with animated location will not work with + * the current version of backup. */ + return; + + AnimatedPropertyStoreCalbackData data; + data.backup = this; + data.id = id; + RNA_id_pointer_create(id, &data.id_pointer_rna); + BKE_fcurves_id_cb(id, animated_property_store_cb, &data); +} + +void AnimationBackup::restore_to_id(ID *id) +{ + return; + + PointerRNA id_pointer_rna; + RNA_id_pointer_create(id, &id_pointer_rna); + for (const AnimationValueBackup &value_backup : values_backup) { + /* Resolve path to the property. + * + * NOTE: Do it again (after storing), since the sub-data pointers might be + * changed after copy-on-write. */ + PathResolvedRNA resolved_rna; + if (!BKE_animsys_store_rna_setting(&id_pointer_rna, + value_backup.rna_path.c_str(), + value_backup.array_index, + &resolved_rna)) { + return; + } + + /* Write property value. */ + if (!BKE_animsys_write_rna_setting(&resolved_rna, value_backup.value)) { + return; + } + } +} + +} // namespace DEG diff --git a/source/blender/depsgraph/intern/eval/deg_eval_runtime_backup_animation.h b/source/blender/depsgraph/intern/eval/deg_eval_runtime_backup_animation.h new file mode 100644 index 00000000000..d97ee2b0556 --- /dev/null +++ b/source/blender/depsgraph/intern/eval/deg_eval_runtime_backup_animation.h @@ -0,0 +1,65 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2019 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup depsgraph + */ + +#pragma once + +#include "BKE_modifier.h" + +#include "intern/depsgraph_type.h" + +namespace DEG { + +struct Depsgraph; + +class AnimationValueBackup { + public: + AnimationValueBackup(); + AnimationValueBackup(const string &rna_path, int array_index, float value); + ~AnimationValueBackup(); + + AnimationValueBackup(const AnimationValueBackup &other) = default; + AnimationValueBackup(AnimationValueBackup &&other) noexcept = default; + + AnimationValueBackup &operator=(const AnimationValueBackup &other) = default; + AnimationValueBackup &operator=(AnimationValueBackup &&other) = default; + + string rna_path; + int array_index; + float value; +}; + +/* Backup of animated properties values. */ +class AnimationBackup { + public: + AnimationBackup(const Depsgraph *depsgraph); + + void reset(); + + void init_from_id(ID *id); + void restore_to_id(ID *id); + + bool meed_value_backup; + vector<AnimationValueBackup> values_backup; +}; + +} // namespace DEG diff --git a/source/blender/editors/animation/time_scrub_ui.c b/source/blender/editors/animation/time_scrub_ui.c index 70a9b7ba1fa..ae489fb5233 100644 --- a/source/blender/editors/animation/time_scrub_ui.c +++ b/source/blender/editors/animation/time_scrub_ui.c @@ -179,9 +179,9 @@ void ED_time_scrub_channel_search_draw(const bContext *C, ARegion *ar, bDopeShee rcti rect; rect.xmin = 0; - rect.xmax = ceilf(ar->sizex * UI_DPI_FAC); - rect.ymin = ar->sizey * UI_DPI_FAC - UI_TIME_SCRUB_MARGIN_Y; - rect.ymax = ceilf(ar->sizey * UI_DPI_FAC); + rect.xmax = ar->winx; + rect.ymin = ar->winy - UI_TIME_SCRUB_MARGIN_Y; + rect.ymax = ar->winy; uint pos = GPU_vertformat_attr_add(immVertexFormat(), "pos", GPU_COMP_F32, 2, GPU_FETCH_FLOAT); immBindBuiltinProgram(GPU_SHADER_2D_UNIFORM_COLOR); diff --git a/source/blender/editors/space_clip/space_clip.c b/source/blender/editors/space_clip/space_clip.c index 16305a9b17b..04c939ec41b 100644 --- a/source/blender/editors/space_clip/space_clip.c +++ b/source/blender/editors/space_clip/space_clip.c @@ -1083,11 +1083,7 @@ static void graph_region_draw(const bContext *C, ARegion *ar) /* scale indicators */ { rcti rect; - BLI_rcti_init(&rect, - 0, - 15 * UI_DPI_FAC, - 15 * UI_DPI_FAC, - UI_DPI_FAC * ar->sizey - UI_TIME_SCRUB_MARGIN_Y); + BLI_rcti_init(&rect, 0, 15 * UI_DPI_FAC, 15 * UI_DPI_FAC, ar->winy - UI_TIME_SCRUB_MARGIN_Y); UI_view2d_draw_scale_y__values(ar, v2d, &rect, TH_TEXT); } } diff --git a/source/blender/editors/space_graph/graph_edit.c b/source/blender/editors/space_graph/graph_edit.c index 03df93e4c8a..98fe8c71454 100644 --- a/source/blender/editors/space_graph/graph_edit.c +++ b/source/blender/editors/space_graph/graph_edit.c @@ -298,7 +298,7 @@ static int graphkeys_viewall(bContext *C, if (!BLI_listbase_is_empty(ED_context_get_markers(C))) { pad_bottom = UI_MARKER_MARGIN_Y; } - BLI_rctf_pad_y(&cur_new, ac.ar->sizey * UI_DPI_FAC, pad_bottom, pad_top); + BLI_rctf_pad_y(&cur_new, ac.ar->winy, pad_bottom, pad_top); UI_view2d_smooth_view(C, ac.ar, &cur_new, smooth_viewtx); return OPERATOR_FINISHED; diff --git a/source/blender/editors/space_graph/space_graph.c b/source/blender/editors/space_graph/space_graph.c index 7bc907bb3db..d01e4112fd0 100644 --- a/source/blender/editors/space_graph/space_graph.c +++ b/source/blender/editors/space_graph/space_graph.c @@ -326,11 +326,7 @@ static void graph_main_region_draw(const bContext *C, ARegion *ar) /* scale numbers */ { rcti rect; - BLI_rcti_init(&rect, - 0, - 15 * UI_DPI_FAC, - 15 * UI_DPI_FAC, - UI_DPI_FAC * ar->sizey - UI_TIME_SCRUB_MARGIN_Y); + BLI_rcti_init(&rect, 0, 15 * UI_DPI_FAC, 15 * UI_DPI_FAC, ar->winy - UI_TIME_SCRUB_MARGIN_Y); UI_view2d_draw_scale_y__values(ar, v2d, &rect, TH_SCROLL_TEXT); } } diff --git a/source/blender/editors/space_sequencer/sequencer_draw.c b/source/blender/editors/space_sequencer/sequencer_draw.c index 70cb28fa937..333a51e2eac 100644 --- a/source/blender/editors/space_sequencer/sequencer_draw.c +++ b/source/blender/editors/space_sequencer/sequencer_draw.c @@ -2119,11 +2119,7 @@ void draw_timeline_seq(const bContext *C, ARegion *ar) /* channel numbers */ { rcti rect; - BLI_rcti_init(&rect, - 0, - 15 * UI_DPI_FAC, - 15 * UI_DPI_FAC, - UI_DPI_FAC * ar->sizey - UI_TIME_SCRUB_MARGIN_Y); + BLI_rcti_init(&rect, 0, 15 * UI_DPI_FAC, 15 * UI_DPI_FAC, ar->winy - UI_TIME_SCRUB_MARGIN_Y); UI_view2d_draw_scale_y__block(ar, v2d, &rect, TH_SCROLL_TEXT); } } diff --git a/source/blender/gpu/GPU_texture.h b/source/blender/gpu/GPU_texture.h index a8e8ca72023..19c7386ad6d 100644 --- a/source/blender/gpu/GPU_texture.h +++ b/source/blender/gpu/GPU_texture.h @@ -188,7 +188,6 @@ GPUTexture *GPU_texture_create_buffer(eGPUTextureFormat data_type, const uint bu GPUTexture *GPU_texture_from_bindcode(int textarget, int bindcode); GPUTexture *GPU_texture_from_blender(struct Image *ima, struct ImageUser *iuser, int textarget); -GPUTexture *GPU_texture_from_preview(struct PreviewImage *prv, int mipmap); /* movie clip drawing */ GPUTexture *GPU_texture_from_movieclip(struct MovieClip *clip, diff --git a/source/blender/gpu/intern/gpu_texture.c b/source/blender/gpu/intern/gpu_texture.c index 497fc13a2c8..1feb5b7732d 100644 --- a/source/blender/gpu/intern/gpu_texture.c +++ b/source/blender/gpu/intern/gpu_texture.c @@ -1072,56 +1072,6 @@ GPUTexture *GPU_texture_from_bindcode(int textarget, int bindcode) return tex; } -GPUTexture *GPU_texture_from_preview(PreviewImage *prv, int mipmap) -{ - GPUTexture *tex = prv->gputexture[0]; - GLuint bindcode = 0; - - if (tex) { - bindcode = tex->bindcode; - } - - /* this binds a texture, so that's why we restore it to 0 */ - if (bindcode == 0) { - GPU_create_gl_tex( - &bindcode, prv->rect[0], NULL, prv->w[0], prv->h[0], GL_TEXTURE_2D, mipmap, false, NULL); - } - if (tex) { - tex->bindcode = bindcode; - glBindTexture(GL_TEXTURE_2D, 0); - return tex; - } - - tex = MEM_callocN(sizeof(GPUTexture), "GPUTexture"); - tex->bindcode = bindcode; - tex->number = -1; - tex->refcount = 1; - tex->target = GL_TEXTURE_2D; - tex->target_base = GL_TEXTURE_2D; - tex->format = -1; - tex->components = -1; - - prv->gputexture[0] = tex; - - if (!glIsTexture(tex->bindcode)) { - GPU_print_error_debug("Blender Texture Not Loaded"); - } - else { - GLint w, h; - - glBindTexture(GL_TEXTURE_2D, tex->bindcode); - glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w); - glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &h); - - tex->w = w; - tex->h = h; - } - - glBindTexture(GL_TEXTURE_2D, 0); - - return tex; -} - GPUTexture *GPU_texture_create_1d(int w, eGPUTextureFormat tex_format, const float *pixels, diff --git a/source/blender/makesdna/DNA_screen_types.h b/source/blender/makesdna/DNA_screen_types.h index d3c5a707b44..60fb7b62dff 100644 --- a/source/blender/makesdna/DNA_screen_types.h +++ b/source/blender/makesdna/DNA_screen_types.h @@ -409,7 +409,9 @@ typedef struct ARegion { short flag; /** Current split size in unscaled pixels (if zero it uses regiontype). - * To convert to pixels use: `UI_DPI_FAC * ar->sizex + 0.5f`. */ + * To convert to pixels use: `UI_DPI_FAC * ar->sizex + 0.5f`. + * However to get the current region size, you should usually use winx/winy from above, not this! + */ short sizex, sizey; /** Private, cached notifier events. */ diff --git a/source/blender/windowmanager/CMakeLists.txt b/source/blender/windowmanager/CMakeLists.txt index ab87f81dba5..d84b0f795ec 100644 --- a/source/blender/windowmanager/CMakeLists.txt +++ b/source/blender/windowmanager/CMakeLists.txt @@ -54,6 +54,7 @@ set(SRC intern/wm_cursors.c intern/wm_dragdrop.c intern/wm_draw.c + intern/wm_event_query.c intern/wm_event_system.c intern/wm_files.c intern/wm_files_link.c diff --git a/source/blender/windowmanager/WM_api.h b/source/blender/windowmanager/WM_api.h index d24157a22a6..02685cfb555 100644 --- a/source/blender/windowmanager/WM_api.h +++ b/source/blender/windowmanager/WM_api.h @@ -207,10 +207,6 @@ void WM_cursor_warp(struct wmWindow *win, int x, int y); void WM_cursor_compatible_xy(wmWindow *win, int *x, int *y); float WM_cursor_pressure(const struct wmWindow *win); -/* event map */ -int WM_userdef_event_map(int kmitype); -int WM_userdef_event_type_from_keymap_type(int kmitype); - /* handlers */ typedef bool (*EventHandlerPoll)(const ARegion *ar, const struct wmEvent *event); @@ -250,6 +246,11 @@ wmKeyMapItem *WM_event_match_keymap_item(struct bContext *C, wmKeyMap *keymap, const struct wmEvent *event); +wmKeyMapItem *WM_event_match_keymap_item_from_handlers(struct bContext *C, + struct wmWindowManager *wm, + struct ListBase *handlers, + const struct wmEvent *event); + typedef int (*wmUIHandlerFunc)(struct bContext *C, const struct wmEvent *event, void *userdata); typedef void (*wmUIHandlerRemoveFunc)(struct bContext *C, void *userdata); @@ -294,8 +295,6 @@ struct wmEventHandler_Dropbox *WM_event_add_dropbox_handler(ListBase *handlers, /* mouse */ void WM_event_add_mousemove(const struct bContext *C); -bool WM_event_is_modal_tweak_exit(const struct wmEvent *event, int tweak_event); -bool WM_event_is_last_mousemove(const struct wmEvent *event); #ifdef WITH_INPUT_NDOF /* 3D mouse */ @@ -631,15 +630,9 @@ bool WM_gesture_is_modal_first(const struct wmGesture *gesture); /* fileselecting support */ void WM_event_add_fileselect(struct bContext *C, struct wmOperator *op); void WM_event_fileselect_event(struct wmWindowManager *wm, void *ophandle, int eventval); -int WM_event_modifier_flag(const struct wmEvent *event); -void WM_event_print(const struct wmEvent *event); void WM_operator_region_active_win_set(struct bContext *C); -int WM_event_drag_threshold(const struct wmEvent *event); -bool WM_event_drag_test(const struct wmEvent *event, const int prev_xy[2]); -bool WM_event_drag_test_with_delta(const struct wmEvent *event, const int delta[2]); - /* drag and drop */ struct wmDrag *WM_event_start_drag( struct bContext *C, int icon, int type, void *poin, double value, unsigned int flags); @@ -783,6 +776,36 @@ bool write_crash_blend(void); /* Lock the interface for any communication */ void WM_set_locked_interface(struct wmWindowManager *wm, bool lock); +/* For testing only 'G_FLAG_EVENT_SIMULATE' */ +struct wmEvent *WM_event_add_simulate(struct wmWindow *win, const struct wmEvent *event_to_add); + +const char *WM_window_cursor_keymap_status_get(const struct wmWindow *win, + int button_index, + int type_index); +void WM_window_cursor_keymap_status_refresh(struct bContext *C, struct wmWindow *win); + +void WM_window_status_area_tag_redraw(struct wmWindow *win); +struct ScrArea *WM_window_status_area_find(struct wmWindow *win, struct bScreen *sc); +bool WM_window_modal_keymap_status_draw(struct bContext *C, + struct wmWindow *win, + struct uiLayout *layout); + +/* wm_event_query.c */ +void WM_event_print(const struct wmEvent *event); + +int WM_event_modifier_flag(const struct wmEvent *event); + +bool WM_event_is_modal_tweak_exit(const struct wmEvent *event, int tweak_event); +bool WM_event_is_last_mousemove(const struct wmEvent *event); + +int WM_event_drag_threshold(const struct wmEvent *event); +bool WM_event_drag_test(const struct wmEvent *event, const int prev_xy[2]); +bool WM_event_drag_test_with_delta(const struct wmEvent *event, const int delta[2]); + +/* event map */ +int WM_userdef_event_map(int kmitype); +int WM_userdef_event_type_from_keymap_type(int kmitype); + #ifdef WITH_INPUT_NDOF void WM_event_ndof_pan_get(const struct wmNDOFMotionData *ndof, float r_pan[3], @@ -800,20 +823,6 @@ bool WM_event_is_tablet(const struct wmEvent *event); bool WM_event_is_ime_switch(const struct wmEvent *event); #endif -/* For testing only 'G_FLAG_EVENT_SIMULATE' */ -struct wmEvent *WM_event_add_simulate(struct wmWindow *win, const struct wmEvent *event_to_add); - -const char *WM_window_cursor_keymap_status_get(const struct wmWindow *win, - int button_index, - int type_index); -void WM_window_cursor_keymap_status_refresh(struct bContext *C, struct wmWindow *win); - -void WM_window_status_area_tag_redraw(struct wmWindow *win); -struct ScrArea *WM_window_status_area_find(struct wmWindow *win, struct bScreen *sc); -bool WM_window_modal_keymap_status_draw(struct bContext *C, - struct wmWindow *win, - struct uiLayout *layout); - /* wm_tooltip.c */ typedef struct ARegion *(*wmTooltipInitFn)(struct bContext *C, struct ARegion *ar, diff --git a/source/blender/windowmanager/WM_types.h b/source/blender/windowmanager/WM_types.h index 0c3a5f92113..e9e4b7c6d12 100644 --- a/source/blender/windowmanager/WM_types.h +++ b/source/blender/windowmanager/WM_types.h @@ -548,9 +548,6 @@ typedef struct wmEvent { char check_drag; char is_motion_absolute; - /** Keymap item, set by handler (weak?). */ - const char *keymap_idname; - /** Tablet info, only use when the tablet is active. */ const struct wmTabletData *tablet_data; diff --git a/source/blender/windowmanager/intern/wm_event_query.c b/source/blender/windowmanager/intern/wm_event_query.c new file mode 100644 index 00000000000..86d92a473ff --- /dev/null +++ b/source/blender/windowmanager/intern/wm_event_query.c @@ -0,0 +1,442 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2007 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup wm + * + * Read-only queries utility functions for the event system. + */ + +#include <stdlib.h> +#include <string.h> + +#include "DNA_listBase.h" +#include "DNA_screen_types.h" +#include "DNA_scene_types.h" +#include "DNA_windowmanager_types.h" +#include "DNA_userdef_types.h" + +#include "BLI_blenlib.h" +#include "BLI_utildefines.h" +#include "BLI_math.h" + +#include "BKE_context.h" + +#include "RNA_access.h" + +#include "WM_api.h" +#include "WM_types.h" + +#include "wm_event_system.h" +#include "wm_event_types.h" + +#include "RNA_enum_types.h" + +#include "DEG_depsgraph.h" + +/* -------------------------------------------------------------------- */ +/** \name Event Printing + * \{ */ + +/* for debugging only, getting inspecting events manually is tedious */ +void WM_event_print(const wmEvent *event) +{ + if (event) { + const char *unknown = "UNKNOWN"; + const char *type_id = unknown; + const char *val_id = unknown; + + RNA_enum_identifier(rna_enum_event_type_items, event->type, &type_id); + RNA_enum_identifier(rna_enum_event_value_items, event->val, &val_id); + + printf( + "wmEvent type:%d / %s, val:%d / %s,\n" + " shift:%d, ctrl:%d, alt:%d, oskey:%d, keymodifier:%d,\n" + " mouse:(%d,%d), ascii:'%c', utf8:'%.*s', pointer:%p\n", + event->type, + type_id, + event->val, + val_id, + event->shift, + event->ctrl, + event->alt, + event->oskey, + event->keymodifier, + event->x, + event->y, + event->ascii, + BLI_str_utf8_size(event->utf8_buf), + event->utf8_buf, + (const void *)event); + +#ifdef WITH_INPUT_NDOF + if (ISNDOF(event->type)) { + const wmNDOFMotionData *ndof = event->customdata; + if (event->type == NDOF_MOTION) { + printf(" ndof: rot: (%.4f %.4f %.4f), tx: (%.4f %.4f %.4f), dt: %.4f, progress: %u\n", + UNPACK3(ndof->rvec), + UNPACK3(ndof->tvec), + ndof->dt, + ndof->progress); + } + else { + /* ndof buttons printed already */ + } + } +#endif /* WITH_INPUT_NDOF */ + + if (event->tablet_data) { + const wmTabletData *wmtab = event->tablet_data; + printf(" tablet: active: %d, pressure %.4f, tilt: (%.4f %.4f)\n", + wmtab->Active, + wmtab->Pressure, + wmtab->Xtilt, + wmtab->Ytilt); + } + } + else { + printf("wmEvent - NULL\n"); + } +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Event Modifier/Type Queries + * \{ */ + +int WM_event_modifier_flag(const wmEvent *event) +{ + int flag = 0; + if (event->ctrl) { + flag |= KM_CTRL; + } + if (event->alt) { + flag |= KM_ALT; + } + if (event->shift) { + flag |= KM_SHIFT; + } + if (event->oskey) { + flag |= KM_OSKEY; + } + return flag; +} + +bool WM_event_type_mask_test(const int event_type, const enum eEventType_Mask mask) +{ + /* Keyboard. */ + if (mask & EVT_TYPE_MASK_KEYBOARD) { + if (ISKEYBOARD(event_type)) { + return true; + } + } + else if (mask & EVT_TYPE_MASK_KEYBOARD_MODIFIER) { + if (ISKEYMODIFIER(event_type)) { + return true; + } + } + + /* Mouse. */ + if (mask & EVT_TYPE_MASK_MOUSE) { + if (ISMOUSE(event_type)) { + return true; + } + } + else if (mask & EVT_TYPE_MASK_MOUSE_WHEEL) { + if (ISMOUSE_WHEEL(event_type)) { + return true; + } + } + else if (mask & EVT_TYPE_MASK_MOUSE_GESTURE) { + if (ISMOUSE_GESTURE(event_type)) { + return true; + } + } + + /* Tweak. */ + if (mask & EVT_TYPE_MASK_TWEAK) { + if (ISTWEAK(event_type)) { + return true; + } + } + + /* Action Zone. */ + if (mask & EVT_TYPE_MASK_ACTIONZONE) { + if (IS_EVENT_ACTIONZONE(event_type)) { + return true; + } + } + + return false; +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Event Motion Queries + * \{ */ + +/* for modal callbacks, check configuration for how to interpret exit with tweaks */ +bool WM_event_is_modal_tweak_exit(const wmEvent *event, int tweak_event) +{ + /* if the release-confirm userpref setting is enabled, + * tweak events can be canceled when mouse is released + */ + if (U.flag & USER_RELEASECONFIRM) { + /* option on, so can exit with km-release */ + if (event->val == KM_RELEASE) { + switch (tweak_event) { + case EVT_TWEAK_L: + case EVT_TWEAK_M: + case EVT_TWEAK_R: + return 1; + } + } + else { + /* if the initial event wasn't a tweak event then + * ignore USER_RELEASECONFIRM setting: see [#26756] */ + if (ELEM(tweak_event, EVT_TWEAK_L, EVT_TWEAK_M, EVT_TWEAK_R) == 0) { + return 1; + } + } + } + else { + /* this is fine as long as not doing km-release, otherwise + * some items (i.e. markers) being tweaked may end up getting + * dropped all over + */ + if (event->val != KM_RELEASE) { + return 1; + } + } + + return 0; +} + +bool WM_event_is_last_mousemove(const wmEvent *event) +{ + while ((event = event->next)) { + if (ELEM(event->type, MOUSEMOVE, INBETWEEN_MOUSEMOVE)) { + return false; + } + } + return true; +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Event Click/Drag Checks + * + * Values under this limit are detected as clicks. + * + * \{ */ + +int WM_event_drag_threshold(const struct wmEvent *event) +{ + int drag_threshold; + if (WM_event_is_tablet(event)) { + drag_threshold = U.drag_threshold_tablet; + } + else if (ISMOUSE(event->prevtype)) { + drag_threshold = U.drag_threshold_mouse; + } + else { + /* Typically keyboard, could be NDOF button or other less common types. */ + drag_threshold = U.drag_threshold; + } + return drag_threshold * U.dpi_fac; +} + +bool WM_event_drag_test_with_delta(const wmEvent *event, const int drag_delta[2]) +{ + const int drag_threshold = WM_event_drag_threshold(event); + return abs(drag_delta[0]) > drag_threshold || abs(drag_delta[1]) > drag_threshold; +} + +bool WM_event_drag_test(const wmEvent *event, const int prev_xy[2]) +{ + const int drag_delta[2] = { + prev_xy[0] - event->x, + prev_xy[1] - event->y, + }; + return WM_event_drag_test_with_delta(event, drag_delta); +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Event Preference Mapping + * \{ */ + +int WM_userdef_event_map(int kmitype) +{ + switch (kmitype) { + case WHEELOUTMOUSE: + return (U.uiflag & USER_WHEELZOOMDIR) ? WHEELUPMOUSE : WHEELDOWNMOUSE; + case WHEELINMOUSE: + return (U.uiflag & USER_WHEELZOOMDIR) ? WHEELDOWNMOUSE : WHEELUPMOUSE; + } + + return kmitype; +} + +/** + * Use so we can check if 'wmEvent.type' is released in modal operators. + * + * An alternative would be to add a 'wmEvent.type_nokeymap'... or similar. + */ +int WM_userdef_event_type_from_keymap_type(int kmitype) +{ + switch (kmitype) { + case EVT_TWEAK_L: + return LEFTMOUSE; + case EVT_TWEAK_M: + return MIDDLEMOUSE; + case EVT_TWEAK_R: + return RIGHTMOUSE; + case WHEELOUTMOUSE: + return (U.uiflag & USER_WHEELZOOMDIR) ? WHEELUPMOUSE : WHEELDOWNMOUSE; + case WHEELINMOUSE: + return (U.uiflag & USER_WHEELZOOMDIR) ? WHEELDOWNMOUSE : WHEELUPMOUSE; + } + + return kmitype; +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Event NDOF Input Access + * \{ */ + +#ifdef WITH_INPUT_NDOF + +void WM_event_ndof_pan_get(const wmNDOFMotionData *ndof, float r_pan[3], const bool use_zoom) +{ + int z_flag = use_zoom ? NDOF_ZOOM_INVERT : NDOF_PANZ_INVERT_AXIS; + r_pan[0] = ndof->tvec[0] * ((U.ndof_flag & NDOF_PANX_INVERT_AXIS) ? -1.0f : 1.0f); + r_pan[1] = ndof->tvec[1] * ((U.ndof_flag & NDOF_PANY_INVERT_AXIS) ? -1.0f : 1.0f); + r_pan[2] = ndof->tvec[2] * ((U.ndof_flag & z_flag) ? -1.0f : 1.0f); +} + +void WM_event_ndof_rotate_get(const wmNDOFMotionData *ndof, float r_rot[3]) +{ + r_rot[0] = ndof->rvec[0] * ((U.ndof_flag & NDOF_ROTX_INVERT_AXIS) ? -1.0f : 1.0f); + r_rot[1] = ndof->rvec[1] * ((U.ndof_flag & NDOF_ROTY_INVERT_AXIS) ? -1.0f : 1.0f); + r_rot[2] = ndof->rvec[2] * ((U.ndof_flag & NDOF_ROTZ_INVERT_AXIS) ? -1.0f : 1.0f); +} + +float WM_event_ndof_to_axis_angle(const struct wmNDOFMotionData *ndof, float axis[3]) +{ + float angle; + angle = normalize_v3_v3(axis, ndof->rvec); + + axis[0] = axis[0] * ((U.ndof_flag & NDOF_ROTX_INVERT_AXIS) ? -1.0f : 1.0f); + axis[1] = axis[1] * ((U.ndof_flag & NDOF_ROTY_INVERT_AXIS) ? -1.0f : 1.0f); + axis[2] = axis[2] * ((U.ndof_flag & NDOF_ROTZ_INVERT_AXIS) ? -1.0f : 1.0f); + + return ndof->dt * angle; +} + +void WM_event_ndof_to_quat(const struct wmNDOFMotionData *ndof, float q[4]) +{ + float axis[3]; + float angle; + + angle = WM_event_ndof_to_axis_angle(ndof, axis); + axis_angle_to_quat(q, axis, angle); +} +#endif /* WITH_INPUT_NDOF */ + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Event Tablet Input Access + * \{ */ + +/* applies the global tablet pressure correction curve */ +float wm_pressure_curve(float pressure) +{ + if (U.pressure_threshold_max != 0.0f) { + pressure /= U.pressure_threshold_max; + } + + CLAMP(pressure, 0.0f, 1.0f); + + if (U.pressure_softness != 0.0f) { + pressure = powf(pressure, powf(4.0f, -U.pressure_softness)); + } + + return pressure; +} + +/* if this is a tablet event, return tablet pressure and set *pen_flip + * to 1 if the eraser tool is being used, 0 otherwise */ +float WM_event_tablet_data(const wmEvent *event, int *pen_flip, float tilt[2]) +{ + int erasor = 0; + float pressure = 1; + + if (tilt) { + zero_v2(tilt); + } + + if (event->tablet_data) { + const wmTabletData *wmtab = event->tablet_data; + + erasor = (wmtab->Active == EVT_TABLET_ERASER); + if (wmtab->Active != EVT_TABLET_NONE) { + pressure = wmtab->Pressure; + if (tilt) { + tilt[0] = wmtab->Xtilt; + tilt[1] = wmtab->Ytilt; + } + } + } + + if (pen_flip) { + (*pen_flip) = erasor; + } + + return pressure; +} + +bool WM_event_is_tablet(const struct wmEvent *event) +{ + return (event->tablet_data) ? true : false; +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Event IME Input Access + * \{ */ + +#ifdef WITH_INPUT_IME +/* most os using ctrl/oskey + space to switch ime, avoid added space */ +bool WM_event_is_ime_switch(const struct wmEvent *event) +{ + return event->val == KM_PRESS && event->type == SPACEKEY && + (event->ctrl || event->oskey || event->shift || event->alt); +} +#endif + +/** \} */ diff --git a/source/blender/windowmanager/intern/wm_event_system.c b/source/blender/windowmanager/intern/wm_event_system.c index bc068173327..eebef43c0f6 100644 --- a/source/blender/windowmanager/intern/wm_event_system.c +++ b/source/blender/windowmanager/intern/wm_event_system.c @@ -70,7 +70,6 @@ #include "RNA_access.h" #include "UI_interface.h" -#include "UI_view2d.h" #include "PIL_time.h" @@ -84,8 +83,6 @@ #include "wm_event_system.h" #include "wm_event_types.h" -#include "RNA_enum_types.h" - #include "DEG_depsgraph.h" #include "DEG_depsgraph_query.h" @@ -114,6 +111,8 @@ static int wm_operator_call_internal(bContext *C, const bool poll_only, wmEvent *event); +static bool wm_operator_check_locked_interface(bContext *C, wmOperatorType *ot); + /* -------------------------------------------------------------------- */ /** \name Event Management * \{ */ @@ -682,6 +681,82 @@ static void wm_handler_ui_cancel(bContext *C) /** \} */ /* -------------------------------------------------------------------- */ +/** \name WM Reports + * + * Access to #wmWindowManager.reports + * \{ */ + +/** + * Show the report in the info header. + */ +void WM_report_banner_show(void) +{ + wmWindowManager *wm = G_MAIN->wm.first; + ReportList *wm_reports = &wm->reports; + ReportTimerInfo *rti; + + /* After adding reports to the global list, reset the report timer. */ + WM_event_remove_timer(wm, NULL, wm_reports->reporttimer); + + /* Records time since last report was added */ + wm_reports->reporttimer = WM_event_add_timer(wm, wm->winactive, TIMERREPORT, 0.05); + + rti = MEM_callocN(sizeof(ReportTimerInfo), "ReportTimerInfo"); + wm_reports->reporttimer->customdata = rti; +} + +#ifdef WITH_INPUT_NDOF +void WM_ndof_deadzone_set(float deadzone) +{ + GHOST_setNDOFDeadZone(deadzone); +} +#endif + +static void wm_add_reports(ReportList *reports) +{ + /* if the caller owns them, handle this */ + if (reports->list.first && (reports->flag & RPT_OP_HOLD) == 0) { + wmWindowManager *wm = G_MAIN->wm.first; + + /* add reports to the global list, otherwise they are not seen */ + BLI_movelisttolist(&wm->reports.list, &reports->list); + + WM_report_banner_show(); + } +} + +void WM_report(ReportType type, const char *message) +{ + ReportList reports; + + BKE_reports_init(&reports, RPT_STORE); + BKE_report(&reports, type, message); + + wm_add_reports(&reports); + + BKE_reports_clear(&reports); +} + +void WM_reportf(ReportType type, const char *format, ...) +{ + DynStr *ds; + va_list args; + + ds = BLI_dynstr_new(); + va_start(args, format); + BLI_dynstr_vappendf(ds, format, args); + va_end(args); + + char *str = BLI_dynstr_get_cstring(ds); + WM_report(type, str); + MEM_freeN(str); + + BLI_dynstr_free(ds); +} + +/** \} */ + +/* -------------------------------------------------------------------- */ /** \name Operator Logic * \{ */ @@ -762,164 +837,6 @@ void WM_operator_region_active_win_set(bContext *C) } } -int WM_event_modifier_flag(const wmEvent *event) -{ - int flag = 0; - if (event->ctrl) { - flag |= KM_CTRL; - } - if (event->alt) { - flag |= KM_ALT; - } - if (event->shift) { - flag |= KM_SHIFT; - } - if (event->oskey) { - flag |= KM_OSKEY; - } - return flag; -} - -/* for debugging only, getting inspecting events manually is tedious */ -void WM_event_print(const wmEvent *event) -{ - if (event) { - const char *unknown = "UNKNOWN"; - const char *type_id = unknown; - const char *val_id = unknown; - - RNA_enum_identifier(rna_enum_event_type_items, event->type, &type_id); - RNA_enum_identifier(rna_enum_event_value_items, event->val, &val_id); - - printf( - "wmEvent type:%d / %s, val:%d / %s,\n" - " shift:%d, ctrl:%d, alt:%d, oskey:%d, keymodifier:%d,\n" - " mouse:(%d,%d), ascii:'%c', utf8:'%.*s', keymap_idname:%s, pointer:%p\n", - event->type, - type_id, - event->val, - val_id, - event->shift, - event->ctrl, - event->alt, - event->oskey, - event->keymodifier, - event->x, - event->y, - event->ascii, - BLI_str_utf8_size(event->utf8_buf), - event->utf8_buf, - event->keymap_idname, - (const void *)event); - -#ifdef WITH_INPUT_NDOF - if (ISNDOF(event->type)) { - const wmNDOFMotionData *ndof = event->customdata; - if (event->type == NDOF_MOTION) { - printf(" ndof: rot: (%.4f %.4f %.4f), tx: (%.4f %.4f %.4f), dt: %.4f, progress: %u\n", - UNPACK3(ndof->rvec), - UNPACK3(ndof->tvec), - ndof->dt, - ndof->progress); - } - else { - /* ndof buttons printed already */ - } - } -#endif /* WITH_INPUT_NDOF */ - - if (event->tablet_data) { - const wmTabletData *wmtab = event->tablet_data; - printf(" tablet: active: %d, pressure %.4f, tilt: (%.4f %.4f)\n", - wmtab->Active, - wmtab->Pressure, - wmtab->Xtilt, - wmtab->Ytilt); - } - } - else { - printf("wmEvent - NULL\n"); - } -} - -/** - * Show the report in the info header. - */ -void WM_report_banner_show(void) -{ - wmWindowManager *wm = G_MAIN->wm.first; - ReportList *wm_reports = &wm->reports; - ReportTimerInfo *rti; - - /* After adding reports to the global list, reset the report timer. */ - WM_event_remove_timer(wm, NULL, wm_reports->reporttimer); - - /* Records time since last report was added */ - wm_reports->reporttimer = WM_event_add_timer(wm, wm->winactive, TIMERREPORT, 0.05); - - rti = MEM_callocN(sizeof(ReportTimerInfo), "ReportTimerInfo"); - wm_reports->reporttimer->customdata = rti; -} - -bool WM_event_is_last_mousemove(const wmEvent *event) -{ - while ((event = event->next)) { - if (ELEM(event->type, MOUSEMOVE, INBETWEEN_MOUSEMOVE)) { - return false; - } - } - return true; -} - -#ifdef WITH_INPUT_NDOF -void WM_ndof_deadzone_set(float deadzone) -{ - GHOST_setNDOFDeadZone(deadzone); -} -#endif - -static void wm_add_reports(ReportList *reports) -{ - /* if the caller owns them, handle this */ - if (reports->list.first && (reports->flag & RPT_OP_HOLD) == 0) { - wmWindowManager *wm = G_MAIN->wm.first; - - /* add reports to the global list, otherwise they are not seen */ - BLI_movelisttolist(&wm->reports.list, &reports->list); - - WM_report_banner_show(); - } -} - -void WM_report(ReportType type, const char *message) -{ - ReportList reports; - - BKE_reports_init(&reports, RPT_STORE); - BKE_report(&reports, type, message); - - wm_add_reports(&reports); - - BKE_reports_clear(&reports); -} - -void WM_reportf(ReportType type, const char *format, ...) -{ - DynStr *ds; - va_list args; - - ds = BLI_dynstr_new(); - va_start(args, format); - BLI_dynstr_vappendf(ds, format, args); - va_end(args); - - char *str = BLI_dynstr_get_cstring(ds); - WM_report(type, str); - MEM_freeN(str); - - BLI_dynstr_free(ds); -} - /* (caller_owns_reports == true) when called from python */ static void wm_operator_reports(bContext *C, wmOperator *op, int retval, bool caller_owns_reports) { @@ -1301,105 +1218,6 @@ static void wm_region_mouse_co(bContext *C, wmEvent *event) } } -#if 1 /* may want to disable operator remembering previous state for testing */ - -static bool operator_last_properties_init_impl(wmOperator *op, IDProperty *last_properties) -{ - bool changed = false; - IDPropertyTemplate val = {0}; - IDProperty *replaceprops = IDP_New(IDP_GROUP, &val, "wmOperatorProperties"); - PropertyRNA *iterprop; - - CLOG_INFO(WM_LOG_OPERATORS, 1, "loading previous properties for '%s'", op->type->idname); - - iterprop = RNA_struct_iterator_property(op->type->srna); - - RNA_PROP_BEGIN (op->ptr, itemptr, iterprop) { - PropertyRNA *prop = itemptr.data; - if ((RNA_property_flag(prop) & PROP_SKIP_SAVE) == 0) { - if (!RNA_property_is_set(op->ptr, prop)) { /* don't override a setting already set */ - const char *identifier = RNA_property_identifier(prop); - IDProperty *idp_src = IDP_GetPropertyFromGroup(last_properties, identifier); - if (idp_src) { - IDProperty *idp_dst = IDP_CopyProperty(idp_src); - - /* note - in the future this may need to be done recursively, - * but for now RNA doesn't access nested operators */ - idp_dst->flag |= IDP_FLAG_GHOST; - - /* add to temporary group instead of immediate replace, - * because we are iterating over this group */ - IDP_AddToGroup(replaceprops, idp_dst); - changed = true; - } - } - } - } - RNA_PROP_END; - - IDP_MergeGroup(op->properties, replaceprops, true); - IDP_FreeProperty(replaceprops); - return changed; -} - -bool WM_operator_last_properties_init(wmOperator *op) -{ - bool changed = false; - if (op->type->last_properties) { - changed |= operator_last_properties_init_impl(op, op->type->last_properties); - for (wmOperator *opm = op->macro.first; opm; opm = opm->next) { - IDProperty *idp_src = IDP_GetPropertyFromGroup(op->type->last_properties, opm->idname); - if (idp_src) { - changed |= operator_last_properties_init_impl(opm, idp_src); - } - } - } - return changed; -} - -bool WM_operator_last_properties_store(wmOperator *op) -{ - if (op->type->last_properties) { - IDP_FreeProperty(op->type->last_properties); - op->type->last_properties = NULL; - } - - if (op->properties) { - CLOG_INFO(WM_LOG_OPERATORS, 1, "storing properties for '%s'", op->type->idname); - op->type->last_properties = IDP_CopyProperty(op->properties); - } - - if (op->macro.first != NULL) { - for (wmOperator *opm = op->macro.first; opm; opm = opm->next) { - if (opm->properties) { - if (op->type->last_properties == NULL) { - op->type->last_properties = IDP_New( - IDP_GROUP, &(IDPropertyTemplate){0}, "wmOperatorProperties"); - } - IDProperty *idp_macro = IDP_CopyProperty(opm->properties); - STRNCPY(idp_macro->name, opm->type->idname); - IDP_ReplaceInGroup(op->type->last_properties, idp_macro); - } - } - } - - return (op->type->last_properties != NULL); -} - -#else - -bool WM_operator_last_properties_init(wmOperator *UNUSED(op)) -{ - return false; -} - -bool WM_operator_last_properties_store(wmOperator *UNUSED(op)) -{ - return false; -} - -#endif - /** * Also used for exec when 'event' is NULL. */ @@ -1954,42 +1772,6 @@ void WM_event_remove_handlers(bContext *C, ListBase *handlers) } } -/* do userdef mappings */ -int WM_userdef_event_map(int kmitype) -{ - switch (kmitype) { - case WHEELOUTMOUSE: - return (U.uiflag & USER_WHEELZOOMDIR) ? WHEELUPMOUSE : WHEELDOWNMOUSE; - case WHEELINMOUSE: - return (U.uiflag & USER_WHEELZOOMDIR) ? WHEELDOWNMOUSE : WHEELUPMOUSE; - } - - return kmitype; -} - -/** - * Use so we can check if 'wmEvent.type' is released in modal operators. - * - * An alternative would be to add a 'wmEvent.type_nokeymap'... or similar. - */ -int WM_userdef_event_type_from_keymap_type(int kmitype) -{ - switch (kmitype) { - case EVT_TWEAK_L: - return LEFTMOUSE; - case EVT_TWEAK_M: - return MIDDLEMOUSE; - case EVT_TWEAK_R: - return RIGHTMOUSE; - case WHEELOUTMOUSE: - return (U.uiflag & USER_WHEELZOOMDIR) ? WHEELUPMOUSE : WHEELDOWNMOUSE; - case WHEELINMOUSE: - return (U.uiflag & USER_WHEELZOOMDIR) ? WHEELDOWNMOUSE : WHEELUPMOUSE; - } - - return kmitype; -} - static bool wm_eventmatch(const wmEvent *winevent, const wmKeyMapItem *kmi) { if (kmi->flag & KMI_INACTIVE) { @@ -2103,10 +1885,10 @@ static wmKeyMapItem *wm_eventmatch_modal_keymap_items(const wmKeyMap *keymap, * This is done since we only want to use double click events to match key-map items, * allowing modal functions to check for press/release events without having to interpret them. */ -static void wm_event_modalkeymap(const bContext *C, - wmOperator *op, - wmEvent *event, - bool *dbl_click_disabled) +static void wm_event_modalkeymap_begin(const bContext *C, + wmOperator *op, + wmEvent *event, + bool *dbl_click_disabled) { BLI_assert(event->type != EVT_MODAL_MAP); @@ -2159,25 +1941,13 @@ static void wm_event_modalkeymap(const bContext *C, } /** - * Check whether operator is allowed to run in case interface is locked, - * If interface is unlocked, will always return truth. + * Restore changes from #wm_event_modalkeymap_begin + * + * \warning bad hacking event system... + * better restore event type for checking of #KM_CLICK for example. + * Modal maps could use different method (ton). */ -static bool wm_operator_check_locked_interface(bContext *C, wmOperatorType *ot) -{ - wmWindowManager *wm = CTX_wm_manager(C); - - if (wm->is_interface_locked) { - if ((ot->flag & OPTYPE_LOCK_BYPASS) == 0) { - return false; - } - } - - return true; -} - -/* bad hacking event system... better restore event type for checking of KM_CLICK for example */ -/* XXX modal maps could use different method (ton) */ -static void wm_event_modalmap_end(wmEvent *event, bool dbl_click_disabled) +static void wm_event_modalkeymap_end(wmEvent *event, bool dbl_click_disabled) { if (event->type == EVT_MODAL_MAP) { event->type = event->prevtype; @@ -2196,7 +1966,8 @@ static int wm_handler_operator_call(bContext *C, ListBase *handlers, wmEventHandler *handler_base, wmEvent *event, - PointerRNA *properties) + PointerRNA *properties, + const char *kmi_idname) { int retval = OPERATOR_PASS_THROUGH; @@ -2221,7 +1992,7 @@ static int wm_handler_operator_call(bContext *C, wm_handler_op_context(C, handler, event); wm_region_mouse_co(C, event); - wm_event_modalkeymap(C, op, event, &dbl_click_disabled); + wm_event_modalkeymap_begin(C, op, event, &dbl_click_disabled); if (ot->flag & OPTYPE_UNDO) { wm->op_undo_depth++; @@ -2236,7 +2007,7 @@ static int wm_handler_operator_call(bContext *C, * the event, operator etc have all been freed. - campbell */ if (CTX_wm_manager(C) == wm) { - wm_event_modalmap_end(event, dbl_click_disabled); + wm_event_modalkeymap_end(event, dbl_click_disabled); if (ot->flag & OPTYPE_UNDO) { wm->op_undo_depth--; @@ -2298,7 +2069,7 @@ static int wm_handler_operator_call(bContext *C, } } else { - wmOperatorType *ot = WM_operatortype_find(event->keymap_idname, 0); + wmOperatorType *ot = WM_operatortype_find(kmi_idname, 0); if (ot && wm_operator_check_locked_interface(C, ot)) { bool use_last_properties = true; @@ -2636,10 +2407,8 @@ static int wm_handlers_do_keymap_with_keymap_handler( PRINT("%s: item matched '%s'\n", __func__, kmi->idname); - /* weak, but allows interactive callback to not use rawkey */ - event->keymap_idname = kmi->idname; - - action |= wm_handler_operator_call(C, handlers, &handler->head, event, kmi->ptr); + action |= wm_handler_operator_call( + C, handlers, &handler->head, event, kmi->ptr, kmi->idname); if (action & WM_HANDLER_BREAK) { /* not always_pass here, it denotes removed handler_base */ @@ -2693,13 +2462,11 @@ static int wm_handlers_do_keymap_with_gizmo_handler( if (wm_eventmatch(event, kmi)) { PRINT("%s: item matched '%s'\n", __func__, kmi->idname); - /* weak, but allows interactive callback to not use rawkey */ - event->keymap_idname = kmi->idname; - CTX_wm_gizmo_group_set(C, gzgroup); /* handler->op is called later, we want keymap op to be triggered here */ - action |= wm_handler_operator_call(C, handlers, &handler->head, event, kmi->ptr); + action |= wm_handler_operator_call( + C, handlers, &handler->head, event, kmi->ptr, kmi->idname); CTX_wm_gizmo_group_set(C, NULL); @@ -2990,7 +2757,7 @@ static int wm_handlers_do_intern(bContext *C, wmEvent *event, ListBase *handlers } } else { - action |= wm_handler_operator_call(C, handlers, handler_base, event, NULL); + action |= wm_handler_operator_call(C, handlers, handler_base, event, NULL, NULL); } } else { @@ -4099,91 +3866,6 @@ void WM_event_add_mousemove(const bContext *C) window->addmousemove = 1; } -/* for modal callbacks, check configuration for how to interpret exit with tweaks */ -bool WM_event_is_modal_tweak_exit(const wmEvent *event, int tweak_event) -{ - /* if the release-confirm userpref setting is enabled, - * tweak events can be canceled when mouse is released - */ - if (U.flag & USER_RELEASECONFIRM) { - /* option on, so can exit with km-release */ - if (event->val == KM_RELEASE) { - switch (tweak_event) { - case EVT_TWEAK_L: - case EVT_TWEAK_M: - case EVT_TWEAK_R: - return 1; - } - } - else { - /* if the initial event wasn't a tweak event then - * ignore USER_RELEASECONFIRM setting: see [#26756] */ - if (ELEM(tweak_event, EVT_TWEAK_L, EVT_TWEAK_M, EVT_TWEAK_R) == 0) { - return 1; - } - } - } - else { - /* this is fine as long as not doing km-release, otherwise - * some items (i.e. markers) being tweaked may end up getting - * dropped all over - */ - if (event->val != KM_RELEASE) { - return 1; - } - } - - return 0; -} - -bool WM_event_type_mask_test(const int event_type, const enum eEventType_Mask mask) -{ - /* Keyboard. */ - if (mask & EVT_TYPE_MASK_KEYBOARD) { - if (ISKEYBOARD(event_type)) { - return true; - } - } - else if (mask & EVT_TYPE_MASK_KEYBOARD_MODIFIER) { - if (ISKEYMODIFIER(event_type)) { - return true; - } - } - - /* Mouse. */ - if (mask & EVT_TYPE_MASK_MOUSE) { - if (ISMOUSE(event_type)) { - return true; - } - } - else if (mask & EVT_TYPE_MASK_MOUSE_WHEEL) { - if (ISMOUSE_WHEEL(event_type)) { - return true; - } - } - else if (mask & EVT_TYPE_MASK_MOUSE_GESTURE) { - if (ISMOUSE_GESTURE(event_type)) { - return true; - } - } - - /* Tweak. */ - if (mask & EVT_TYPE_MASK_TWEAK) { - if (ISTWEAK(event_type)) { - return true; - } - } - - /* Action Zone. */ - if (mask & EVT_TYPE_MASK_ACTIONZONE) { - if (IS_EVENT_ACTIONZONE(event_type)) { - return true; - } - } - - return false; -} - /** \} */ /* -------------------------------------------------------------------- */ @@ -4421,22 +4103,6 @@ static void wm_eventemulation(wmEvent *event, bool test_only) } } -/* applies the global tablet pressure correction curve */ -float wm_pressure_curve(float pressure) -{ - if (U.pressure_threshold_max != 0.0f) { - pressure /= U.pressure_threshold_max; - } - - CLAMP(pressure, 0.0f, 1.0f); - - if (U.pressure_softness != 0.0f) { - pressure = powf(pressure, powf(4.0f, -U.pressure_softness)); - } - - return pressure; -} - /* adds customdata to event */ static void update_tablet_data(wmWindow *win, wmEvent *event) { @@ -5003,6 +4669,29 @@ void wm_event_add_ghostevent(wmWindowManager *wm, wmWindow *win, int type, void #endif } +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name WM Interface Locking + * \{ */ + +/** + * Check whether operator is allowed to run in case interface is locked, + * If interface is unlocked, will always return truth. + */ +static bool wm_operator_check_locked_interface(bContext *C, wmOperatorType *ot) +{ + wmWindowManager *wm = CTX_wm_manager(C); + + if (wm->is_interface_locked) { + if ((ot->flag & OPTYPE_LOCK_BYPASS) == 0) { + return false; + } + } + + return true; +} + void WM_set_locked_interface(wmWindowManager *wm, bool lock) { /* This will prevent events from being handled while interface is locked @@ -5024,96 +4713,12 @@ void WM_set_locked_interface(wmWindowManager *wm, bool lock) BKE_spacedata_draw_locks(lock); } -#ifdef WITH_INPUT_NDOF +/** \} */ /* -------------------------------------------------------------------- */ -/** \name NDOF Utility Functions +/** \name Event / Keymap Matching API * \{ */ -void WM_event_ndof_pan_get(const wmNDOFMotionData *ndof, float r_pan[3], const bool use_zoom) -{ - int z_flag = use_zoom ? NDOF_ZOOM_INVERT : NDOF_PANZ_INVERT_AXIS; - r_pan[0] = ndof->tvec[0] * ((U.ndof_flag & NDOF_PANX_INVERT_AXIS) ? -1.0f : 1.0f); - r_pan[1] = ndof->tvec[1] * ((U.ndof_flag & NDOF_PANY_INVERT_AXIS) ? -1.0f : 1.0f); - r_pan[2] = ndof->tvec[2] * ((U.ndof_flag & z_flag) ? -1.0f : 1.0f); -} - -void WM_event_ndof_rotate_get(const wmNDOFMotionData *ndof, float r_rot[3]) -{ - r_rot[0] = ndof->rvec[0] * ((U.ndof_flag & NDOF_ROTX_INVERT_AXIS) ? -1.0f : 1.0f); - r_rot[1] = ndof->rvec[1] * ((U.ndof_flag & NDOF_ROTY_INVERT_AXIS) ? -1.0f : 1.0f); - r_rot[2] = ndof->rvec[2] * ((U.ndof_flag & NDOF_ROTZ_INVERT_AXIS) ? -1.0f : 1.0f); -} - -float WM_event_ndof_to_axis_angle(const struct wmNDOFMotionData *ndof, float axis[3]) -{ - float angle; - angle = normalize_v3_v3(axis, ndof->rvec); - - axis[0] = axis[0] * ((U.ndof_flag & NDOF_ROTX_INVERT_AXIS) ? -1.0f : 1.0f); - axis[1] = axis[1] * ((U.ndof_flag & NDOF_ROTY_INVERT_AXIS) ? -1.0f : 1.0f); - axis[2] = axis[2] * ((U.ndof_flag & NDOF_ROTZ_INVERT_AXIS) ? -1.0f : 1.0f); - - return ndof->dt * angle; -} - -void WM_event_ndof_to_quat(const struct wmNDOFMotionData *ndof, float q[4]) -{ - float axis[3]; - float angle; - - angle = WM_event_ndof_to_axis_angle(ndof, axis); - axis_angle_to_quat(q, axis, angle); -} -#endif /* WITH_INPUT_NDOF */ - -/* if this is a tablet event, return tablet pressure and set *pen_flip - * to 1 if the eraser tool is being used, 0 otherwise */ -float WM_event_tablet_data(const wmEvent *event, int *pen_flip, float tilt[2]) -{ - int erasor = 0; - float pressure = 1; - - if (tilt) { - zero_v2(tilt); - } - - if (event->tablet_data) { - const wmTabletData *wmtab = event->tablet_data; - - erasor = (wmtab->Active == EVT_TABLET_ERASER); - if (wmtab->Active != EVT_TABLET_NONE) { - pressure = wmtab->Pressure; - if (tilt) { - tilt[0] = wmtab->Xtilt; - tilt[1] = wmtab->Ytilt; - } - } - } - - if (pen_flip) { - (*pen_flip) = erasor; - } - - return pressure; -} - -bool WM_event_is_tablet(const struct wmEvent *event) -{ - return (event->tablet_data) ? true : false; -} - -#ifdef WITH_INPUT_IME -/* most os using ctrl/oskey + space to switch ime, avoid added space */ -bool WM_event_is_ime_switch(const struct wmEvent *event) -{ - return event->val == KM_PRESS && event->type == SPACEKEY && - (event->ctrl || event->oskey || event->shift || event->alt); -} -#endif - -/** \} */ - wmKeyMap *WM_event_get_keymap_from_handler(wmWindowManager *wm, wmEventHandler_Keymap *handler) { wmKeyMap *keymap; @@ -5141,10 +4746,10 @@ wmKeyMapItem *WM_event_match_keymap_item(bContext *C, wmKeyMap *keymap, const wm return NULL; } -static wmKeyMapItem *wm_kmi_from_event(bContext *C, - wmWindowManager *wm, - ListBase *handlers, - const wmEvent *event) +wmKeyMapItem *WM_event_match_keymap_item_from_handlers(bContext *C, + wmWindowManager *wm, + ListBase *handlers, + const wmEvent *event) { LISTBASE_FOREACH (wmEventHandler *, handler_base, handlers) { /* during this loop, ui handlers for nested menus can tag multiple handlers free */ @@ -5167,6 +4772,8 @@ static wmKeyMapItem *wm_kmi_from_event(bContext *C, return NULL; } +/** \} */ + /* -------------------------------------------------------------------- */ /** \name Cursor Keymap Status * @@ -5379,7 +4986,7 @@ void WM_window_cursor_keymap_status_refresh(bContext *C, wmWindow *win) wm_eventemulation(&test_event, true); wmKeyMapItem *kmi = NULL; for (int handler_index = 0; handler_index < ARRAY_SIZE(handlers); handler_index++) { - kmi = wm_kmi_from_event(C, wm, handlers[handler_index], &test_event); + kmi = WM_event_match_keymap_item_from_handlers(C, wm, handlers[handler_index], &test_event); if (kmi) { break; } @@ -5475,43 +5082,3 @@ bool WM_window_modal_keymap_status_draw(bContext *UNUSED(C), wmWindow *win, uiLa } /** \} */ - -/* -------------------------------------------------------------------- */ -/** \name Event Click/Drag Checks - * - * Values under this limit are detected as clicks. - * - * \{ */ - -int WM_event_drag_threshold(const struct wmEvent *event) -{ - int drag_threshold; - if (WM_event_is_tablet(event)) { - drag_threshold = U.drag_threshold_tablet; - } - else if (ISMOUSE(event->prevtype)) { - drag_threshold = U.drag_threshold_mouse; - } - else { - /* Typically keyboard, could be NDOF button or other less common types. */ - drag_threshold = U.drag_threshold; - } - return drag_threshold * U.dpi_fac; -} - -bool WM_event_drag_test_with_delta(const wmEvent *event, const int drag_delta[2]) -{ - const int drag_threshold = WM_event_drag_threshold(event); - return abs(drag_delta[0]) > drag_threshold || abs(drag_delta[1]) > drag_threshold; -} - -bool WM_event_drag_test(const wmEvent *event, const int prev_xy[2]) -{ - const int drag_delta[2] = { - prev_xy[0] - event->x, - prev_xy[1] - event->y, - }; - return WM_event_drag_test_with_delta(event, drag_delta); -} - -/** \} */ diff --git a/source/blender/windowmanager/intern/wm_operators.c b/source/blender/windowmanager/intern/wm_operators.c index 4ec62ff6d62..a118347ad9c 100644 --- a/source/blender/windowmanager/intern/wm_operators.c +++ b/source/blender/windowmanager/intern/wm_operators.c @@ -720,6 +720,111 @@ void WM_operator_properties_free(PointerRNA *ptr) /** \} */ /* -------------------------------------------------------------------- */ +/** \name Operator Last Properties API + * \{ */ + +#if 1 /* may want to disable operator remembering previous state for testing */ + +static bool operator_last_properties_init_impl(wmOperator *op, IDProperty *last_properties) +{ + bool changed = false; + IDPropertyTemplate val = {0}; + IDProperty *replaceprops = IDP_New(IDP_GROUP, &val, "wmOperatorProperties"); + PropertyRNA *iterprop; + + CLOG_INFO(WM_LOG_OPERATORS, 1, "loading previous properties for '%s'", op->type->idname); + + iterprop = RNA_struct_iterator_property(op->type->srna); + + RNA_PROP_BEGIN (op->ptr, itemptr, iterprop) { + PropertyRNA *prop = itemptr.data; + if ((RNA_property_flag(prop) & PROP_SKIP_SAVE) == 0) { + if (!RNA_property_is_set(op->ptr, prop)) { /* don't override a setting already set */ + const char *identifier = RNA_property_identifier(prop); + IDProperty *idp_src = IDP_GetPropertyFromGroup(last_properties, identifier); + if (idp_src) { + IDProperty *idp_dst = IDP_CopyProperty(idp_src); + + /* note - in the future this may need to be done recursively, + * but for now RNA doesn't access nested operators */ + idp_dst->flag |= IDP_FLAG_GHOST; + + /* add to temporary group instead of immediate replace, + * because we are iterating over this group */ + IDP_AddToGroup(replaceprops, idp_dst); + changed = true; + } + } + } + } + RNA_PROP_END; + + IDP_MergeGroup(op->properties, replaceprops, true); + IDP_FreeProperty(replaceprops); + return changed; +} + +bool WM_operator_last_properties_init(wmOperator *op) +{ + bool changed = false; + if (op->type->last_properties) { + changed |= operator_last_properties_init_impl(op, op->type->last_properties); + for (wmOperator *opm = op->macro.first; opm; opm = opm->next) { + IDProperty *idp_src = IDP_GetPropertyFromGroup(op->type->last_properties, opm->idname); + if (idp_src) { + changed |= operator_last_properties_init_impl(opm, idp_src); + } + } + } + return changed; +} + +bool WM_operator_last_properties_store(wmOperator *op) +{ + if (op->type->last_properties) { + IDP_FreeProperty(op->type->last_properties); + op->type->last_properties = NULL; + } + + if (op->properties) { + CLOG_INFO(WM_LOG_OPERATORS, 1, "storing properties for '%s'", op->type->idname); + op->type->last_properties = IDP_CopyProperty(op->properties); + } + + if (op->macro.first != NULL) { + for (wmOperator *opm = op->macro.first; opm; opm = opm->next) { + if (opm->properties) { + if (op->type->last_properties == NULL) { + op->type->last_properties = IDP_New( + IDP_GROUP, &(IDPropertyTemplate){0}, "wmOperatorProperties"); + } + IDProperty *idp_macro = IDP_CopyProperty(opm->properties); + STRNCPY(idp_macro->name, opm->type->idname); + IDP_ReplaceInGroup(op->type->last_properties, idp_macro); + } + } + } + + return (op->type->last_properties != NULL); +} + +#else + +bool WM_operator_last_properties_init(wmOperator *UNUSED(op)) +{ + return false; +} + +bool WM_operator_last_properties_store(wmOperator *UNUSED(op)) +{ + return false; +} + +#endif + +/** \} */ + +/* -------------------------------------------------------------------- */ /** \name Default Operator Callbacks * \{ */ diff --git a/source/blender/windowmanager/wm_event_system.h b/source/blender/windowmanager/wm_event_system.h index c53ccda170a..31aa61facb5 100644 --- a/source/blender/windowmanager/wm_event_system.h +++ b/source/blender/windowmanager/wm_event_system.h @@ -148,6 +148,7 @@ void wm_event_do_depsgraph(bContext *C, bool is_after_open_file); void wm_event_do_refresh_wm_and_depsgraph(bContext *C); void wm_event_do_notifiers(bContext *C); +/* wm_event_query.c */ float wm_pressure_curve(float raw_pressure); /* wm_keymap.c */ diff --git a/tests/python/CMakeLists.txt b/tests/python/CMakeLists.txt index 7241c26dfec..b5af3e14237 100644 --- a/tests/python/CMakeLists.txt +++ b/tests/python/CMakeLists.txt @@ -22,6 +22,7 @@ set(USE_EXPERIMENTAL_TESTS FALSE) set(TEST_SRC_DIR ${CMAKE_SOURCE_DIR}/../lib/tests) +set(TEST_PYTHON_DIR ${CMAKE_SOURCE_DIR}/tests/python) set(TEST_OUT_DIR ${CMAKE_BINARY_DIR}/tests) # ugh, any better way to do this on testing only? @@ -126,13 +127,17 @@ add_blender_test( add_blender_test( bmesh_bevel ${TEST_SRC_DIR}/modeling/bevel_regression.blend - --python-text run_tests + --python ${TEST_PYTHON_DIR}/bevel_operator.py + -- + --run-all-tests ) add_blender_test( bmesh_boolean ${TEST_SRC_DIR}/modeling/bool_regression.blend - --python-text run_tests + --python ${TEST_PYTHON_DIR}/boolean_operator.py + -- + --run-all-tests ) add_blender_test( @@ -149,6 +154,24 @@ add_blender_test( --python-text run_tests.py ) +add_blender_test( + modifiers + ${TEST_SRC_DIR}/modeling/modifiers.blend + --python ${TEST_PYTHON_DIR}/modifiers.py + -- + --run-all-tests +) + +# ------------------------------------------------------------------------------ +# OPERATORS TESTS +add_blender_test( + operators + ${TEST_SRC_DIR}/modeling/operators.blend + --python ${TEST_PYTHON_DIR}/operators.py + -- + --run-all-tests +) + # ------------------------------------------------------------------------------ # IO TESTS diff --git a/tests/python/bevel_operator.py b/tests/python/bevel_operator.py new file mode 100644 index 00000000000..f91c208bae3 --- /dev/null +++ b/tests/python/bevel_operator.py @@ -0,0 +1,184 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# To run all tests, use +# BLENDER_VERBOSE=1 blender path/to/bevel_regression.blend --python path/to/bevel_operator.py -- --run_all_tests +# To run one test, use +# BLENDER_VERBOSE=1 blender path/to/bevel_regression.blend --python path/to/bevel_operator.py -- --run_test <index> +# where <index> is the index of the test specified in the list tests. + +import bpy +import os +import sys + +sys.path.append(os.path.dirname(os.path.realpath(__file__))) +from modules.mesh_test import OperatorTest + + +def main(): + tests = [ + # 0 + ['EDGE', {10}, 'Cube_test', 'Cube_result_1', 'bevel', {'offset': 0.2}], + ['EDGE', {10, 7}, 'Cube_test', 'Cube_result_2', 'bevel', {'offset': 0.2, 'offset_type': 'WIDTH'}], + ['EDGE', {8, 10, 7}, 'Cube_test', 'Cube_result_3', 'bevel', {'offset': 0.2, 'offset_type': 'DEPTH'}], + ['EDGE', {10}, 'Cube_test', 'Cube_result_4', 'bevel', {'offset': 0.4, 'segments': 2}], + ['EDGE', {10, 7}, 'Cube_test', 'Cube_result_5', 'bevel', {'offset': 0.4, 'segments': 3}], + # 5 + ['EDGE', {8, 10, 7}, 'Cube_test', 'Cube_result_6', 'bevel', {'offset': 0.4, 'segments': 4}], + ['EDGE', {0, 10, 4, 7}, 'Cube_test', 'Cube_result_7', 'bevel', {'offset': 0.4, 'segments': 5, 'profile': 0.2}], + ['EDGE', {8, 10, 7}, 'Cube_test', 'Cube_result_8', 'bevel', {'offset': 0.4, 'segments': 5, 'profile': 0.25}], + ['EDGE', {8, 10, 7}, 'Cube_test', 'Cube_result_9', 'bevel', {'offset': 0.4, 'segments': 6, 'profile': 0.9}], + ['EDGE', {10, 7}, 'Cube_test', 'Cube_result_10', 'bevel', {'offset': 0.4, 'segments': 4, 'profile': 1.0}], + # 10 + ['EDGE', {8, 10, 7}, 'Cube_test', 'Cube_result_11', 'bevel', {'offset': 0.4, 'segments': 5, 'profile': 1.0}], + ['EDGE', {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, 'Cube_test', 'Cube_result_12', 'bevel', + {'offset': 0.4, 'segments': 8}], + ['EDGE', {5}, 'Pyr4_test', 'Pyr4_result_1', 'bevel', {'offset': 0.2}], + ['EDGE', {2, 5}, 'Pyr4_test', 'Pyr4_result_2', 'bevel', {'offset': 0.2}], + ['EDGE', {2, 3, 5}, 'Pyr4_test', 'Pyr4_result_3', 'bevel', {'offset': 0.2}], + # 15 + ['EDGE', {1, 2, 3, 5}, 'Pyr4_test', 'Pyr4_result_4', 'bevel', {'offset': 0.2}], + ['EDGE', {1, 2, 3, 5}, 'Pyr4_test', 'Pyr4_result_5', 'bevel', {'offset': 0.2, 'segments': 3}], + ['EDGE', {2, 3}, 'Pyr4_test', 'Pyr4_result_6', 'bevel', {'offset': 0.2, 'segments': 2}], + ['EDGE', {1, 2, 3, 5}, 'Pyr4_test', 'Pyr4_result_7', 'bevel', {'offset': 0.2, 'segments': 4, 'profile': 0.15}], + ['VERT', {1}, 'Pyr4_test', 'Pyr4_result_8', 'bevel', {'offset': 0.75, 'segments': 4, 'vertex_only': True}], + # 20 + ['VERT', {1}, 'Pyr4_test', 'Pyr4_result_9', 'bevel', + {'offset': 0.75, 'segments': 3, 'vertex_only': True, 'profile': 0.25}], + ['EDGE', {2, 3}, 'Pyr6_test', 'Pyr6_result_1', 'bevel', {'offset': 0.2}], + ['EDGE', {8, 2, 3}, 'Pyr6_test', 'Pyr6_result_2', 'bevel', {'offset': 0.2, 'segments': 2}], + ['EDGE', {0, 2, 3, 4, 6, 7, 9, 10, 11}, 'Pyr6_test', 'Pyr6_result_3', 'bevel', + {'offset': 0.2, 'segments': 4, 'profile': 0.8}], + ['EDGE', {8, 9, 3, 11}, 'Sept_test', 'Sept_result_1', 'bevel', {'offset': 0.1}], + # 25 + ['EDGE', {8, 9, 11}, 'Sept_test', 'Sept_result_2', 'bevel', {'offset': 0.1, 'offset_type': 'WIDTH'}], + ['EDGE', {2, 8, 9, 12, 13, 14}, 'Saddle_test', 'Saddle_result_1', 'bevel', {'offset': 0.3, 'segments': 5}], + ['VERT', {4}, 'Saddle_test', 'Saddle_result_2', 'bevel', {'offset': 0.6, 'segments': 6, 'vertex_only': True}], + ['EDGE', {2, 5, 8, 11, 14, 18, 21, 24, 27, 30, 34, 37, 40, 43, 46, 50, 53, 56, 59, 62, 112, 113, 114, 115}, + 'Bent_test', 'Bent_result_1', 'bevel', {'offset': 0.2, 'segments': 3}], + ['EDGE', {1, 8, 9, 10, 11}, 'Bentlines_test', 'Bentlines_result_1', 'bevel', {'offset': 0.2, 'segments': 3}], + # 30 + ['EDGE', {26, 12, 20}, 'Flaretop_test', 'Flaretop_result_1', 'bevel', {'offset': 0.4, 'segments': 2}], + ['EDGE', {26, 12, 20}, 'Flaretop_test', 'Flaretop_result_2', 'bevel', + {'offset': 0.4, 'segments': 2, 'profile': 1.0}], + ['FACE', {1, 6, 7, 8, 9, 10, 11, 12}, 'Flaretop_test', 'Flaretop_result_3', 'bevel', + {'offset': 0.4, 'segments': 4}], + ['EDGE', {4, 8, 10, 18, 24}, 'BentL_test', 'BentL_result_1', 'bevel', {'offset': 0.2}], + ['EDGE', {0, 1, 2, 10}, 'Wires_test', 'Wires_test_result_1', 'bevel', {'offset': 0.3}], + # 35 + ['VERT', {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, 'Wires_test', 'Wires_test_result_2', 'bevel', + {'offset': 0.3, 'vertex_only': True}], + ['EDGE', {3, 4, 5}, 'tri', 'tri_result_1', 'bevel', {'offset': 0.2}], + ['EDGE', {3, 4, 5}, 'tri', 'tri_result_2', 'bevel', {'offset': 0.2, 'segments': 2}], + ['EDGE', {3, 4, 5}, 'tri', 'tri_result_3', 'bevel', {'offset': 0.2, 'segments': 3}], + ['EDGE', {3, 4}, 'tri', 'tri_result_4', 'bevel', {'offset': 0.2}], + # 40 + ['EDGE', {3, 4}, 'tri', 'tri_result_5', 'bevel', {'offset': 0.2, 'segments': 2}], + ['VERT', {3}, 'tri', 'tri_result_6', 'bevel', {'offset': 0.2, 'vertex_only': True}], + ['VERT', {3}, 'tri', 'tri_result_7', 'bevel', {'offset': 0.2, 'segments': 2, 'vertex_only': True}], + ['VERT', {3}, 'tri', 'tri_result_8', 'bevel', {'offset': 0.2, 'segments': 3, 'vertex_only': True}], + ['VERT', {1}, 'tri', 'tri_result_9', 'bevel', {'offset': 0.2, 'vertex_only': True}], + # 45 + ['EDGE', {3, 4, 5}, 'tri1gap', 'tri1gap_result_1', 'bevel', {'offset': 0.2}], + ['EDGE', {3, 4, 5}, 'tri1gap', 'tri1gap_result_2', 'bevel', {'offset': 0.2, 'segments': 2}], + ['EDGE', {3, 4, 5}, 'tri1gap', 'tri1gap_result_3', 'bevel', {'offset': 0.2, 'segments': 3}], + ['EDGE', {3, 4}, 'tri1gap', 'tri1gap_result_4', 'bevel', {'offset': 0.2}], + ['EDGE', {3, 4}, 'tri1gap', 'tri1gap_result_5', 'bevel', {'offset': 0.2, 'segments': 2}], + # 50 + ['EDGE', {3, 4}, 'tri1gap', 'tri1gap_result_6', 'bevel', {'offset': 0.2, 'segments': 3}], + ['EDGE', {3, 5}, 'tri1gap', 'tri1gap_result_7', 'bevel', {'offset': 0.2}], + ['EDGE', {3, 5}, 'tri1gap', 'tri1gap_result_8', 'bevel', {'offset': 0.2, 'segments': 2}], + ['EDGE', {3, 5}, 'tri1gap', 'tri1gap_result_9', 'bevel', {'offset': 0.2, 'segments': 3}], + ['VERT', {3}, 'tri1gap', 'tri1gap_result_10', 'bevel', {'offset': 0.2, 'vertex_only': True}], + # 55 + ['EDGE', {3, 4, 5}, 'tri2gaps', 'tri2gaps_result_1', 'bevel', {'offset': 0.2}], + ['EDGE', {3, 4, 5}, 'tri2gaps', 'tri2gaps_result_2', 'bevel', {'offset': 0.2, 'segments': 2}], + ['EDGE', {3, 4, 5}, 'tri2gaps', 'tri2gaps_result_3', 'bevel', {'offset': 0.2, 'segments': 3}], + ['EDGE', {3, 4}, 'tri2gaps', 'tri2gaps_result_4', 'bevel', {'offset': 0.2}], + ['EDGE', {3, 4}, 'tri2gaps', 'tri2gaps_result_5', 'bevel', {'offset': 0.2, 'segments': 2}], + # 60 + ['EDGE', {3, 4}, 'tri2gaps', 'tri2gaps_result_6', 'bevel', {'offset': 0.2, 'segments': 3}], + ['EDGE', {3, 4, 5}, 'tri3gaps', 'tri3gaps_result_1', 'bevel', {'offset': 0.2}], + ['EDGE', {3, 4, 5}, 'tri3gaps', 'tri3gaps_result_2', 'bevel', {'offset': 0.2, 'segments': 2}], + ['EDGE', {3, 4, 5}, 'tri3gaps', 'tri3gaps_result_3', 'bevel', {'offset': 0.2, 'segments': 3}], + ['EDGE', {32, 33, 34, 35, 24, 25, 26, 27, 28, 29, 30, 31}, 'cube3', 'cube3_result_1', 'bevel', {'offset': 0.2}], + # 65 + ['EDGE', {32, 33, 34, 35, 24, 25, 26, 27, 28, 29, 30, 31}, 'cube3', 'cube3_result_2', 'bevel', + {'offset': 0.2, 'segments': 2}], + ['EDGE', {32, 35}, 'cube3', 'cube3_result_3', 'bevel', {'offset': 0.2}], + ['EDGE', {24, 35}, 'cube3', 'cube3_result_4', 'bevel', {'offset': 0.2}], + ['EDGE', {24, 32, 35}, 'cube3', 'cube3_result_5', 'bevel', {'offset': 0.2, 'segments': 2}], + ['EDGE', {24, 32, 35}, 'cube3', 'cube3_result_6', 'bevel', {'offset': 0.2, 'segments': 3}], + # 70 + ['EDGE', {0, 1, 6, 7, 12, 14, 16, 17}, 'Tray', 'Tray_result_1', 'bevel', {'offset': 0.01, 'segments': 2}], + ['EDGE', {33, 4, 38, 8, 41, 10, 42, 12, 14, 17, 24, 31}, 'Bumptop', 'Bumptop_result_1', 'bevel', + {'offset': 0.1, 'segments': 4}], + ['EDGE', {16, 14, 15}, 'Multisegment_test', 'Multisegment_result_1', 'bevel', {'offset': 0.2}], + ['EDGE', {16, 14, 15}, 'Multisegment_test', 'Multisegment_result_1', 'bevel', {'offset': 0.2}], + ['EDGE', {19, 20, 23, 15}, 'Window_test', 'Window_result_1', 'bevel', {'offset': 0.05, 'segments': 2}], + # 75 + ['EDGE', {8}, 'Cube_hn_test', 'Cube_hn_result_1', 'bevel', {'offset': 0.2, 'harden_normals': True}], + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps_test', 'Blocksteps_result_1', 'bevel', + {'offset': 0.2, 'miter_outer': 'PATCH'}], + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps_test', 'Blocksteps_result_2', 'bevel', + {'offset': 0.2, 'segments': 2, 'miter_outer': 'PATCH'}], + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps_test', 'Blocksteps_result_3', 'bevel', + {'offset': 0.2, 'segments': 3, 'miter_outer': 'PATCH'}], + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps_test', 'Blocksteps_result_4', 'bevel', + {'offset': 0.2, 'miter_outer': 'ARC'}], + # 80 + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps_test', 'Blocksteps_result_5', 'bevel', + {'offset': 0.2, 'segments': 2, 'miter_outer': 'ARC'}], + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps_test', 'Blocksteps_result_6', 'bevel', + {'offset': 0.2, 'segments': 3, 'miter_outer': 'ARC'}], + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps_test', 'Blocksteps_result_7', 'bevel', + {'offset': 0.2, 'miter_outer': 'PATCH', 'miter_inner': 'ARC'}], + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps_test', 'Blocksteps_result_8', 'bevel', + {'offset': 0.2, 'segments': 2, 'miter_outer': 'PATCH', 'miter_inner': 'ARC'}], + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps2_test', 'Blocksteps2_result_9', 'bevel', + {'offset': 0.2, 'segments': 2, 'miter_outer': 'ARC'}], + # 85 + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps3_test', 'Blocksteps3_result_10', 'bevel', + {'offset': 0.2, 'segments': 2, 'miter_outer': 'ARC'}], + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps4_test', 'Blocksteps4_result_11', 'bevel', + {'offset': 0.2, 'segments': 2, 'miter_outer': 'ARC'}], + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps4_test', 'Blocksteps4_result_12', 'bevel', + {'offset': 0.2, 'segments': 3, 'miter_outer': 'ARC'}], + ['EDGE', {1, 7}, 'Spike_test', 'Spike_result_1', 'bevel', {'offset': 0.2, 'segments': 3}] + ] + + operator_test = OperatorTest(tests) + + command = list(sys.argv) + for i, cmd in enumerate(command): + if cmd == "--run-all-tests": + operator_test.run_all_tests() + break + elif cmd == "--run-test": + index = int(command[i + 1]) + operator_test.run_test(index) + break + + +if __name__ == "__main__": + try: + main() + except: + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/tests/python/boolean_operator.py b/tests/python/boolean_operator.py new file mode 100644 index 00000000000..8183b527591 --- /dev/null +++ b/tests/python/boolean_operator.py @@ -0,0 +1,68 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +# To run all tests, use +# BLENDER_VERBOSE=1 blender path/to/bool_regression.blend --python path/to/boolean_operator.py -- --run_all_tests +# To run one test, use +# BLENDER_VERBOSE=1 blender path/to/bool_regression.blend --python path/to/boolean_operator.py -- --run_test <index> +# where <index> is the index of the test specified in the list tests. + +import bpy +import os +import sys + +sys.path.append(os.path.dirname(os.path.realpath(__file__))) +from modules.mesh_test import OperatorTest + + +def main(): + tests = [ + ['FACE', {0, 1, 2, 3, 4, 5}, 'Cubecube', 'Cubecube_result_1', 'intersect_boolean', {'operation': 'UNION'}], + ['FACE', {0, 1, 2, 3, 4, 5}, 'Cubecube', 'Cubecube_result_2', 'intersect_boolean', {'operation': 'INTERSECT'}], + ['FACE', {0, 1, 2, 3, 4, 5}, 'Cubecube', 'Cubecube_result_3', 'intersect_boolean', {'operation': 'DIFFERENCE'}], + ['FACE', {0, 1, 2, 3, 4, 5}, 'Cubecube', 'Cubecube_result_4', 'intersect', {'separate_mode': 'CUT'}], + ['FACE', {0, 1, 2, 3, 4, 5}, 'Cubecube', 'Cubecube_result_5', 'intersect', {'separate_mode': 'ALL'}], + ['FACE', {0, 1, 2, 3, 4, 5}, 'Cubecube', 'Cubecube_result_6', 'intersect', {'separate_mode': 'NONE'}], + ['FACE', {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, 'Cubecube', 'Cubecube_result_7', 'intersect', + {'mode': 'SELECT', 'separate_mode': 'NONE'}], + ['FACE', {6, 7, 8, 9, 10}, 'Cubecone', 'Cubecone_result_1', 'intersect_boolean', {'operation': 'UNION'}], + ['FACE', {0, 1, 2, 3, 4, 5}, 'Cubecones', 'Cubecones_result_1', 'intersect_boolean', {'operation': 'UNION'}], + ] + + operator_test = OperatorTest(tests) + + command = list(sys.argv) + for i, cmd in enumerate(command): + if cmd == "--run-all-tests": + operator_test.run_all_tests() + break + elif cmd == "--run-test": + index = int(command[i + 1]) + operator_test.run_test(index) + break + + +if __name__ == "__main__": + try: + main() + except: + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/tests/python/modifiers.py b/tests/python/modifiers.py new file mode 100644 index 00000000000..22ddfd163b1 --- /dev/null +++ b/tests/python/modifiers.py @@ -0,0 +1,156 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +import bpy +import os +import sys +from random import shuffle, seed +seed(0) + +sys.path.append(os.path.dirname(os.path.realpath(__file__))) +from modules.mesh_test import ModifierTest, ModifierSpec + + +def get_generate_modifiers_list(test_object_name, randomize=False): + """ + Construct a list of 'Generate' modifiers with default parameters. + :param test_object_name: str - name of test object. Some modifiers like boolean need an extra parameter beside + the default one. E.g. boolean needs object, mask needs vertex group etc... + The extra parameter name will be <test_object_name>_<modifier_type> + :param randomize: bool - if True shuffle the list of modifiers. + :return: list of 'Generate' modifiers with default parameters. + """ + + boolean_test_object = bpy.data.objects[test_object_name + "_boolean"] + + generate_modifiers = [ + ModifierSpec('array', 'ARRAY', {}), + ModifierSpec('bevel', 'BEVEL', {'width': 0.1}), + ModifierSpec('boolean', 'BOOLEAN', {'object': boolean_test_object}), + ModifierSpec('build', 'BUILD', {'frame_start': 0, 'frame_duration': 1}), + ModifierSpec('decimate', 'DECIMATE', {}), + ModifierSpec('edge split', 'EDGE_SPLIT', {}), + + # mask can effectively delete the mesh since the vertex group need to be updated after each + # applied modifier. Needs to be tested separately. + # ModifierSpec('mask', 'MASK', {'vertex_group': mask_vertex_group}, False), + + ModifierSpec('mirror', 'MIRROR', {}), + ModifierSpec('multires', 'MULTIRES', {}), + + # remesh can also generate an empty mesh. Skip. + # ModifierSpec('remesh', 'REMESH', {}), + + # ModifierSpec('screw', 'SCREW', {}), # screw can make the test very slow. Skipping for now. + # ModifierSpec('skin', 'SKIN', {}), # skin is not reproducible . + + ModifierSpec('solidify', 'SOLIDIFY', {}), + ModifierSpec('subsurf', 'SUBSURF', {}), + ModifierSpec('triangulate', 'TRIANGULATE', {}), + ModifierSpec('wireframe', 'WIREFRAME', {}) + + ] + + if randomize: + shuffle(generate_modifiers) + + return generate_modifiers + + +def main(): + + mask_first_list = get_generate_modifiers_list("testCubeMaskFirst", randomize=True) + mask_vertex_group = "testCubeMaskFirst" + "_mask" + mask_first_list.insert(0, ModifierSpec('mask', 'MASK', {'vertex_group': mask_vertex_group})) + + tests = [ + ############################### + # List of 'Generate' modifiers on a cube + ############################### + # 0 + # ["testCube", "expectedCube", get_generate_modifiers_list("testCube")], + ["testCubeRandom", "expectedCubeRandom", get_generate_modifiers_list("testCubeRandom", randomize=True)], + ["testCubeMaskFirst", "expectedCubeMaskFirst", mask_first_list], + + ############################################ + # One 'Generate' modifier on primitive meshes + ############################################# + # 4 + ["testCubeArray", "expectedCubeArray", [ModifierSpec('array', 'ARRAY', {})]], + ["testCylinderBuild", "expectedCylinderBuild", [ModifierSpec('build', 'BUILD', {'frame_start': 0, 'frame_duration': 1})]], + + # 6 + ["testConeDecimate", "expectedConeDecimate", [ModifierSpec('decimate', 'DECIMATE', {'ratio': 0.5})]], + ["testCubeEdgeSplit", "expectedCubeEdgeSplit", [ModifierSpec('edge split', 'EDGE_SPLIT', {})]], + ["testSphereMirror", "expectedSphereMirror", [ModifierSpec('mirror', 'MIRROR', {})]], + ["testCylinderMask", "expectedCylinderMask", [ModifierSpec('mask', 'MASK', {'vertex_group': "mask_vertex_group"})]], + ["testConeMultiRes", "expectedConeMultiRes", [ModifierSpec('multires', 'MULTIRES', {})]], + + # 11 + ["testCubeScrew", "expectedCubeScrew", [ModifierSpec('screw', 'SCREW', {})]], + ["testCubeSolidify", "expectedCubeSolidify", [ModifierSpec('solidify', 'SOLIDIFY', {})]], + ["testMonkeySubsurf", "expectedMonkeySubsurf", [ModifierSpec('subsurf', 'SUBSURF', {})]], + ["testSphereTriangulate", "expectedSphereTriangulate", [ModifierSpec('triangulate', 'TRIANGULATE', {})]], + ["testMonkeyWireframe", "expectedMonkeyWireframe", [ModifierSpec('wireframe', 'WIREFRAME', {})]], + #ModifierSpec('skin', 'SKIN', {}), # skin is not reproducible . + + ############################################# + # One 'Deform' modifier on primitive meshes + ############################################# + # 16 + ["testMonkeyArmature", "expectedMonkeyArmature", + [ModifierSpec('armature', 'ARMATURE', {'object': bpy.data.objects['testArmature'], 'use_vertex_groups': True})]], + ["testTorusCast", "expectedTorusCast", [ModifierSpec('cast', 'CAST', {'factor': 2.64})]], + ["testCubeCurve", "expectedCubeCurve", + [ModifierSpec('curve', 'CURVE', {'object': bpy.data.objects['testBezierCurve']})]], + ["testMonkeyDisplace", "expectedMonkeyDisplace", [ModifierSpec('displace', "DISPLACE", {})]], + + # Hook modifier requires moving the hook object to get a mesh change, so can't test it with the current framework + # ["testMonkeyHook", "expectedMonkeyHook", + # [ModifierSpec('hook', 'HOOK', {'object': bpy.data.objects["EmptyHook"], 'vertex_group': "HookVertexGroup"})]], + + # 20 + #ModifierSpec('laplacian_deform', 'LAPLACIANDEFORM', {}) Laplacian requires a more complex mesh + ["testCubeLattice", "expectedCubeLattice", + [ModifierSpec('lattice', 'LATTICE', {'object': bpy.data.objects["testLattice"]})]], + ] + + modifiers_test = ModifierTest(tests) + + command = list(sys.argv) + for i, cmd in enumerate(command): + if cmd == "--run-all-tests": + modifiers_test.apply_modifiers = True + modifiers_test.run_all_tests() + break + elif cmd == "--run-test": + modifiers_test.apply_modifiers = False + index = int(command[i + 1]) + modifiers_test.run_test(index) + break + + +if __name__ == "__main__": + try: + main() + except: + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/tests/python/modules/mesh_test.py b/tests/python/modules/mesh_test.py new file mode 100644 index 00000000000..9fb487bcef9 --- /dev/null +++ b/tests/python/modules/mesh_test.py @@ -0,0 +1,495 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +# A framework to run regression tests on mesh modifiers and operators based on howardt's mesh_ops_test.py +# +# General idea: +# A test is: +# Object mode +# Select <test_object> +# Duplicate the object +# Select the object +# Apply operation for each operation in <operations_stack> with given parameters +# (an operation is either a modifier or an operator) +# test_mesh = <test_object>.data +# run test_mesh.unit_test_compare(<expected object>.data) +# delete the duplicate object +# +# The words in angle brackets are parameters of the test, and are specified in +# the main class MeshTest. +# +# If the environment variable BLENDER_TEST_UPDATE is set to 1, the <expected_object> +# is updated with the new test result. +# Tests are verbose when the environment variable BLENDER_VERBOSE is set. + + +import bpy +import os +import inspect + + +class ModifierSpec: + """ + Holds one modifier and its parameters. + """ + + def __init__(self, modifier_name: str, modifier_type: str, modifier_parameters: dict): + """ + Constructs a modifier spec. + :param modifier_name: str - name of object modifier, e.g. "myFirstSubsurfModif" + :param modifier_type: str - type of object modifier, e.g. "SUBSURF" + :param modifier_parameters: dict - {name : val} dictionary giving modifier parameters, e.g. {"quality" : 4} + """ + self.modifier_name = modifier_name + self.modifier_type = modifier_type + self.modifier_parameters = modifier_parameters + + def __str__(self): + return "Modifier: " + self.modifier_name + " of type " + self.modifier_type + \ + " with parameters: " + str(self.modifier_parameters) + + +class OperatorSpec: + """ + Holds one operator and its parameters. + """ + + def __init__(self, operator_name: str, operator_parameters: dict, select_mode: str, selection: set): + """ + Constructs an operatorSpec. Raises ValueError if selec_mode is invalid. + :param operator_name: str - name of mesh operator from bpy.ops.mesh, e.g. "bevel" or "fill" + :param operator_parameters: dict - {name : val} dictionary containing operator parameters. + :param select_mode: str - mesh selection mode, must be either 'VERT', 'EDGE' or 'FACE' + :param selection: set - set of vertices/edges/faces indices to select, e.g. [0, 9, 10]. + """ + self.operator_name = operator_name + self.operator_parameters = operator_parameters + if select_mode not in ['VERT', 'EDGE', 'FACE']: + raise ValueError("select_mode must be either {}, {} or {}".format('VERT', 'EDGE', 'FACE')) + self.select_mode = select_mode + self.selection = selection + + def __str__(self): + return "Operator: " + self.operator_name + " with parameters: " + str(self.operator_parameters) + \ + " in selection mode: " + self.select_mode + ", selecting " + str(self.selection) + + +class MeshTest: + """ + A mesh testing class targeted at testing modifiers and operators on a single object. + It holds a stack of mesh operations, i.e. modifiers or operators. The test is executed using + the public method run_test(). + """ + + def __init__(self, test_object_name: str, expected_object_name: str, operations_stack=None, apply_modifiers=False): + """ + Constructs a MeshTest object. Raises a KeyError if objects with names expected_object_name + or test_object_name don't exist. + :param test_object: str - Name of object of mesh type to run the operations on. + :param expected_object: str - Name of object of mesh type that has the expected + geometry after running the operations. + :param operations_stack: list - stack holding operations to perform on the test_object. + :param apply_modifier: bool - True if we want to apply the modifiers right after adding them to the object. + This affects operations of type ModifierSpec only. + """ + if operations_stack is None: + operations_stack = [] + for operation in operations_stack: + if not (isinstance(operation, ModifierSpec) or isinstance(operation, OperatorSpec)): + raise ValueError("Expected operation of type {} or {}. Got {}". + format(type(ModifierSpec), type(OperatorSpec), + type(operation))) + self.operations_stack = operations_stack + self.apply_modifier = apply_modifiers + + self.verbose = os.environ.get("BLENDER_VERBOSE") is not None + self.update = os.getenv('BLENDER_TEST_UPDATE') is not None + + # Initialize test objects. + objects = bpy.data.objects + self.test_object = objects[test_object_name] + self.expected_object = objects[expected_object_name] + if self.verbose: + print("Found test object {}".format(test_object_name)) + print("Found test object {}".format(expected_object_name)) + + # Private flag to indicate whether the blend file was updated after the test. + self._test_updated = False + + def set_test_object(self, test_object_name): + """ + Set test object for the test. Raises a KeyError if object with given name does not exist. + :param test_object_name: name of test object to run operations on. + """ + objects = bpy.data.objects + self.test_object = objects[test_object_name] + + def set_expected_object(self, expected_object_name): + """ + Set expected object for the test. Raises a KeyError if object with given name does not exist + :param expected_object_name: Name of expected object. + """ + objects = bpy.data.objects + self.expected_object = objects[expected_object_name] + + def add_modifier(self, modifier_spec: ModifierSpec): + """ + Add a modifier to the operations stack. + :param modifier_spec: modifier to add to the operations stack + """ + self.operations_stack.append(modifier_spec) + if self.verbose: + print("Added modififier {}".format(modifier_spec)) + + def add_operator(self, operator_spec: OperatorSpec): + """ + Adds an operator to the operations stack. + :param operator_spec: OperatorSpec - operator to add to the operations stack. + """ + self.operations_stack.append(operator_spec) + + def _on_failed_test(self, compare, evaluated_test_object): + if self.update: + if self.verbose: + print("Test failed expectantly. Updating expected mesh...") + + # Replace expected object with object we ran operations on, i.e. evaluated_test_object. + evaluated_test_object.location = self.expected_object.location + expected_object_name = self.expected_object.name + + bpy.data.objects.remove(self.expected_object, do_unlink=True) + evaluated_test_object.name = expected_object_name + + # Save file + blend_file = bpy.data.filepath + bpy.ops.wm.save_as_mainfile(filepath=blend_file) + + self._test_updated = True + + # Set new expected object. + self.expected_object = evaluated_test_object + return True + + else: + blender_file = bpy.data.filepath + print("Test failed with error: {}. Resulting object mesh '{}' did not match expected object '{}' " + "from file blender file {}". + format(compare, evaluated_test_object.name, self.expected_object.name, blender_file)) + + return False + + def is_test_updated(self): + """ + Check whether running the test with BLENDER_TEST_UPDATE actually modified the .blend test file. + :return: Bool - True if blend file has been updated. False otherwise. + """ + return self._test_updated + + def _apply_modifier(self, test_object, modifier_spec: ModifierSpec): + """ + Add modifier to object and apply (if modifier_spec.apply_modifier is True) + :param test_object: bpy.types.Object - Blender object to apply modifier on. + :param modifier_spec: ModifierSpec - ModifierSpec object with parameters + """ + modifier = test_object.modifiers.new(modifier_spec.modifier_name, + modifier_spec.modifier_type) + if self.verbose: + print("Created modifier '{}' of type '{}'.". + format(modifier_spec.modifier_name, modifier_spec.modifier_type)) + + for param_name in modifier_spec.modifier_parameters: + try: + setattr(modifier, param_name, modifier_spec.modifier_parameters[param_name]) + if self.verbose: + print("\t set parameter '{}' with value '{}'". + format(param_name, modifier_spec.modifier_parameters[param_name])) + except AttributeError: + # Clean up first + bpy.ops.object.delete() + raise AttributeError("Modifier '{}' has no parameter named '{}'". + format(modifier_spec.modifier_type, param_name)) + + if self.apply_modifier: + bpy.ops.object.modifier_apply(modifier=modifier_spec.modifier_name) + + def _apply_operator(self, test_object, operator: OperatorSpec): + """ + Apply operator on test object. + :param test_object: bpy.types.Object - Blender object to apply operator on. + :param operator: OperatorSpec - OperatorSpec object with parameters. + """ + mesh = test_object.data + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.mode_set(mode='OBJECT') + + # Do selection. + bpy.context.tool_settings.mesh_select_mode = (operator.select_mode == 'VERT', + operator.select_mode == 'EDGE', + operator.select_mode == 'FACE') + for index in operator.selection: + if operator.select_mode == 'VERT': + mesh.vertices[index].select = True + elif operator.select_mode == 'EDGE': + mesh.edges[index].select = True + elif operator.select_mode == 'FACE': + mesh.polygons[index].select = True + else: + raise ValueError("Invalid selection mode") + + # Apply operator in edit mode. + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_mode(type=operator.select_mode) + mesh_operator = getattr(bpy.ops.mesh, operator.operator_name) + if not mesh_operator: + raise AttributeError("No mesh operator {}".format(operator.operator_name)) + retval = mesh_operator(**operator.operator_parameters) + if retval != {'FINISHED'}: + raise RuntimeError("Unexpected operator return value: {}".format(retval)) + if self.verbose: + print("Applied operator {}".format(operator)) + + bpy.ops.object.mode_set(mode='OBJECT') + + def run_test(self): + """ + Apply operations in self.operations_stack on self.test_object and compare the + resulting mesh with self.expected_object.data + :return: bool - True if the test passed, False otherwise. + """ + self._test_updated = False + bpy.context.view_layer.objects.active = self.test_object + + # Duplicate test object. + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.select_all(action="DESELECT") + bpy.context.view_layer.objects.active = self.test_object + + self.test_object.select_set(True) + bpy.ops.object.duplicate() + evaluated_test_object = bpy.context.active_object + evaluated_test_object.name = "evaluated_object" + if self.verbose: + print(evaluated_test_object.name, "is set to active") + + # Add modifiers and operators. + for operation in self.operations_stack: + if isinstance(operation, ModifierSpec): + self._apply_modifier(evaluated_test_object, operation) + + elif isinstance(operation, OperatorSpec): + self._apply_operator(evaluated_test_object, operation) + else: + raise ValueError("Expected operation of type {} or {}. Got {}". + format(type(ModifierSpec), type(OperatorSpec), + type(operation))) + + # Compare resulting mesh with expected one. + if self.verbose: + print("Comparing expected mesh with resulting mesh...") + evaluated_test_mesh = evaluated_test_object.data + expected_mesh = self.expected_object.data + compare = evaluated_test_mesh.unit_test_compare(mesh=expected_mesh) + success = (compare == 'Same') + + if success: + if self.verbose: + print("Success!") + + # Clean up. + if self.verbose: + print("Cleaning up...") + # Delete evaluated_test_object. + bpy.ops.object.delete() + return True + + else: + return self._on_failed_test(compare, evaluated_test_object) + + +class OperatorTest: + """ + Helper class that stores and executes operator tests. + + Example usage: + + >>> tests = [ + >>> ['FACE', {0, 1, 2, 3, 4, 5}, 'Cubecube', 'Cubecube_result_1', 'intersect_boolean', {'operation': 'UNION'}], + >>> ['FACE', {0, 1, 2, 3, 4, 5}, 'Cubecube', 'Cubecube_result_2', 'intersect_boolean', {'operation': 'INTERSECT'}], + >>> ] + >>> operator_test = OperatorTest(tests) + >>> operator_test.run_all_tests() + """ + + def __init__(self, operator_tests): + """ + Constructs an operator test. + :param operator_tests: list - list of operator test cases. Each element in the list must contain the following + in the correct order: + 1) select_mode: str - mesh selection mode, must be either 'VERT', 'EDGE' or 'FACE' + 2) selection: set - set of vertices/edges/faces indices to select, e.g. [0, 9, 10]. + 3) test_object_name: bpy.Types.Object - test object + 4) expected_object_name: bpy.Types.Object - expected object + 5) operator_name: str - name of mesh operator from bpy.ops.mesh, e.g. "bevel" or "fill" + 6) operator_parameters: dict - {name : val} dictionary containing operator parameters. + """ + self.operator_tests = operator_tests + self.verbose = os.environ.get("BLENDER_VERBOSE") is not None + self._failed_tests_list = [] + + def run_test(self, index: int): + """ + Run a single test from operator_tests list + :param index: int - index of test + :return: bool - True if test is successful. False otherwise. + """ + case = self.operator_tests[index] + if len(case) != 6: + raise ValueError("Expected exactly 6 parameters for each test case, got {}".format(len(case))) + select_mode = case[0] + selection = case[1] + test_object_name = case[2] + expected_object_name = case[3] + operator_name = case[4] + operator_parameters = case[5] + + operator_spec = OperatorSpec(operator_name, operator_parameters, select_mode, selection) + + test = MeshTest(test_object_name, expected_object_name) + test.add_operator(operator_spec) + + success = test.run_test() + if test.is_test_updated(): + # Run the test again if the blend file has been updated. + success = test.run_test() + return success + + def run_all_tests(self): + for index, _ in enumerate(self.operator_tests): + if self.verbose: + print() + print("Running test {}...".format(index)) + success = self.run_test(index) + + if not success: + self._failed_tests_list.append(index) + + if len(self._failed_tests_list) != 0: + print("Following tests failed: {}".format(self._failed_tests_list)) + + blender_path = bpy.app.binary_path + blend_path = bpy.data.filepath + frame = inspect.stack()[1] + module = inspect.getmodule(frame[0]) + python_path = module.__file__ + + print("Run following command to open Blender and run the failing test:") + print("{} {} --python {} -- {} {}" + .format(blender_path, blend_path, python_path, "--run-test", "<test_index>")) + + raise Exception("Tests {} failed".format(self._failed_tests_list)) + + +class ModifierTest: + """ + Helper class that stores and executes modifier tests. + + Example usage: + + >>> modifier_list = [ + >>> ModifierSpec("firstSUBSURF", "SUBSURF", {"quality": 5}), + >>> ModifierSpec("firstSOLIDIFY", "SOLIDIFY", {"thickness_clamp": 0.9, "thickness": 1}) + >>> ] + >>> tests = [ + >>> ["testCube", "expectedCube", modifier_list], + >>> ["testCube_2", "expectedCube_2", modifier_list] + >>> ] + >>> modifiers_test = ModifierTest(tests) + >>> modifiers_test.run_all_tests() + """ + + def __init__(self, modifier_tests: list, apply_modifiers=False): + """ + Construct a modifier test. + :param modifier_tests: list - list of modifier test cases. Each element in the list must contain the following + in the correct order: + 1) test_object_name: bpy.Types.Object - test object + 2) expected_object_name: bpy.Types.Object - expected object + 3) modifiers: list - list of mesh_test.ModifierSpec objects. + """ + self.modifier_tests = modifier_tests + self.apply_modifiers = apply_modifiers + self.verbose = os.environ.get("BLENDER_VERBOSE") is not None + self._failed_tests_list = [] + + def run_test(self, index: int): + """ + Run a single test from self.modifier_tests list + :param index: int - index of test + :return: bool - True if test passed, False otherwise. + """ + case = self.modifier_tests[index] + if len(case) != 3: + raise ValueError("Expected exactly 3 parameters for each test case, got {}".format(len(case))) + test_object_name = case[0] + expected_object_name = case[1] + spec_list = case[2] + + test = MeshTest(test_object_name, expected_object_name) + if self.apply_modifiers: + test.apply_modifier = True + + for modifier_spec in spec_list: + test.add_modifier(modifier_spec) + + success = test.run_test() + if test.is_test_updated(): + # Run the test again if the blend file has been updated. + success = test.run_test() + + return success + + def run_all_tests(self): + """ + Run all tests in self.modifiers_tests list. Raises an exception if one the tests fails. + """ + for index, _ in enumerate(self.modifier_tests): + if self.verbose: + print() + print("Running test {}...\n".format(index)) + success = self.run_test(index) + + if not success: + self._failed_tests_list.append(index) + + if len(self._failed_tests_list) != 0: + print("Following tests failed: {}".format(self._failed_tests_list)) + + blender_path = bpy.app.binary_path + blend_path = bpy.data.filepath + frame = inspect.stack()[1] + module = inspect.getmodule(frame[0]) + python_path = module.__file__ + + print("Run following command to open Blender and run the failing test:") + print("{} {} --python {} -- {} {}" + .format(blender_path, blend_path, python_path, "--run-test", "<test_index>")) + + raise Exception("Tests {} failed".format(self._failed_tests_list)) diff --git a/tests/python/operators.py b/tests/python/operators.py new file mode 100644 index 00000000000..c5b3ac745c6 --- /dev/null +++ b/tests/python/operators.py @@ -0,0 +1,172 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +import bpy +import os +import sys +from random import shuffle, seed + +seed(0) + +sys.path.append(os.path.dirname(os.path.realpath(__file__))) +from modules.mesh_test import OperatorTest, OperatorSpec + +# Central vertical loop of Suzanne +MONKEY_LOOP_VERT = {68, 69, 71, 73, 74, 75, 76, 77, 90, 129, 136, 175, 188, 189, 198, 207, + 216, 223, 230, 301, 302, 303, 304, 305, 306, 307, 308} +MONKEY_LOOP_EDGE = {131, 278, 299, 305, 307, 334, 337, 359, 384, 396, 399, 412, 415, 560, + 567, 572, 577, 615, 622, 627, 632, 643, 648, 655, 660, 707} + + +def main(): + tests = [ + #### 0 + # bisect + ['FACE', {0, 1, 2, 3, 4, 5}, "testCubeBisect", "expectedCubeBisect", "bisect", + {"plane_co": (0, 0, 0), "plane_no": (0, 1, 1), "clear_inner": True, "use_fill": True}], + + # blend from shape + ['FACE', {0, 1, 2, 3, 4, 5}, "testCubeBlendFromShape", "expectedCubeBlendFromShape", "blend_from_shape", + {"shape": "Key 1"}], + + # bridge edge loops + ["FACE", {0, 1}, "testCubeBrigeEdgeLoop", "expectedCubeBridgeEdgeLoop", "bridge_edge_loops", {}], + + # decimate + ["FACE", {i for i in range(500)}, "testMonkeyDecimate", "expectedMonkeyDecimate", "decimate", {"ratio": 0.1}], + + ### 4 + # delete + ["VERT", {3}, "testCubeDeleteVertices", "expectedCubeDeleteVertices", "delete", {}], + ["FACE", {0}, "testCubeDeleteFaces", "expectedCubeDeleteFaces", "delete", {}], + ["EDGE", {0, 1, 2, 3}, "testCubeDeleteEdges", "expectedCubeDeleteEdges", "delete", {}], + + # delete edge loop + ["VERT", MONKEY_LOOP_VERT, "testMokneyDeleteEdgeLoopVertices", "expectedMonkeyDeleteEdgeLoopVertices", + "delete_edgeloop", {}], + ["EDGE", MONKEY_LOOP_EDGE, "testMokneyDeleteEdgeLoopEdges", "expectedMonkeyDeleteEdgeLoopEdges", + "delete_edgeloop", {}], + + ### 9 + # delete loose + ["VERT", {i for i in range(12)}, "testCubeDeleteLooseVertices", "expectedCubeDeleteLooseVertices", + "delete_loose", {"use_verts": True, "use_edges": False, "use_faces": False}], + ["EDGE", {i for i in range(14)}, "testCubeDeleteLooseEdges", "expectedCubeDeleteLooseEdges", + "delete_loose", {"use_verts": False, "use_edges": True, "use_faces": False}], + ["FACE", {i for i in range(7)}, "testCubeDeleteLooseFaces", "expectedCubeDeleteLooseFaces", + "delete_loose", {"use_verts": False, "use_edges": False, "use_faces": True}], + + # dissolve degenerate + ["VERT", {i for i in range(8)}, "testCubeDissolveDegenerate", "expectedCubeDissolveDegenerate", + "dissolve_degenerate", {}], + + ### 13 + # dissolve edges + ["EDGE", {0, 5, 6, 9}, "testCylinderDissolveEdges", "expectedCylinderDissolveEdges", + "dissolve_edges", {}], + + # dissolve faces + ["VERT", {5, 34, 47, 49, 83, 91, 95}, "testCubeDissolveFaces", "expectedCubeDissolveFaces", "dissolve_faces", + {}], + + ### 15 + # dissolve verts + ["VERT", {16, 20, 22, 23, 25}, "testCubeDissolveVerts", "expectedCubeDissolveVerts", "dissolve_verts", {}], + + # duplicate + ["VERT", {i for i in range(33)} - {23}, "testConeDuplicateVertices", "expectedConeDuplicateVertices", + "duplicate", {}], + ["VERT", {23}, "testConeDuplicateOneVertex", "expectedConeDuplicateOneVertex", "duplicate", {}], + ["FACE", {6, 9}, "testConeDuplicateFaces", "expectedConeDuplicateFaces", "duplicate", {}], + ["EDGE", {i for i in range(64)}, "testConeDuplicateEdges", "expectedConeDuplicateEdges", "duplicate", {}], + + ### 20 + # edge collapse + ["EDGE", {1, 9, 4}, "testCylinderEdgeCollapse", "expectedCylinderEdgeCollapse", "edge_collapse", {}], + + # edge face add + ["VERT", {1, 3, 4, 5, 7}, "testCubeEdgeFaceAddFace", "expectedCubeEdgeFaceAddFace", "edge_face_add", {}], + ["VERT", {4, 5}, "testCubeEdgeFaceAddEdge", "expectedCubeEdgeFaceAddEdge", "edge_face_add", {}], + + # edge rotate + ["EDGE", {1}, "testCubeEdgeRotate", "expectedCubeEdgeRotate", "edge_rotate", {}], + + # edge split + ["EDGE", {2, 5, 8, 11, 14, 17, 20, 23}, "testCubeEdgeSplit", "expectedCubeEdgeSplit", "edge_split", {}], + + ### 25 + # face make planar + ["FACE", {i for i in range(500)}, "testMonkeyFaceMakePlanar", "expectedMonkeyFaceMakePlanar", + "face_make_planar", {}], + + # face split by edges + ["VERT", {i for i in range(6)}, "testPlaneFaceSplitByEdges", "expectedPlaneFaceSplitByEdges", + "face_split_by_edges", {}], + + # fill + ["EDGE", {20, 21, 22, 23, 24, 45, 46, 47, 48, 49}, "testIcosphereFill", "expectedIcosphereFill", + "fill", {}], + ["EDGE", {20, 21, 22, 23, 24, 45, 46, 47, 48, 49}, "testIcosphereFillUseBeautyFalse", + "expectedIcosphereFillUseBeautyFalse", "fill", {"use_beauty": False}], + + # fill grid + ["EDGE", {1, 2, 3, 4, 5, 7, 9, 10, 11, 12, 13, 15}, "testPlaneFillGrid", "expectedPlaneFillGrid", + "fill_grid", {}], + ["EDGE", {1, 2, 3, 4, 5, 7, 9, 10, 11, 12, 13, 15}, "testPlaneFillGridSimpleBlending", + "expectedPlaneFillGridSimpleBlending", "fill_grid", {"use_interp_simple": True}], + + ### 31 + # fill holes + ["VERT", {i for i in range(481)}, "testSphereFillHoles", "expectedSphereFillHoles", "fill_holes", {"sides": 9}], + + # inset faces + ["VERT", {5, 16, 17, 19, 20, 22, 23, 34, 47, 49, 50, 52, 59, 61, 62, 65, 83, 91, 95}, "testCubeInset", + "expectedCubeInset", "inset", {"thickness": 0.2}], + ["VERT", {5, 16, 17, 19, 20, 22, 23, 34, 47, 49, 50, 52, 59, 61, 62, 65, 83, 91, 95}, + "testCubeInsetEvenOffsetFalse", "expectedCubeInsetEvenOffsetFalse", + "inset", {"thickness": 0.2, "use_even_offset": False}], + ["VERT", {5, 16, 17, 19, 20, 22, 23, 34, 47, 49, 50, 52, 59, 61, 62, 65, 83, 91, 95}, "testCubeInsetDepth", + "expectedCubeInsetDepth", "inset", {"thickness": 0.2, "depth": 0.2}], + ["FACE", {35, 36, 37, 45, 46, 47, 55, 56, 57}, "testGridInsetRelativeOffset", "expectedGridInsetRelativeOffset", + "inset", {"thickness": 0.4, "use_relative_offset": True}], + ] + + operators_test = OperatorTest(tests) + + command = list(sys.argv) + for i, cmd in enumerate(command): + if cmd == "--run-all-tests": + operators_test.run_all_tests() + break + elif cmd == "--run-test": + operators_test.apply_modifiers = False + index = int(command[i + 1]) + operators_test.run_test(index) + break + + +if __name__ == "__main__": + try: + main() + except: + import traceback + + traceback.print_exc() + sys.exit(1) |