- 使用指南
- 数字绘画基础知识
- 参考手册
- 实例教程
- 常见问题解答
- 参与者手册
- 扩展包和第三方教程
- 其他
- 显示设置
- 日志查看器
- 数位板设置
- Automated Krita builds on CI matrix
- Brush GUI Design with Lager
- Building Krita from Source
- CMake Settings for Developers
- Enable static analyzer
- How to patch Qt
- Introduction to Hacking Krita
- The Krita Palette format KPL
- Krita SVG Extensions
- Modern C++ usage guidelines for the Krita codebase
- Developing Features
- Optimize Image Processing with XSIMD
- Optimizing tips and tools for Krita
- Google Summer of Code
- Advanced Merge Request Guide
- Python Developer Tools
- Introduction to Quality Assurance
- Making a release
- Reporting Bugs
- Strokes queue
- Testing Strategy
- Triaging Bugs
- Unittests in Krita
- 矢量图层
- 常规设置
- 颜料图层
- 图层组
- 克隆图层
- 文件图层
- 填充图层
- 滤镜图层
- 笔刷引擎
- 透明度蒙版
- 滤镜笔刷引擎
- 滤镜蒙版
- 裁剪工具
- 移动工具
- 变形工具
- 变形笔刷引擎
- 变形蒙版
- 网格与参考线
- 工作区
- 笔刷预设
- 色板
- 键盘快捷键
- 设置菜单
- 性能设置
- 笔尖
- 不透明度和流量
- 常用笔刷选项
- 多路笔刷工具
- 手绘笔刷工具
- 直线工具
- 曲线工具
- 辅助尺工具
- 图层
- 矩形选区工具
- 椭圆选区工具
- 多边形选区工具
- 手绘轮廓选区工具
- 相似颜色选区工具
- 相连颜色选区工具
- 曲线选区工具
- 磁性选区工具
- 形状选择工具
- 锚点编辑工具
- 工具菜单
- 动画时间轴
- 绘图纸外观
- 动画曲线
- 分镜头脚本
- 颜色
- 色域蒙版
- 美术拾色器
- 多功能拾色器
- 智能填色蒙版工具
- *.gih
- 像素笔刷引擎
- *.kra
- SeExpr
- SeExpr 脚本
- 渐变
- 颜色涂抹笔刷引擎
- 纹理
- 拾色器工具
- LUT 色彩管理
- 小型拾色器
- 有损和无损图像压缩
- *.bmp
- *.csv
- *.exr
- *.gbr
- *.gif
- *.heif 和 *.avif
- *.jpg
- *.jxl
- *.kpl
- *.ora
- .pbm、.pgm 和 *.ppm
- *.png
- *.psd
- *.svg
- *.tiff
- *.webp
- 数学运算
- 变暗
- HSX
- 变亮
- 颜色混合
- 负片
- 其他
- 二元逻辑
- 取模运算
- 二次方
- 鬃毛笔刷引擎
- 粉笔笔刷引擎
- 克隆笔刷引擎
- 曲线笔刷引擎
- 力学笔刷引擎
- 网格笔刷引擎
- 排线笔刷引擎
- MyPaint 笔刷引擎
- 粒子轨迹笔刷引擎
- 快速笔刷引擎
- 形状笔刷引擎
- 草图笔刷引擎
- 喷雾笔刷引擎
- 切线空间法线笔刷引擎
- 笔刷选项
- 锁定笔刷选项
- 蒙版笔刷
- 传感器
- 添加形状
- 动画
- 矢量图形排列
- 笔刷预设历史
- 色彩通道
- 颜色滑动条
- 图层显示方案
- 过渡色调混合器
- 直方图
- 导航器
- 图案
- 录像工具
- 参考图像
- 形状属性
- 图像版本快照
- 量化拾色器
- 操作流程
- 触摸屏辅助按钮
- 撤销历史
- 矢量图形库
- 宽色域拾色器
- 调整颜色/图像
- 艺术效果
- 模糊
- 边缘检测
- 浮雕
- 图像增强
- 映射
- 其他
- 小波分解
- 图层样式
- 选区蒙版
- 拆分透明度通道到蒙版
- 编辑菜单
- 文件菜单
- 帮助菜单
- 图像菜单
- 图层菜单
- 选择菜单
- 视图菜单
- 窗口菜单
- 作者档案设置
- 画布快捷键设置
- 隐藏面板模式设置
- 色彩管理设置
- 拾色器设置
- G’Mic 设置
- 弹出面板设置
- Python 插件管理器
- 笔尖
- 笔刷预设
- 图案
- 文字工具
- 渐变编辑工具
- 图案编辑工具
- 西文书法工具
- 矩形工具
- 椭圆工具
- 多边形工具
- 折线工具
- 手绘路径工具
- 力学笔刷工具
- 填充工具
- 闭合填充工具/圈涂
- 渐变工具
- 智能补丁工具
- 参考图像工具
- 测量工具
- 缩放工具
- 平移工具
- Building Krita with Docker on Linux
- Building krita on host Linux system (unsupported)
- Building Krita for Android
- Working on Krita Dependencies
- 渐变
- 多维网格
- 图案
- 网点
- 单纯形法噪点
Brush GUI Design with Lager
Krita controls overview
In Krita we have a really complicated system of brush settings, so in the beginning it would be nice to make a short overview of what we have
PaintOp
PaintOp is a brush engine that can load a brush preset and paint on canvas
PaintOpOption
Option is a high-level property of the brush. E.g. “Size”, “Opacity” or “Smudge Rate”. In the GUI an option is represented as a single page full of smaller settings. Most of Krita options also have a curve that links option’s value to the stylus sensors.
Sensor
Sensor represents a single sensor available in the stylus.
Problem Definition
The building block of any brush engine GUI is a PaintOpOption. When building a configuration widget for a PaintOp we just compose a set of independent options, pass them the brush preset (in a form of KisPropertiesConfiguration
object) and show the result to the user.
Each option has four responsibilities:
read/write serialized XML or properties data
define dependencies between properties of the option and other options, for example
Brush Application widget is available only for RGB brushes. For all standard brushes it should be grayed out and set to “Mask” mode
Lightness Strength option is available only when an RGB brush is selected and “Lightness Map” mode is enabled
show options in the GUI as Qt’s widgets
apply the actual effect of the option to the stroke on the canvas
The problem of our current implementation is that all four responsibilities are packed either in one (sometimes two) classes (see e.g. KisAirbrushOptionWidget
, or KisSmudgeOption
+ KisSmudgeOptionWidget
). And the dependencies logic is usually implemented in Widget part of the pack, which makes it extremely hard to debug and maintain (not speaking about porting to QML).
What is Lager?
Lager is a C++ library to assist value-oriented design by implementing the unidirectional data-flow architecture. It is heavily inspired by Elm and Redux, and enables composable designs by promoting the use of simple value types and testable application logic via pure functions.
What does it mean for us?
Value-oriented design
Value oriented design means that the library operates with immutable “value types”. We don’t “edit” any model. When we want to change something we just replace the whole “state”.
For Krita it means that we have a C++ structure for each part of the brush settings and can manipulate it easily. See, for example, KisAirbrushOptionData
which represents the corresponding option:
struct KisAirbrushOptionData : boost::equality_comparable<KisAirbrushOptionData> { inline friend bool operator==(const KisAirbrushOptionData &lhs, const KisAirbrushOptionData &rhs); bool isChecked {false}; qreal airbrushRate {50.0}; bool ignoreSpacing {false}; bool read(const KisPropertiesConfiguration *setting); void write(KisPropertiesConfiguration *setting) const; };
KisAirbrushOptionData
is a simple structure without any constructor, destructor or virtual functions. It is assignable and comparable. One can also write or read its value to a KisPropertiesConfiguration
object.
The main benefit of having such representation of the option is that now we can compare old and new value of the option and if the value hasn’t changed, don’t issue any update. It solves the problem of cycling updates that we have in the old implementation. The old implementation stores all the options in a single KisPropertiesConfiguration
, so we cannot split or compare it.
Unidirectional data-flow architecture
The original idea of Lager is that the system would be implemented in a fully “functional programming” approach. That is, there is a single “state” and the GUI calling “pure functions” to replace this state. We cannot use this “functional” design fully right now, but we can use other composing tools lager provides for our benefit.
Basically, Lager provides tools for building tree-like structures of values that depend on each other in uni-directional way.
Let’s consider the following simplified example of a scatter option:
struct KisSensorData { KoID id; QString curve; }; struct KisCurveOptionData { bool isChecked {false}; qreal strength {1.0}; KisSensorData pressureSensor; KisSensorData rotationSensor; KisSensorData fuzzySensor; }; struct KisScatterOptionData { bool scatterAxisX {true}; bool scatterAxisY {true}; KisCurveOptionData curveOption; };
You can see that the scatter option is composed of a curve option and a few own properties, like scatterAxisX
and scatterAxisY
.
The whole GUI is represented as a graph. Each node of this graph knows its value (and has a representation as a plain C++ struct).
Since each node knows its current value, when an update comes, the node can compare the new value against the current one and cancel update propagation in case the value haven’t changed. It allows us to avoid the problem of cycling updates, since a lot of Qt’s widgets emit updates even when the value doesn’t change.
What Lager provides?
Lager library consists of four main classes:
lager::state<>
is the single source of truth in the system. It stores the actual data and always represents the root of the graph.lager::cursor<>
is a node of the graph. A cursor connects to the state and track all of its updates. One can read or write into the cursor and the value will be propagated up the tree:// create state with automatic updates lager::state<KisScatterOptionData, lager::automatic_tag> optionState; // connect to one specific subvalue of the state lager::cursor<qreal> strength = optionState[&KisScatterOptionData::curveOption][&KisCurveOptionData::strength]; // read the linked value strengthSpinBox->setValue(strength.get()); // write the linked value strength.set(strengthSpinBox->value()); // subscribe to the linked value updates // (please note that lager also has a way to connect via // native Qt signals) strength.bind(std::bind(&QDoubleSpinBox::setValue, strengthSpinBox, std::placeholders::_1));
lager::reader<>
andlager::writer<>
work in the same way as cursors, but for read-only and write-only access types
On-the-fly value transformations
When creating a node with a cursor one can not only access member variables, but also do transformations on the fly!
lager::state<KisScatterOptionData, lager::automatic_tag> optionState; // connect to one specific subvalue of the state lager::cursor<qreal> strength = optionState[&KisScatterOptionData::curveOption][&KisCurveOptionData::strength]; // create a cursor that automatically scales the strength value from 0...1 range // to 0...100 lager::cursor<qreal> scaledStrength = strength.zoom(kiszug::lenses::scale<qreal>(100.0));
Here we use a .zoom()
expression with a lens that implements conversion of the value in both directions. That is, when scaledStrength
value is read, the lens multiplies the source value by 100.0. When scaledStrength
is written, it automatically divides the new value by 100.0 before writing into the source.
Value aggregation and “effectiveValue” pattern
In some cases one needs to combine multiple cursors coming from different sources. For example, Lightness Strength option’s checked state depends on the two separate values:
whether the user checked it using the checkbox
whether Lightness Strength is actually supported by the brush
When the brush does not support Lightness Strength, then the option is unchecked and disabled. That can be written in Lager using the lager::with()
expression:
lager::state<KisLightnessStrengthOptionData, lager::automatic_tag> optionState; // the cursor provided by the brush option externally lager::cursor<bool> allowedByTheBrush = ...; // connect to the user-set value lager::cursor<bool> isCheckedByUser = optionState[&KisLightnessStrengthOptionData::curveOption] [&KisCurveOptionData::isChecked]; // combine the two cursors using logical-and operator into // an "effective" isChecked value; lager::reader<bool> effectiveIsChecked = // `lager::with()` expression combines multiple cursors into one tuple lager::with(allowedByTheBrush, isCheckedByUser) // `.map()` expression accepts a standard function or functor which is used to // transform the source cursor on-the-fly .map(std::logical_and{});
We use such “effectiveValue” design a lot. It is the main tool against the cycling dependencies. The point is, we cannot assign anything to isCheckedByUser
from within the update, it would create a cycling dependency:
// piping one cursor into another creates loops, don't do this! allowedByTheBrush.bind(std::bind(&lager::cursor<bool>::set, &isCheckedByUser, std::placeholders::_1);
Such design has a small complication though. This “effective” value is no longer serialized by KisScatterOptionData
automatically, since it is not present in KisScatterOptionData
. To overcome this issue we use the process of “baking” the model into the data. This process will be explained later.
Combining value transformations
Lager performs value transformations via so called transducers. Transducer is a special form of a lambda expression that allows combining multiple operations into a single C++ entity, which can be manipulated later. Standard transducers for Lager are provided by zug library (check official documentation for zug). Krita also provides a set of useful transducers in KisZug.h
.
Let’s check an example from KisPredefinedBrushModel.h
. Our brightness adjustment is stored in a form of a qreal value with range 0…1, but the GUI widget shows it as an integer percentage value in range 0…100. Here is an example of how we can link these values with Lager:
struct PredefinedBrushData { // source value is `qreal`! qreal brightnessAdjustment {0.0}; }; // destination value is `int`! lager::cursor<int> brightnessAdjustment = predefinedBrushData[&PredefinedBrushData::brightnessAdjustment] // `xform` expression accepts two transducers that transform the expression // on-the-fly. The first transducer is a "getter", the second is a "setter" .xform( // getter: multiply the value by 100.0 and then round it to the nearest // integer kiszug::map_mupliply<qreal>(100.0) | kiszug::map_round, // setter: cast integer into a `qreal` and scale back into 0...1 range kiszug::map_static_cast<qreal> | kiszug::map_mupliply<qreal>(0.01));
Extending value types
The value oriented design has one non-obvious complication. Since we want all the values to be easily assignable and comparable, we can use no polymorphism. Basically, virtual functions are prohibited in the “values” we operate with.
Consequently, if we need to extend some type, e.g. KisCurveOptionData
, we cannot do that by overriding virtual methods (what we would do in the old design). Instead we should combine KisCurveOptionData
with extra data using composition or inheritance. Here is an example of how we do that for KisScatterOptionData
:
// Define the scatter-specific options in a separate mixin class that // implements all standard operations: equality comparison, read and write struct KisScatterOptionMixIn : boost::equality_comparable<KisScatterOptionMixInImpl> { friend bool operator==(const KisScatterOptionMixInImpl &lhs, const KisScatterOptionMixInImpl &rhs); bool axisX {true}; bool axisY {true}; bool read(const KisPropertiesConfiguration *setting); void write(KisPropertiesConfiguration *setting) const; }; // Combine this mixin class with KisCurveOptionData and manually forward // all the main operators to the parent classes struct KisScatterOptionData : KisCurveOptionData, , KisScatterOptionMixIn , boost::equality_comparable<KisScatterOptionData> { KisScatterOptionData() : KisCurveOptionData(KoID("Scatter", i18n("Scatter"))) { } friend bool operator==(const KisScatterOptionMixInImpl &lhs, const KisScatterOptionMixInImpl &rhs) { return static_cast<const KisCurveOptionData&>(lhs) == static_cast<const KisCurveOptionData&>(rhs) && static_cast<const KisScatterOptionMixIn&>(lhs) == static_cast<const KisScatterOptionMixIn&>(rhs); } bool read(const KisPropertiesConfiguration *setting) { return KisCurveOptionData::read(setting) && KisScatterOptionMixIn::read(setting); } void write(KisPropertiesConfiguration *setting) const { KisCurveOptionData::write(setting); KisScatterOptionMixIn::write(setting); } };
In this example we manually define a class that combines our scatter-specific mixin class with the base KisCurveOptionData
. You see it requires a lot of boiler-plate code. Hence there is a special tool to do such composition automatically :)
// Combine the mixin class with KisCurveOptionData using a special tool class // KisOptionTuple. It inherits from all its template parameters and automatically // implements equality comparison, read and write operators. struct KisScatterOptionData : KisOptionTuple<KisCurveOptionData, KisScatterOptionMixIn> { KisScatterOptionData() : KisOptionTuple<KisCurveOptionData, KisScatterOptionMixIn>(KoID("Scatter", i18n("Scatter"))) { } };
提示
Even though virtual function are prohibited, we still use them in one place, KisDynamicSensor
. KisDynamicSensor
is a representation of a single sensor in KisCurveOptionData
and it is somewhat polymorphic. But these polymorphic sensors are fully contained inside a single curve option. They are created internally and none of their pointers are ever exposed to the outer world.
Official documentation
Lager
Source code: https://github.com/arximboldi/lager
Documentation: https://sinusoid.es/lager/introduction.html
Zug
Source code: https://github.com/arximboldi/zug
Documentation: https://sinusoid.es/zug/index.html
How all this applies to Krita?
From the previous chapters you know that each option in Krita has four responsibilities:
read/write serialized XML or properties data
define dependencies between properties of the option and other options, for example
show options in the GUI as Qt’s widgets
apply the actual effect of the option to the stroke on the canvas
The problem of the old implementation was that all of them were implemented in a single class, which was hard to maintain and extent.
In the Lager-based implementation each option now has five different entities that map to these responsibilities cleanly:
Data
reads/writes to/from XML or properties; has no logic inside!State
— the single source of truth of the system. It just wrapsData
intolager::state<Data>
and brings it into the world of Lager.Model
models all dependencies between brush settings and other options; it implements all the logic of the option.
a model is connected to its state via
lager::cursor<>
a model creates a Qt Property for each brush setting so we could connect it either to a widget or QML control
Widget
implements an actual widget for the option
a widget connects to model’s Qt Properties using KisWidgetConnectionUtils. In the future QML controls will be connected to these properties directly.
widgets have no logic inside!
Option
is used byKisPaintOp
to apply the actual effect to the brush stroke. Options do not depend on any Lager or GUI classes, they only use Data objects to actually read the data.
A complete example from Krita
Let’s consider KisPaintingModeOption as a simple example. This option is used to select brush painting mode and has only one setting that can flip between two values: build-up and wash.
‘Data’ for “painting mode” option
First define a Data
structure that implements equality comparison, read and write operators:
enum class enumPaintingMode { BUILDUP, WASH }; struct KisPaintingModeOptionData : boost::equality_comparable<KisPaintingModeOptionData> { inline friend bool operator==(const KisPaintingModeOptionData &lhs, const KisPaintingModeOptionData &rhs); enumPaintingMode paintingMode { enumPaintingMode::BUILDUP }; bool read(const KisPropertiesConfiguration *setting); void write(KisPropertiesConfiguration *setting) const; };
‘Model’ for “painting mode” option
Now let’s implement a model for this option. Painting mode has a minor complication: it is available only when masking brush feature is disabled. When the user enables masking brush feature, the painting mode option becomes disabled and selects WASH
mode automatically.
提示
The code below uses LAGER_QT_CURSOR
macro. It defines a cursor of the provided type, creates a Qt Property with the provided name and links it to the cursor. To access the cursor later we should write LAGER_QT(propertyName)
.
namespace { int calcEffectivePaintingMode(enumPaintingMode mode, bool maskingBrushEnabled) { return static_cast<int>(maskingBrushEnabled ? enumPaintingMode::WASH : mode); } } class KisPaintingModeOptionModel : public QObject { Q_OBJECT public: // declare cursors of the model lager::cursor<KisPaintingModeOptionData> optionData; lager::reader<bool> maskingBrushEnabled; // // Define option settings and create Qt Properties for them: // // paintingMode is the mode selected by the user in the GUI LAGER_QT_CURSOR(int, paintingMode); // effectivePaintingMode is the actual mode used by the brush // calculated from the combination of user selection and the // masking brush presence LAGER_QT_READER(int, effectivePaintingMode); // A special property type that updates a state (isEnabled + currentIndex) // of a button group in a single signal call. It is useful to avoid partial // updates that can lead to cycles in some cases. LAGER_QT_READER(ButtonGroupState, paintingModeState); // The constructor of the model accepts two cursors. `optionData` is stored in // an external 'state'; `maskingBrushEnabled` cursor is provided by masking // brush option KisPaintingModeOptionModel(lager::cursor<KisPaintingModeOptionData> _optionData, lager::reader<bool> _maskingBrushEnabled) : optionData(_optionData) , maskingBrushEnabled(_maskingBrushEnabled) // in paintingMode cursor we just erase the enum type to be able // to make connection to QGroupBox , LAGER_QT(paintingMode) { optionData[&KisPaintingModeOptionData::paintingMode] .zoom(kiszug::lenses::do_static_cast<enumPaintingMode, int>) } // effectivePaintingMode depends on both inputs of the model , LAGER_QT(effectivePaintingMode) { lager::with(optionData[&KisPaintingModeOptionData::paintingMode], maskingBrushEnabled) .map(&calcEffectivePaintingMode) } // combine two properties into one state , LAGER_QT(paintingModeState) { lager::with(LAGER_QT(effectivePaintingMode), maskingBrushEnabled.map(std::logical_not{})) .map(ToControlState{})} { } // bakedOptionData() creates a new 'Data' objects that has all // the "effective" values baked into it. KisPaintingModeOptionData bakedOptionData() const { KisPaintingModeOptionData data = optionData.get(); data.paintingMode = static_cast<enumPaintingMode>(effectivePaintingMode()); return data; } };
Please pay attention to bakedOptionData()
method of the model. The model has one “effective” property that is not directly stored in its Data
storage. Therefore, before serializing the model, we should first bake all the “effective” values into the data object and then use this new object for actual writing. Granted copying option’s data objects is cheap and easy now.
‘Widget’ for “painting mode” option
Finally, let’s consider a simplified version of the code in KisPaintingModeOptionWidget
:
class KisPaintingModeOptionWidget : public KisPaintOpOption { public: KisPaintingModeOptionWidget(lager::cursor<KisPaintingModeOptionData> optionData, lager::reader<bool> maskingBrushEnabled) : m_model(optionData, maskingBrushEnabled) { // for connectControlState() using namespace KisWidgetConnectionUtils; // Create the main widget KisPaintingModeWidget *widget = new KisPaintingModeWidget(); setConfigurationPage(widget); // Create the button group for mode selection QButtonGroup *group = new QButtonGroup(widget); // .. skipped .. // .. initialize group and add actual buttons to it ... // .. skipped .. // Connect the group to the model: "paintingModeState" is the // "read" property, "paintingMode" is "write" property. We read // from "effective" property and write directly into 'data'. connectControlState(group, &m_model, "paintingModeState", "paintingMode"); // connect the changes in the model to the output signal // of the configuration page m_model.optionData.bind( std::bind(&KisPaintingModeOptionWidget::emitSettingChanged, this)); } void writeOptionSetting(KisPropertiesConfigurationSP setting) const override { // write **baked** data! m_model.bakedOptionData().write(setting.data()); } void readOptionSetting(const KisPropertiesConfigurationSP setting) override { KisPaintingModeOptionData data = *m_model.optionData; data.read(setting.data()); m_model.optionData.set(data); } private: KisPaintingModeOptionModel m_model; };
‘Option’ for “mirror” option
Since painting mode is very simple, it doesn’t have any Option representation. The brush engine uses its Data object directly.
For a good example of an ‘option’ let’s consider KisMirrorOption
. This class is used by the brush engine while painting the actual stroke of the canvas. The responsibility of KisMirrorOption
is to accept the state of the stylus (in a form of KisPaintInformation
object) and calculate MirrorProperties
from it.
#include <KisPaintOpOptionUtils.h> namespace kpou = KisPaintOpOptionUtils; class KisMirrorOption : public KisCurveOption { public: // The public constructor creates a data object from // the settings pointer and passes it to a private constructor // that initializes all the necessary state KisMirrorOption(const KisPropertiesConfiguration *setting) : KisMirrorOption( kpou::loadOptionData<KisMirrorOptionData>(setting)) { } private: // The private constructor initializes all the necessary state // from the data and passes it to the base option class. // // Please note that the data is **not** stored anywhere in the // option, it is used only during the initialization KisMirrorOption(const KisMirrorOptionData &data) : KisCurveOption(data) , m_enableHorizontalMirror(data.enableHorizontalMirror) , m_enableVerticalMirror(data.enableVerticalMirror) { } public: MirrorProperties apply(const KisPaintInformation &info) const { // ... // skipped some calculations using: // * m_enableHorizontalMirror // * m_enableVerticalMirror // * KisCurveOption::computeSizeLikeValue(info) // ... MirrorProperties mirrors; mirrors.verticalMirror = ...; mirrors.horizontalMirror = ...; mirrors.coordinateSystemFlipped = ...; return mirrors; } private: bool m_enableHorizontalMirror; bool m_enableVerticalMirror; };
Paint engine porting guide
When porting is it recommended to use KisBrushOp
as an reference implementation.
The rough plan for porting an arbitrary painting engine FooOp
to lager is the following:
Port the GUI part
Open
KisFooOpSettingsWidget
class and look at its constructor that creates all the option widgets.Replace all standard option widgets with the already ported ones. Use
KisBrushOpSettingsWidget
as a reference of existing widgets.Test if GUI still works correctly and affects the brush in an expected way
Port all non-standard options to lager and add them to
KisFooOpSettingsWidget
. Usually, old and new class names map as the following:
KisFooBarOptionData
usually borrows reading and writing code fromKisPressureFooBarOption
KisFooBarOptionModel
is just written from scratch
KisFooBarOptionWidget
borrows GUI code fromKisPressureFooBarOptionWidget
Use
KisScatterOptionData
,KisScatterOptionModel
andKisScatterOptionWidget
as a reference implementation.
Test if GUI still works correctly and affects the brush in an expected way
Port the painting part
Open
KisFooOp
Replace all standard
KisPressureFooBarOption
classes with the already ported ones. UseKisBrushOp
as a reference of existing options.Port all non-standard options to lager: you just need to extract
KisPressureFooBarOption::apply()
function into a separate class namedKisFooBarOption
. UseKisScatterOption
as a reference implementation.Test if the brush still reacts to the GUI changes in an expected way
Check if any of the options you ported had
KisPressureFooBarOption::lodLimitation()
method. If so, port these limitations to your newKisFooBarOptionData
and use a special creation functionKisPaintOpOptionWidgetUtils::createOptionWidgetWithLodLimitations()
to create a widget for it. UseKisSizeOptionData
andKisSizeOptionWidget
as a reference implementation.If any new brush option has “effective” values, verify that you have
KisFooBarOptionModel::bakedOptionData()
method in the model and calls it fromKisFooBarOptionWidget::writeOptionSetting()
in the widget.Open
KisFooOpSettings
and port all the uniform properties to use new data classes. UseKisColorSmudgeOpSettings
as a reference implementation.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论