[Blender3.3] 日本の地形モデルメッシュ追加アドオンの開発 (5): スクリプトファイルの分割
前回の終わりに書いた通り、アドオンのスクリプトファイル一つだと辛くなってきたため、ファイルを複数に分割することにしました。
分割する場合、特定のフォルダ配下に __init__.py 他、関係するスクリプトファイルを配置することになります。それで、これまでの terrain_model.py の名前を継承して、フォルダ名を terrain_model として、その下にファイルを置くことにしました。
なお、このterrain_modelフォルダ、以前(トレース情報の出力)トレース用に作成した trace.py と同じフォルダに置いてあります。なので、pythonからの自動読み込みできるように設定したフォルダの下です。
では、まず __init__.py から。
- # -*- coding: utf-8 -*-
- #
- # package: terrain_model
- # file: __init__.py
- bl_info = {
- "name": "New Terrain Model (JP)",
- "author": "Shiki Kuraga",
- "version": (1, 0),
- "blender": (3, 3, 0),
- "location": "View3D > Add > Mesh > New Terrain Model (JP)",
- "description": "Adds a new Terrain Model (JP) Mesh Object",
- "warning": "",
- "support": "TESTING",
- "doc_url": "",
- "category": "Add Mesh",
- }
- __all__ = [ 'translation', 'operator', 'model' ]
- from . import translation
- from . import operator
- import bpy
- from trace import Trace
- ## This allows you to right click on a button and link to documentation
- #def add_object_manual_map():
- # url_manual_prefix = "https://docs.blender.org/manual/en/latest/"
- # url_manual_mapping = (
- # ("bpy.ops.mesh.add_object2", "scene_layout/object/types.html"),
- # )
- # return url_manual_prefix, url_manual_mapping
- def register():
- Trace.print("register()")
- bpy.app.translations.register(__name__, translation.translation_dict)
- operator.register()
- # bpy.utils.register_manual_map(add_object_manual_map)
- def unregister():
- Trace.print("unregister()")
- operator.unregister()
- bpy.app.translations.unregister(__name__)
- # bpy.utils.unregister_manual_map(add_object_manual_map)
- if __name__ == "__main__":
- register()
次に、翻訳辞書を入れておくための translation.py です。
- # -*- coding: utf-8 -*-
- #
- # package: terrain_model
- # file: translation.py
- translation_dict = {
- "ja_JP": {
- ("*", "Latitude"):
- "緯度",
- ("*", "Longitude"):
- "経度",
- ("*", "Scope (km)"):
- "範囲長さ (km)",
- ("*", "Terrain Model (JP)"):
- "地形モデル (日本)",
- ("*", "Add Terrain Model Mesh (JP)"):
- "地形モデルの追加 (日本)"
- }
- }
三つ目ですが、メニューなど操作関係の機能を実装するための operation.py になります。
- # -*- coding: utf-8 -*-
- #
- # package: terrain_model
- # file: operator.py
- import bpy
- import math
- from bpy.types import Operator
- from bpy.props import FloatProperty, IntProperty
- from bpy_extras.object_utils import AddObjectHelper, object_data_add
- from . import model
- from trace import Trace
- def T(str):
- return bpy.app.translations.pgettext(str)
- class OBJECT_OT_add_object(Operator, AddObjectHelper):
- """Create a new Terrain Model Mesh Object"""
- bl_idname = "terrain_model.add_object"
- bl_label = T("Add Terrain Model Mesh (JP)")
- bl_options = {'REGISTER', 'UNDO'}
- prop_lat: FloatProperty(
- name = "Latitude",
- description = "Latitude",
- default = 35.0 * math.pi / 180.0,
- subtype = 'ANGLE',
- min = -0.495 * math.pi,
- max = 0.495 * math.pi,
- precision = 6
- )
- prop_lon: FloatProperty(
- name = "Longitude",
- description = "Longitude",
- default = 135.0 * math.pi / 180.0,
- subtype = 'ANGLE',
- min = -1 * math.pi,
- max = 1 * math.pi,
- precision = 6
- )
- prop_scope: FloatProperty(
- name = "Scope (km)",
- description = "Scope (km)",
- default = 1.0,
- step = 10, # actual value = 10/100
- min = 0.1
- )
- prop_zoom: IntProperty(
- name = "Zoom",
- description = "Zoom",
- default = 14,
- min = 3,
- max = 15
- )
- def __init__(self):
- Trace.print("__init__")
- def __del__(self):
- Trace.print("__del__")
- def execute(self, context):
- model.add_object(self, context)
- return {'FINISHED'}
- def invoke(self, context, event):
- wm = context.window_manager
- # invoke properties dialog for this object
- return wm.invoke_props_dialog(self)
- # Registration
- def add_object_button(self, context):
- # insert separator
- self.layout.separator()
- self.layout.operator(
- OBJECT_OT_add_object.bl_idname,
- text = T("Terrain Model (JP)"),
- icon = 'PLUGIN')
- ## This allows you to right click on a button and link to documentation
- #def add_object_manual_map():
- # url_manual_prefix = "https://docs.blender.org/manual/en/latest/"
- # url_manual_mapping = (
- # ("bpy.ops.terrain_model.add_object", "scene_layout/object/types.html"),
- # )
- # return url_manual_prefix, url_manual_mapping
- def register():
- Trace.print("operator.register()")
- bpy.utils.register_class(OBJECT_OT_add_object)
- # bpy.utils.register_manual_map(add_object_manual_map)
- bpy.types.VIEW3D_MT_mesh_add.append(add_object_button)
- def unregister():
- Trace.print("operator.unregister()")
- bpy.utils.unregister_class(OBJECT_OT_add_object)
- # bpy.utils.unregister_manual_map(add_object_manual_map)
- bpy.types.VIEW3D_MT_mesh_add.remove(add_object_button)
- if __name__ == "__main__":
- register()
最後に、地形モデルの管理とメッシュの作成を行う model.py です。
- # -*- coding: utf-8 -*-
- #
- # package: terrain_model
- # file: model.py
- import bpy
- import math
- import requests
- from bpy_extras.object_utils import object_data_add
- from mathutils import Vector
- from trace import Trace
- def T(str):
- return bpy.app.translations.pgettext(str)
- def add_object(self, context):
- Trace.print("add_object(lat=%f,lon=%f,scope=%f)" % (self.prop_lat, self.prop_lon, self.prop_scope))
- scene = context.scene
- scale_length = scene.unit_settings.scale_length
- scale_x = self.prop_scope * 1000 / scale_length #self.scale.x
- scale_y = self.prop_scope * 1000 / scale_length #self.scale.y
- zoom = self.prop_zoom
- (x, y) = TerrainTile.latlon_to_xy(self.prop_lat, self.prop_lon)
- ix = int(x * (1 << zoom))
- iy = int(y * (1 << zoom))
- tile = get_cyberjapandata(zoom, ix, iy)
- mesh = tile.create_mesh(-1 * scale_x, 1 * scale_y, 2 * scale_x, -2 * scale_y)
- # useful for development when the mesh may be invalid.
- # mesh.validate(verbose=True)
- object_data_add(context, mesh, operator = self)
- def get_cyberjapandata(zoom: int, x: int, y: int, dataid = "DEM10B"):
- """Get cyberjapandata"""
- # URL:https://cyberjapandata.gsi.go.jp/xyz/dem5a/{z}/{x}/{y}.txt(DEM5A テキスト形式)zoom: 1...15
- # URL:https://cyberjapandata.gsi.go.jp/xyz/dem5b/{z}/{x}/{y}.txt(DEM5B テキスト形式)zoom: 1...15
- # URL:https://cyberjapandata.gsi.go.jp/xyz/dem/{z}/{x}/{y}.txt(DEM10B テキスト形式) zoom: 1...14
- # check dataid
- if dataid == "DEM10B":
- (dtype, zmax) = ("dem", 14)
- elif dataid == "DEM5A":
- (dtype, zmax) = ("dem5a", 15)
- elif dataid == "DEM5B":
- (dtype, zmax) = ("dem5b", 15)
- else:
- raise ValueError("Invalid DATAID: %s" % dataid)
- # check zoom
- if type(zoom) is not int:
- raise TypeError("param zoom isn't int: %s" % type(zoom))
- elif zoom < 1 or zoom > zmax:
- raise ValueError("Invalid value of z(1-%d): %d" % (zmax, zoom))
- # check x
- tilenum = (1 << zoom)
- if type(x) is not int:
- raise TypeError("param x isn't int: %s" % type(x))
- elif x < 0 or x >= tilenum:
- Trace.print("Out of range value x(0-%d): %d" % (tilenum, x))
- return TerrainTile()
- # check y
- if type(y) is not int:
- raise TypeError("param y isn't int: %s" % type(y))
- elif y < 0 or y >= tilenum:
- Trace.print("Out of range value y(0-%d): %d" % (tilenum, y))
- return TerrainTile()
- # get data
- url = "https://cyberjapandata.gsi.go.jp/xyz/%s/%d/%d/%d.txt" % (dtype, zoom, x, y)
- Trace.print("get data from url: <%s>" % url)
- res = requests.get(url)
- s = res.content.decode()
- tile = TerrainTile(csvtext = s)
- Trace.print(tile)
- return tile
- class TerrainTile:
- """Data Handler of Terrain Tile"""
- PIXEL_NUM = 256
- def latlon_to_xy(latitude,longitude):
- x = ((math.pi + longitude) / (2 * math.pi)) % 1
- siny = math.sin(latitude)
- siny = max(-0.999, min(0.999, siny))
- y = 0.5 - math.log((1 + siny) / (1 - siny)) / (4 * math.pi)
- return (x, y)
- def __init__(self, csvtext = ""):
- self.altitudes = []
- if len(csvtext) > 0:
- self.load_from_csvtext(csvtext)
- def __str__(self):
- return "
" % len(self.altitudes) def load_from_csvtext(self, csvtext): alts = [] if len(csvtext.strip()) == 0: self.altitudes = alts return PIXEL_NUM = TerrainTile.PIXEL_NUM lines = csvtext.split('\n') if len(lines) < PIXEL_NUM: raise ValueError("num of lines is less than PIXEL_NUM(%d): %d lines" % (PIXEL_NUM, len(lines))) for i, line in enumerate(lines[:PIXEL_NUM]): cols = line.split(',') if len(cols) != PIXEL_NUM: raise ValueError("num of columns at line %d is not PIXEL_NUM(%d): %d cols" % (i + 1, PIXEL_NUM, len(cols))) for col in cols: col = col.strip() if col == "e": alts.append(None) else: alts.append(float(col)) self.altitudes = alts def create_mesh(self, start_x, start_y, width, height, name = "Terrain Model Mesh", adjust = True): offset_z = 0 if adjust: max_z = max(filter(None, self.altitudes)) min_z = min(filter(None, self.altitudes)) Trace.print("min_z = ", min_z) if min_z > 0: offset_z = -min_z elif max_z < 0: offset_z = -max_z idmap = [] verts = [] for index_y in range(0, self.PIXEL_NUM): y = start_y + (index_y + 0.5) * height / self.PIXEL_NUM for index_x, alt in enumerate(self.altitudes[(index_y * self.PIXEL_NUM):((index_y + 1) * self.PIXEL_NUM)]): if alt is not None: x = start_x + (index_x + 0.5) * width / self.PIXEL_NUM idmap.append(len(verts)) verts.append(Vector((x, y, alt + offset_z))) else: idmap.append(-1) faces = [] for index_y in range(0, self.PIXEL_NUM - 1): iy0 = index_y * self.PIXEL_NUM iy1 = iy0 + self.PIXEL_NUM for ix0, ix1 in enumerate(range(1, self.PIXEL_NUM)): face = [idmap[i] for i in [ix0 + iy0, ix1 + iy0, ix1 + iy1, ix0 + iy1] if idmap[i] >= 0] if (len(face) >= 3): faces.append(face) if len(faces) > 0: edges = [] mesh = bpy.data.meshes.new(name = name) mesh.from_pydata(verts, edges, faces) return mesh return None
内容は、これまでのものと変わりません。
しかし、今まであったトレースを開始するためのコードが __init__.py に存在しません。
というのも、__init__.py に記入して呼び出すと不都合があることが分かったからです。
テキストエディタからのスクリプト実行時、__init__.pyの内容を直接実行しようとすると、from . import translation の処理が親フォルダがないというエラーになります。(正確には、"ImportError: attempted relative import with no known parent package")
これは、__init__.py の直接実行だと、__init__.pyのあるフォルダがカレントフォルダになってしまい、親フォルダがカレントフォルダ外となりエラーとなっているようです。
そのため、__init__.py にトレース開始のコードは入れられないのです。
なので、terrain_modelフォルダの親フォルダにテキストエディタ内で走らせるためのスクリプトを別に用意しました。これを、test_terrain_model.py とします。
そのファイルの内容は以下です。
- # -*- coding: utf-8 -*-
- #
- # file: test_terrain_model.py
- package_name = 'terrain_model'
- # check if retry
- import bpy
- import sys
- import importlib
- if package_name in sys.modules.keys():
- # reload on retrying
- module_obj = sys.modules[package_name]
- module_obj.unregister()
- importlib.reload(module_obj)
- module_names = [ package_name + '.' + s for s in module_obj.__all__ ]
- modules = [ sys.modules[s] for s in sys.modules.keys() if s in module_names ]
- for module in modules:
- importlib.reload(module)
- # start testing
- module = importlib.import_module(package_name)
- for name in module.__all__:
- importlib.import_module('.' + name, package_name)
- # start testing
- from trace import Trace
- Trace.on()
- module.register()
このファイル、再利用できるように特定のパッケージに依存しないように作成しましたので、今一つ見づらいかもしれませんが、やっていることはこれまでのterrain_model.pyでトレースを開始するためのコードです。ただ、その前にパッケージ内のモジュールの再読み込みの処理を入れてます。
これで、terrain_model内のどのファイルを更新しても、このトレース開始用のコードを走らせれば再読み込みしてくれます。(面倒なので、アドオンのインストールはしないでテストしてます)
次は、本アドオンの完成に向けた手直しを進めたいかなと思ってます。
【前回(4)】日本の地形モデルメッシュ追加アドオンの開発 (5) 【次回(6)】