Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

[Blender] Blender2.7xのアドオンをBlender2.8へ移植する

More than 1 year has passed since last update.

Blender Advent Calendar 2018 の23日目の記事です。

Blender 2.8のBeta版が2018/11/29にリリースされたことにより、Blender 2.8におけるPython APIがほぼFixしました。
Blender 2.8は既存のPython APIを大きく変えるリリースであるとアナウンスされ、アドオン開発者はPython APIがFixした後にアドオンのソースコードを修正する必要があると言われています。
実際にBlender 2.8のBeta版が公開された後、多くのBlenderのアドオン開発者が、過去に作成したアドオンをBlender 2.8へ移植するための作業を開始しています。
私がメンテナンスしているアドオン Magic UV についても、Blender 2.8における大きなPython APIの変更の影響を受けていて、本記事を書きながらBlender 2.8の移植を行っている状況です。

本記事では、私がMagic UVをBlender 2.8に移植するために必要であった修正の中で、一般的な修正点を説明します。
なお、Blender 2.8におけるPython APIの変更点は非常に多く、本記事で示した修正を適用したとしても、Blender 2.7xで動作していたアドオンの完全動作は保証できません。
実際にBlender 2.7xのアドオンをBlender 2.8に移植するためには、本記事で行った修正に加えて必要に応じて修正が必要になる可能性があります。

アドオン共通

ここでは、Blender 2.7xでアドオンをBlender 2.8へ移植するために、Python APIの利用状況に関わらず、ほぼすべてのアドオンが適用しなければならない変更点を示します。

bl_infoのバージョン

アドオンの情報を示す変数である bl_infoversion(2, 80, 0) に変更する必要があります。
bl_infoversion は後方互換ですが、Blender 2.8に限っては bl_infoversion(2, 80, 0) 以上に設定されたアドオンしかBlenderで認識しません。

v2.7x
bl_info = {
    "version": (2, 79, 0)
}
v2.80
bl_info = {
    "version": (2, 80, 0)
}

Blender関連のクラスの命名規則

bpy.types.Operator を基底クラスとして作成したオペレータクラスなど、Blender関連のクラスの命名規則は存在していましたが、必ずしもこの命名規則に従う必要はありませんでした。
しかし、命名規則に従わないアドオンが多く存在したことで、Blender 2.7xではアドオン間でクラス名の衝突問題が頻繁に起こっていました。
このため、Blender 2.8では命名規則に従うことが義務付けられ、命名規則に従わないアドオンはコンソールの標準出力にエラーメッセージが出力されるようになります。

具体的な命名規則は、以下の通りです。
なお、XXX は英大文字から始まる英字/数字から構成される文字列、YYY は英字/数字/アンダースコア(_)から構成される文字列です。

基底クラス 派生クラスの命名規則
bpy.types.Header XXX_HT_YYY
bpy.types.Menu XXX_MT_YYY
bpy.types.Operator XXX_OT_YYY
bpy.types.Panel XXX_PT_YYY
bpy.types.UIList XXX_UL_YYY

Blender関連のクラスの登録

Blender 2.7xでは、bpy.utils.register_module を利用することにより、一括してBlender関連のクラスを登録することができました。
しかしBlender 2.8では、bpy.utils.register_module の処理コストが大きいことから廃止されます。
従ってBlender 2.8では、bpy.utils.register_class を利用してクラスを登録する必要があります。

v2.7x
# クラスの登録
bpy.utils.register_module(__name__)

# クラスの登録解除
bpy.utils.unregister_module(__name__)
v2.80
classes = [
    HOGE_MT_PiyoMenu,
    HOGE_OT_FugaOperator,
    HOGE_PT_FooPanel,
]

# クラスの登録
for cls in classes:
    bpy.utils.register_class(cls)

# クラスの登録解除
for cls in classes:
    bpy.utils.unregister_class(cls)

プロパティの宣言

Blender 2.7xにおいて、プロパティ(bpy.props.IntProperty など)をBlender関連のクラスに含める場合は、単純な代入文で問題ありませんでした。
しかしBlender 2.8では、Python 3.5から導入された Type Hints機能 のための型アノテーション記法を推奨しています。
従来の単純な代入文でもBlender 2.8では動作しますが、コンソールに警告メッセージが出力されます。

v2.7x
class FugaOperator(bpy.types.Operator):
    property = IntProperty()
v2.80
class HOGE_OT_FugaOperator(bpy.types.Operator):
    property: IntProperty()

必要に応じて修正が必要

ここでは、Python APIの利用状況によって修正が必要になることがある修正内容を示します。

一部のAPIでキーワード引数が必須化

Blender 2.8では、一部のAPIでキーワード引数が必須になります。
キーワード引数が必須化された全てのPython APIについてここで紹介することはできないため、Magic UVをBlender 2.8へ移植する時に必要となった修正の一部を紹介します。

UILayout.label

Blender 2.7xでは、UILayout.label の第1引数にラベルで表示する文字列を指定することができますが、Blender 2.8では明示的にキーワード text を指定して表示する文字列を指定する必要があります。

v2.7x
class FooPanel(bpy.types.Panel):
    def draw(self, context):
        layout = self.layout
        layout.label("bar")
v2.80
class HOGE_PT_FooPanel(bpy.types.Panel):
    def draw(self, context):
        layout = self.layout
        # textはキーワード引数として指定する必要がある
        layout.label(text="bar")

context.window_manager.event_timer_add

Blender 2.8で提供されるタイマを登録するためのAPI context.window_manager.event_timer_add についても、キーワード引数が必須になっています。

v2.7x
def invoke(self, context, _):
    timer = context.window_manager.event_timer_add(0.1, context.window)
v2.80
def invoke(self, context, _):
    # windowはキーワード引数として指定する必要がある
    timer = context.window_manager.event_timer_add(0.1, window=context.window)

オブジェクト関連のAPI

オブジェクトの選択

Blender 2.8では、オブジェクトの選択状態を取得/設定するAPIが変更されます。

v2.7x
obj = bpy.data.objects[0]
if obj.select == False:
    obj.select = True
v2.80
obj = bpy.data.objects[0]
if obj.select_get() == False:
    obj.select_set(True)

アクティブオブジェクトの設定

アクティブなオブジェクトを設定する方法についても、Blender 2.8で変更されます。

v2.7x
bpy.context.scene.objects.active = bpy.data.objects[0]
v2.80
bpy.context.view_layer.objects.active = bpy.data.objects[0]

メッシュ関連のAPI

BMeshのTextureレイヤが消えている

Blender 2.7xでは、メッシュに割り当てられているTextureレイヤを、BMeshから取得することができました。
しかしBlender 2.8では、このAPIが削除されています。

v2.7x
import bmesh

obj = bpy.context.active_object
bm = bmesh.from_edit_mesh(obj.data)

# Textureレイヤを取得
tex_layer = bm.faces.layers.tex.verify()

# 面に割り当てられたTextureを取得
img = bm.faces[0][tex_layer].image
v2.80
import bmesh

obj = bpy.context.active_object
bm = bmesh.from_edit_mesh(obj.data)

# ★エラー:bm.faces.layersに、texは存在しない
tex_layer = bm.faces.layers.tex.verify()

# ★エラー:bm.faces[0]に、キーtex_layerは存在しない
img = bm.faces[0][tex_layer].image

UI関連のAPI

Tool-Shelfへのパネル配置は不可

Blender 2.7xにおけるTool-Shelfは、Blender 2.8ではToolbarに変更されます。
ToolbarのUIをアドオンから変更することは現状許されていないため、Blender 2.7xでTool-Shelfを利用していたアドオンは修正が必要になります。
アドオンのUI自体を変えてしまうのも1つの手ですが、Blender 2.7xのTool-Shelfとできるだけ近づけたい場合は、タブ機能が存在するSidebarを利用するのがよいでしょう。
Sidebarは、Blender 2.7xのProperty-Shelfから変更されたものになります。
ここでは、Blender 2.7xのTool-Shelfに配置されていたパネルを、Blender 2.8のSidebarに配置し直すために必要な修正方法を示します。

Tool-Shelfにパネルを配置する場合、Blender 2.7xでは bpy.types.Panel を継承したクラスにおいて、配置先のリージョンを示すプロパティ bl_region_type に、TOOLS を指定する必要がありました。

v2.7x
class FooPanel(bpy.types.Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'TOOLS'
    bl_label = "Foo"
    bl_category = "Hoge"
    bl_context = 'mesh_edit'

Blender 2.8では、'TOOLS' はToolbar向けに唯一設定可能なものとされ、新たなパネルを配置できません。
実際、bl_region_typeTOOLS を指定すると、エラーが発生してしまいます。
このため対応案の1つとして、Sidebarにパネルを配置する方法がありますが、Sidebarにパネルを配置するためには、bl_region_typeUI (Blender 2.7xでは、bl_region_typeUI を指定するとProperty-Shelfへの配置となる)を指定します。

v2.80
class HOGE_PT_FooPanel(bpy.types.Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_label = "Foo"
    bl_category = "Hoge"
    bl_context = 'mesh_edit'

アイコンの追加と削除

Blender 2.8でUIが大きく変更されたことにより、アドオンから利用可能なアイコンも大きく変更されています。
Blender 2.8で新しくアイコンが追加されているだけでなく、Blender 2.7xで利用可能であったアイコンが削除されている場合もあります。

ここでは、Blender 2.7xとBlender 2.8における利用可能なアイコンを比較します。
各アイコンの隣には、アイコンを表示するためにAPIの引数 icon に指定する文字列も表示しました。
Blender 2.8についてはアイコンが表示されていないものがありますが、表示されていないアイコンは利用できないアイコンです。
もしこれらのアイコンを表示させようとすると、エラーメッセージが出力されるます。

なお、アイコンを一覧表示するために、本記事の サンプル を使用しました。

Blender 2.7xで利用可能なアイコン

icons_v279_1.png
icons_v279_2.png
icons_v279_3.png
icons_v279_4.png
icons_v279_5.png

Blender 2.80で利用可能なアイコン

icons_v280_1.png
icons_v280_2.png
icons_v280_3.png
icons_v280_4.png
icons_v280_5.png
icons_v280_6.png

数学関連のAPI

Blender 2.7xでは、行列積を M1 * M2 のように書くことができましたが、Blender 2.8では M1 @ M2 のように書く必要があります。
これは、PEP465 で推奨されている記法に従ったものであり、BlenderのPython APIでもこれに従った形になります。
なお従来の記法 M1 * M2 は、Element-wiseな積(アダマール積)として提供される予定です。

v2.7x
from mathutils import Matrix, Vector

mat = Matrix([[2.4, 1.3], [1.5, 1.0]])
vec = Vector([0.8, 3.5])

# 行列積
# 結果:
#   Vector (6.4700, 4.7000)
ret = mat * vec

# アダマール積
# 結果:
#   Matrix 2x2 (4.8000, 2.6000)
#              (3.0000, 2.0000)
ret = mat * 2
v2.80
from mathutils import Matrix, Vector

mat = Matrix([[2.4, 1.3], [1.5, 1.0]])
vec = Vector([0.8, 3.5])

# 行列積
# 結果:
#   Vector (6.4700, 4.7000)
ret = mat @ vec

# アダマール積
# 結果:
#   Matrix 2x2 (4.8000, 2.6000)
#              (3.0000, 2.0000)
ret = mat * 2

# ★エラー:*を行列積に使用できない
ret = mat * vec

描画関連のAPI

Blender 2.7xにおいて独自のUIを構築するときは、OpenGLのAPIをPythonから利用可能な bgl モジュールを利用して描画していました。
Blender 2.8では、バックエンドで使用するOpenGLのバージョンが変わったことにより、bgl モジュールで提供されるAPIが大きく変わりました。
また、Blender 2.8では bgl モジュールを利用することが非推奨とされ、代わりに gpu モジュールを利用することが推奨されています。

gpu モジュールは、bgl モジュールと使い方が大きく異なるため、bgl モジュールを利用するアドオンは gpu モジュールを使うように修正しなければなりません。
ここでは簡単な例として、四角形を描画するプログラムを比較してみます。

v2.7x
import bpy
import bgl

def draw():
    verts = [
        [0.0, 0.0],
        [0.0, 200.0],
        [200.0, 200.0],
        [200.0, 0.0],
    ]

    bgl.glEnable(bgl.GL_BLEND)
    bgl.glBegin(bgl.GL_QUADS)
    bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
    for (x, y) in verts:
        bgl.glVertex2f(x, y)
    bgl.glEnd()

handle = bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'POST_PIXEL')
v2.80
import bpy
import gpu
from gpu_extras.batch import batch_for_shader

verts = [
    [0.0, 0.0],
    [0.0, 200.0],
    [200.0, 200.0],
    [200.0, 0.0],
]
indices = [
    [0, 1, 2, 3]
]

shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
batch = batch_for_shader(shader, 'TRIS', {"pos": verts},
                         indices=indices)

def draw():
    shader.bind()
    shader.uniform_float("color", (1.0, 1.0, 1.0, 1.0))
    batch.draw()

handle = bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'POST_PIXEL')

gpu モジュールで提供されるPython APIは、上記で示したAPI以外にも多く存在するため、ここではこれ以上説明しません。
詳細は、公式のドキュメント を確認してください。

gpu モジュールをバックエンドとしてBlender 2.7xの bgl モジュールのように扱えるライブラリ bglx を開発中です。開発途中で機能の追加はこれからですが、もしよければご利用ください。

タイマ関連のAPI

※ タイマ関連のAPIは、ドキュメント に記載されているのみで、現時点では利用できないようです。このため、本記事ではドキュメントを要約したものについてのみ示します。APIが利用できるようになりましたら、本記事も更新したいと思います。

Blender 2.7xでは、context.window_manager.event_timer_addmodal メソッドを利用してタイマを登録し、定期的に処理を実行することができましたが、オペレータクラスの作成やイベントの扱いを考えると、簡単に使えるものではありませんでした。
Blender 2.8では、定期的な処理を実行する仕組みをより簡単に実現できるように、新たなAPI bpy.app.timers が利用できます。
以下では、登録から5秒後に1秒ごとにカウント値を出力し、カウント数が10になった時にタイマの登録を解除するサンプルを示します。

v2.80
import bpy

counter = 0

def print_message():
    global counter
    counter = counter + 1
    print("Count: {}".format(counter))
    if counter == 10:
        print("Finished")
        return None     # counterが10になった時に、定期的な実行を終了する(タイマを登録解除する)
    return 1.0     # 1秒ごとに実行する

# 登録後5秒後に、1秒ごとにprint_messageを実行する(タイマを登録する)
bpy.app.timers.register(print_message, first_interval=5.0)

その他気が付いた変更点

ここでは、Magic UVをBlender 2.8へ移植する時に気がついた、細かいPython APIの仕様変更点をまとめます。

領域1x1のWINDOWリージョン

Blender 2.8では、横幅1pixel、縦幅1pixelの 'WINDOW' リージョンが、各エリアに追加されています。
このため、'VIEW_3D' エリアの 'WINDOW' リージョンを取得する場合は、Blender 2.8で追加された1x1のリージョンを間違って取得しないように修正しなければなりません。

v2.7x
area = None
region = None

for area in bpy.context.screen.areas:
    if area.type == 'VIEW_3D':
        break
if area:
    for region in area.regions:
        if region.type == 'WINDOW':
            break
v2.80
area = None
region = None

for area in bpy.context.screen.areas:
    if area.type == 'VIEW_3D':
        break
if area:
    for region in area.regions:
        if region.type == 'WINDOW':
            # Blender 2.8では、1x1の'WINDOW'リージョンが存在する
            if region.width <= 1 or region.height <= 1:
                continue
            break

UV/Image Editorの2D座標値

UV/Image Editorのカーソルの位置を取得するためには、'IMAGE_EDITOR' スペースの cursor_location を参照しますが、Blender 2.7xではテクスチャのサイズの分だけスケールダウンしないと、UV座標と一致しませんでした。
つまり、テクスチャの左下にカーソルがあるときは cursor_location(0.0, 0.0) を返し、右上にカーソルがあるときはテクスチャサイズ (テクスチャの横幅, テクスチャの縦幅) を返していました。
しかしBlender 2.8では、テクスチャサイズでスケールダウンしなくても、テクスチャの左下にカーソルがあるときは cursor_location(0.0, 0.0) を返し、右上にカーソルがあるときは、(1.0, 1.0) を返すようになります。

v2.7x
tex_size = area.spaces.active.image.size
loc = space.cursor_location

# テクスチャ座標からUV座標へ変換
cx = loc[0] / tex_size[0]
cy = loc[1] / tex_size[1]
v2.80
# すでにUV座標に設定されているため、テクスチャサイズを用いてUV座標へ変換する必要がない
loc = space.cursor_location

サンプル

Blender 2.7xとBlender 2.8のアドオンの実装方法の違いを確認するため、Blender 2.7xとBlender 2.8それぞれで利用可能なアイコンを全て表示するサンプルを紹介します。
アドオンの作り方の詳細については [Blender] Blenderプラグインの作り方 を参照していただき、アドオンの作り方については省略します。
なお、Blender 2.7xからBlender 2.8に移植するために変更する必要がある箇所をコメントで示しました。

Blender 2.7x

v2.7x
import bpy
from bpy.props import IntProperty


bl_info = {
    "name": "Sample Add-on in Blender 2.7x",
    "author": "Nutti",
    "version": (1, 0),
    "blender": (2, 79, 0),
    "location": "View3D > Sidebar",
    "description": "Show all icons available in Blender 2.7x",
    "warning": "",
    "support": "TESTING",
    "wiki_url": "",
    "tracker_url": "",
    "category": "User Interface"
}


def get_num_column(self):
    return self.get('num_column', ShowIconPanel.num_column)


def set_num_column(self, value):
    self['num_column'] = value
    ShowIconPanel.num_column = value


class ShowIcons(bpy.types.Operator):
    bl_idname = "object.show_all_icons"
    bl_label = "Show All Icons"
    bl_description = "Show all icons available in Blender 2.7x"
    bl_options = {'REGISTER', 'UNDO'}

    num_column = IntProperty(
        name="Number of Icons",
        description="Number of Icons in one column",
        default=2,
        min=1,
        max=5,
        get=get_num_column,
        set=set_num_column,
    )

    def draw(self, context):
        layout = self.layout
        layout.prop(self, "num_column")

    def execute(self, context):
        ShowIconPanel.show = True
        return {'FINISHED'}


class HideIcons(bpy.types.Operator):
    bl_idname = "object.hide_icons"
    bl_label = "Hide Icons"
    bl_description = "Hide icons"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        ShowIconPanel.show = False
        return {'FINISHED'}


class ShowIconPanel(bpy.types.Panel):
    bl_label = "Sample Add-on in Blender 2.7x"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'TOOLS'
    bl_category = "Show Icon"
    bl_context = "objectmode"

    num_column = 3
    show = False

    def draw_header(self, context):
        layout = self.layout
        layout.label(text="", icon='PLUGIN')

    def draw(self, context):
        layout = self.layout

        if ShowIconPanel.show:
            layout.operator(HideIcons.bl_idname, text="Hide")
            layout.separator()
            layout.label(text="Availabe Icons in Blender 2.7x:")
            icon = bpy.types.UILayout.bl_rna.functions['prop'].parameters['icon']
            for i, key in enumerate(icon.enum_items.keys()):
                if i % ShowIconPanel.num_column == 0:
                    row = layout.row()
                row.label(text=key, icon=key)
        else:
            layout.operator(ShowIcons.bl_idname, text="Show")


def register():
    bpy.utils.register_module(__name__)


def unregister():
    bpy.utils.unregister_module(__name__)


if __name__ == "__main__":
    register()

Blender 2.80

v2.80
import bpy
from bpy.props import IntProperty


bl_info = {
    "name": "Sample Add-on in Blender 2.8",
    "author": "Nutti",
    "version": (1, 0),
    "blender": (2, 80, 0),      # ★(2, 80, 0)に変更
    "location": "View3D > Sidebar",
    "description": "Show all icons available in Blender 2.8",
    "warning": "",
    "support": "TESTING",
    "wiki_url": "",
    "tracker_url": "",
    "category": "User Interface"
}


def get_num_column(self):
    return self.get('num_column', SAMPLE_PT_ShowIcon.num_column)


def set_num_column(self, value):
    self['num_column'] = value
    SAMPLE_PT_ShowIcon.num_column = value


# ★クラスの命名規則「XXX_OT_YYY」に従うように変更
class SAMPLE_OT_ShowIcons(bpy.types.Operator):
    bl_idname = "object.show_all_icons"
    bl_label = "Show All Icons"
    bl_description = "Show all icons available in Blender 2.8"
    bl_options = {'REGISTER', 'UNDO'}

    # ★型アノテーションによる記法に変更
    num_column: IntProperty(
        name="Number of Icons",
        description="Number of Icons in one column",
        default=2,
        min=1,
        max=5,
        get=get_num_column,
        set=set_num_column,
    )

    def draw(self, context):
        layout = self.layout
        layout.prop(self, "num_column")

    def execute(self, context):
        SAMPLE_PT_ShowIcon.show = True
        return {'FINISHED'}


# ★クラスの命名規則「XXX_OT_YYY」に従うように変更
class SAMPLE_OT_HideIcons(bpy.types.Operator):
    bl_idname = "object.hide_icons"
    bl_label = "Hide Icons"
    bl_description = "Hide icons"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        SAMPLE_PT_ShowIcon.show = False
        return {'FINISHED'}


# ★クラスの命名規則「XXX_PT_YYY」に従うように変更
class SAMPLE_PT_ShowIcon(bpy.types.Panel):
    bl_label = "Sample Add-on in Blender 2.8"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'       # ★Sidebarに配置
    bl_category = "Show Icon"
    bl_context = "objectmode"

    num_column = 3
    show = False

    def draw_header(self, context):
        layout = self.layout
        layout.label(text="", icon='PLUGIN')

    def draw(self, context):
        layout = self.layout

        if SAMPLE_PT_ShowIcon.show:
            layout.operator(SAMPLE_OT_HideIcons.bl_idname, text="Hide")
            layout.separator()
            layout.label(text="Availabe Icons in Blender 2.8:")
            icon = bpy.types.UILayout.bl_rna.functions['prop'].parameters['icon']
            for i, key in enumerate(icon.enum_items.keys()):
                if i % SAMPLE_PT_ShowIcon.num_column == 0:
                    row = layout.row()
                row.label(text=key, icon=key)
        else:
            layout.operator(SAMPLE_OT_ShowIcons.bl_idname, text="Show")


classes = [
    SAMPLE_OT_ShowIcons,
    SAMPLE_OT_HideIcons,
    SAMPLE_PT_ShowIcon,
]

def register():
    # ★bpy.utils.register_classを利用
    for c in classes:
        bpy.utils.register_class(c)


def unregister():
    # ★bpy.utils.unregister_classを利用
    for c in classes:
        bpy.utils.unregister_class(c)


if __name__ == "__main__":
    register()

おわりに

Blender 2.8で提供されるPython APIについて、確認できた範囲で変更点をまとめてみました。
Blender 2.8では大幅にPython APIが変更されるとアナウンスされていましたが、bgl モジュールが利用できなくなったことを除けば、Blender 2.7xのアドオンを移植すること自体は難しくはないと思います。
しかし、Blender 2.8に移植したアドオンはBlender 2.7xと互換ではなくなるため、Blender 2.8とBlender 2.7xの両方で動作するようにアドオンを作成するのは、少し難しいかもしれません。
なおMagic UVでは、Blenderのバージョンによって読み込むモジュールを制御することでこの問題に対処しているため、必要でしたらソースコードを確認してみてください。

明日24日目は、drmdrさん です。

参考情報

nutti
同人ゲーム開発、Blenderアドオン開発、DTM
https://colorful-pico.net
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away