[Blender3.3] 日本の地形モデルメッシュ追加アドオンの開発 (5): スクリプトファイルの分割

 前回の終わりに書いた通り、アドオンのスクリプトファイル一つだと辛くなってきたため、ファイルを複数に分割することにしました。

分割する場合、特定のフォルダ配下に __init__.py 他、関係するスクリプトファイルを配置することになります。それで、これまでの terrain_model.py の名前を継承して、フォルダ名を terrain_model として、その下にファイルを置くことにしました。

なお、このterrain_modelフォルダ、以前(トレース情報の出力)トレース用に作成した trace.py と同じフォルダに置いてあります。なので、pythonからの自動読み込みできるように設定したフォルダの下です。

では、まず __init__.py から。

  1. # -*- coding: utf-8 -*-
  2. #
  3. # package: terrain_model
  4. # file: __init__.py
  5.  
  6. bl_info = {
  7. "name": "New Terrain Model (JP)",
  8. "author": "Shiki Kuraga",
  9. "version": (1, 0),
  10. "blender": (3, 3, 0),
  11. "location": "View3D > Add > Mesh > New Terrain Model (JP)",
  12. "description": "Adds a new Terrain Model (JP) Mesh Object",
  13. "warning": "",
  14. "support": "TESTING",
  15. "doc_url": "",
  16. "category": "Add Mesh",
  17. }
  18.  
  19. __all__ = [ 'translation', 'operator', 'model' ]
  20.  
  21. from . import translation
  22. from . import operator
  23.  
  24. import bpy
  25. from trace import Trace
  26.  
  27. ## This allows you to right click on a button and link to documentation
  28. #def add_object_manual_map():
  29. # url_manual_prefix = "https://docs.blender.org/manual/en/latest/"
  30. # url_manual_mapping = (
  31. # ("bpy.ops.mesh.add_object2", "scene_layout/object/types.html"),
  32. # )
  33. # return url_manual_prefix, url_manual_mapping
  34.  
  35.  
  36. def register():
  37. Trace.print("register()")
  38. bpy.app.translations.register(__name__, translation.translation_dict)
  39. operator.register()
  40. # bpy.utils.register_manual_map(add_object_manual_map)
  41.  
  42.  
  43. def unregister():
  44. Trace.print("unregister()")
  45. operator.unregister()
  46. bpy.app.translations.unregister(__name__)
  47. # bpy.utils.unregister_manual_map(add_object_manual_map)
  48.  
  49.  
  50. if __name__ == "__main__":
  51. register()

次に、翻訳辞書を入れておくための translation.py です。

  1. # -*- coding: utf-8 -*-
  2. #
  3. # package: terrain_model
  4. # file: translation.py
  5.  
  6. translation_dict = {
  7. "ja_JP": {
  8. ("*", "Latitude"):
  9. "緯度",
  10. ("*", "Longitude"):
  11. "経度",
  12. ("*", "Scope (km)"):
  13. "範囲長さ (km)",
  14. ("*", "Terrain Model (JP)"):
  15. "地形モデル (日本)",
  16. ("*", "Add Terrain Model Mesh (JP)"):
  17. "地形モデルの追加 (日本)"
  18. }
  19. }

三つ目ですが、メニューなど操作関係の機能を実装するための operation.py になります。

  1. # -*- coding: utf-8 -*-
  2. #
  3. # package: terrain_model
  4. # file: operator.py
  5.  
  6. import bpy
  7. import math
  8. from bpy.types import Operator
  9. from bpy.props import FloatProperty, IntProperty
  10. from bpy_extras.object_utils import AddObjectHelper, object_data_add
  11. from . import model
  12. from trace import Trace
  13.  
  14. def T(str):
  15. return bpy.app.translations.pgettext(str)
  16.  
  17. class OBJECT_OT_add_object(Operator, AddObjectHelper):
  18. """Create a new Terrain Model Mesh Object"""
  19. bl_idname = "terrain_model.add_object"
  20. bl_label = T("Add Terrain Model Mesh (JP)")
  21. bl_options = {'REGISTER', 'UNDO'}
  22.  
  23. prop_lat: FloatProperty(
  24. name = "Latitude",
  25. description = "Latitude",
  26. default = 35.0 * math.pi / 180.0,
  27. subtype = 'ANGLE',
  28. min = -0.495 * math.pi,
  29. max = 0.495 * math.pi,
  30. precision = 6
  31. )
  32.  
  33. prop_lon: FloatProperty(
  34. name = "Longitude",
  35. description = "Longitude",
  36. default = 135.0 * math.pi / 180.0,
  37. subtype = 'ANGLE',
  38. min = -1 * math.pi,
  39. max = 1 * math.pi,
  40. precision = 6
  41. )
  42.  
  43. prop_scope: FloatProperty(
  44. name = "Scope (km)",
  45. description = "Scope (km)",
  46. default = 1.0,
  47. step = 10, # actual value = 10/100
  48. min = 0.1
  49. )
  50.  
  51. prop_zoom: IntProperty(
  52. name = "Zoom",
  53. description = "Zoom",
  54. default = 14,
  55. min = 3,
  56. max = 15
  57. )
  58. def __init__(self):
  59. Trace.print("__init__")
  60. def __del__(self):
  61. Trace.print("__del__")
  62.  
  63. def execute(self, context):
  64.  
  65. model.add_object(self, context)
  66.  
  67. return {'FINISHED'}
  68.  
  69. def invoke(self, context, event):
  70. wm = context.window_manager
  71. # invoke properties dialog for this object
  72. return wm.invoke_props_dialog(self)
  73.  
  74. # Registration
  75.  
  76. def add_object_button(self, context):
  77. # insert separator
  78. self.layout.separator()
  79. self.layout.operator(
  80. OBJECT_OT_add_object.bl_idname,
  81. text = T("Terrain Model (JP)"),
  82. icon = 'PLUGIN')
  83.  
  84.  
  85. ## This allows you to right click on a button and link to documentation
  86. #def add_object_manual_map():
  87. # url_manual_prefix = "https://docs.blender.org/manual/en/latest/"
  88. # url_manual_mapping = (
  89. # ("bpy.ops.terrain_model.add_object", "scene_layout/object/types.html"),
  90. # )
  91. # return url_manual_prefix, url_manual_mapping
  92.  
  93.  
  94. def register():
  95. Trace.print("operator.register()")
  96. bpy.utils.register_class(OBJECT_OT_add_object)
  97. # bpy.utils.register_manual_map(add_object_manual_map)
  98. bpy.types.VIEW3D_MT_mesh_add.append(add_object_button)
  99.  
  100.  
  101. def unregister():
  102. Trace.print("operator.unregister()")
  103. bpy.utils.unregister_class(OBJECT_OT_add_object)
  104. # bpy.utils.unregister_manual_map(add_object_manual_map)
  105. bpy.types.VIEW3D_MT_mesh_add.remove(add_object_button)
  106.  
  107.  
  108. if __name__ == "__main__":
  109. register()

最後に、地形モデルの管理とメッシュの作成を行う model.py です。

  1. # -*- coding: utf-8 -*-
  2. #
  3. # package: terrain_model
  4. # file: model.py
  5.  
  6. import bpy
  7. import math
  8. import requests
  9. from bpy_extras.object_utils import object_data_add
  10. from mathutils import Vector
  11. from trace import Trace
  12.  
  13. def T(str):
  14. return bpy.app.translations.pgettext(str)
  15.  
  16. def add_object(self, context):
  17. Trace.print("add_object(lat=%f,lon=%f,scope=%f)" % (self.prop_lat, self.prop_lon, self.prop_scope))
  18. scene = context.scene
  19. scale_length = scene.unit_settings.scale_length
  20.  
  21. scale_x = self.prop_scope * 1000 / scale_length #self.scale.x
  22. scale_y = self.prop_scope * 1000 / scale_length #self.scale.y
  23.  
  24. zoom = self.prop_zoom
  25. (x, y) = TerrainTile.latlon_to_xy(self.prop_lat, self.prop_lon)
  26. ix = int(x * (1 << zoom))
  27. iy = int(y * (1 << zoom))
  28. tile = get_cyberjapandata(zoom, ix, iy)
  29. mesh = tile.create_mesh(-1 * scale_x, 1 * scale_y, 2 * scale_x, -2 * scale_y)
  30. # useful for development when the mesh may be invalid.
  31. # mesh.validate(verbose=True)
  32. object_data_add(context, mesh, operator = self)
  33.  
  34.  
  35. def get_cyberjapandata(zoom: int, x: int, y: int, dataid = "DEM10B"):
  36. """Get cyberjapandata"""
  37. # URL:https://cyberjapandata.gsi.go.jp/xyz/dem5a/{z}/{x}/{y}.txt(DEM5A テキスト形式)zoom: 1...15
  38. # URL:https://cyberjapandata.gsi.go.jp/xyz/dem5b/{z}/{x}/{y}.txt(DEM5B テキスト形式)zoom: 1...15
  39. # URL:https://cyberjapandata.gsi.go.jp/xyz/dem/{z}/{x}/{y}.txt(DEM10B テキスト形式) zoom: 1...14
  40.  
  41. # check dataid
  42. if dataid == "DEM10B":
  43. (dtype, zmax) = ("dem", 14)
  44. elif dataid == "DEM5A":
  45. (dtype, zmax) = ("dem5a", 15)
  46. elif dataid == "DEM5B":
  47. (dtype, zmax) = ("dem5b", 15)
  48. else:
  49. raise ValueError("Invalid DATAID: %s" % dataid)
  50. # check zoom
  51. if type(zoom) is not int:
  52. raise TypeError("param zoom isn't int: %s" % type(zoom))
  53. elif zoom < 1 or zoom > zmax:
  54. raise ValueError("Invalid value of z(1-%d): %d" % (zmax, zoom))
  55. # check x
  56. tilenum = (1 << zoom)
  57. if type(x) is not int:
  58. raise TypeError("param x isn't int: %s" % type(x))
  59. elif x < 0 or x >= tilenum:
  60. Trace.print("Out of range value x(0-%d): %d" % (tilenum, x))
  61. return TerrainTile()
  62.  
  63. # check y
  64. if type(y) is not int:
  65. raise TypeError("param y isn't int: %s" % type(y))
  66. elif y < 0 or y >= tilenum:
  67. Trace.print("Out of range value y(0-%d): %d" % (tilenum, y))
  68. return TerrainTile()
  69. # get data
  70. url = "https://cyberjapandata.gsi.go.jp/xyz/%s/%d/%d/%d.txt" % (dtype, zoom, x, y)
  71. Trace.print("get data from url: <%s>" % url)
  72. res = requests.get(url)
  73. s = res.content.decode()
  74. tile = TerrainTile(csvtext = s)
  75. Trace.print(tile)
  76. return tile
  77.  
  78.  
  79. class TerrainTile:
  80. """Data Handler of Terrain Tile"""
  81. PIXEL_NUM = 256
  82. def latlon_to_xy(latitude,longitude):
  83. x = ((math.pi + longitude) / (2 * math.pi)) % 1
  84. siny = math.sin(latitude)
  85. siny = max(-0.999, min(0.999, siny))
  86. y = 0.5 - math.log((1 + siny) / (1 - siny)) / (4 * math.pi)
  87. return (x, y)
  88. def __init__(self, csvtext = ""):
  89. self.altitudes = []
  90. if len(csvtext) > 0:
  91. self.load_from_csvtext(csvtext)
  92.  
  93. def __str__(self):
  94. return "" % len(self.altitudes)
  95. def load_from_csvtext(self, csvtext):
  96. alts = []
  97. if len(csvtext.strip()) == 0:
  98. self.altitudes = alts
  99. return
  100. PIXEL_NUM = TerrainTile.PIXEL_NUM
  101. lines = csvtext.split('\n')
  102. if len(lines) < PIXEL_NUM:
  103. raise ValueError("num of lines is less than PIXEL_NUM(%d): %d lines" % (PIXEL_NUM, len(lines)))
  104. for i, line in enumerate(lines[:PIXEL_NUM]):
  105. cols = line.split(',')
  106. if len(cols) != PIXEL_NUM:
  107. raise ValueError("num of columns at line %d is not PIXEL_NUM(%d): %d cols" % (i + 1, PIXEL_NUM, len(cols)))
  108. for col in cols:
  109. col = col.strip()
  110. if col == "e":
  111. alts.append(None)
  112. else:
  113. alts.append(float(col))
  114. self.altitudes = alts
  115. def create_mesh(self, start_x, start_y, width, height, name = "Terrain Model Mesh", adjust = True):
  116. offset_z = 0
  117. if adjust:
  118. max_z = max(filter(None, self.altitudes))
  119. min_z = min(filter(None, self.altitudes))
  120. Trace.print("min_z = ", min_z)
  121. if min_z > 0:
  122. offset_z = -min_z
  123. elif max_z < 0:
  124. offset_z = -max_z
  125. idmap = []
  126. verts = []
  127. for index_y in range(0, self.PIXEL_NUM):
  128. y = start_y + (index_y + 0.5) * height / self.PIXEL_NUM
  129. for index_x, alt in enumerate(self.altitudes[(index_y * self.PIXEL_NUM):((index_y + 1) * self.PIXEL_NUM)]):
  130. if alt is not None:
  131. x = start_x + (index_x + 0.5) * width / self.PIXEL_NUM
  132. idmap.append(len(verts))
  133. verts.append(Vector((x, y, alt + offset_z)))
  134. else:
  135. idmap.append(-1)
  136. faces = []
  137. for index_y in range(0, self.PIXEL_NUM - 1):
  138. iy0 = index_y * self.PIXEL_NUM
  139. iy1 = iy0 + self.PIXEL_NUM
  140. for ix0, ix1 in enumerate(range(1, self.PIXEL_NUM)):
  141. face = [idmap[i] for i in [ix0 + iy0, ix1 + iy0, ix1 + iy1, ix0 + iy1] if idmap[i] >= 0]
  142. if (len(face) >= 3):
  143. faces.append(face)
  144. if len(faces) > 0:
  145. edges = []
  146. mesh = bpy.data.meshes.new(name = name)
  147. mesh.from_pydata(verts, edges, faces)
  148. return mesh
  149. 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 とします。

そのファイルの内容は以下です。

  1. # -*- coding: utf-8 -*-
  2. #
  3. # file: test_terrain_model.py
  4.  
  5. package_name = 'terrain_model'
  6.  
  7. # check if retry
  8. import bpy
  9. import sys
  10. import importlib
  11.  
  12. if package_name in sys.modules.keys():
  13. # reload on retrying
  14. module_obj = sys.modules[package_name]
  15. module_obj.unregister()
  16. importlib.reload(module_obj)
  17. module_names = [ package_name + '.' + s for s in module_obj.__all__ ]
  18. modules = [ sys.modules[s] for s in sys.modules.keys() if s in module_names ]
  19. for module in modules:
  20. importlib.reload(module)
  21.  
  22. # start testing
  23. module = importlib.import_module(package_name)
  24. for name in module.__all__:
  25. importlib.import_module('.' + name, package_name)
  26.  
  27. # start testing
  28. from trace import Trace
  29. Trace.on()
  30.  
  31. module.register()

このファイル、再利用できるように特定のパッケージに依存しないように作成しましたので、今一つ見づらいかもしれませんが、やっていることはこれまでのterrain_model.pyでトレースを開始するためのコードです。ただ、その前にパッケージ内のモジュールの再読み込みの処理を入れてます。

これで、terrain_model内のどのファイルを更新しても、このトレース開始用のコードを走らせれば再読み込みしてくれます。(面倒なので、アドオンのインストールはしないでテストしてます)

次は、本アドオンの完成に向けた手直しを進めたいかなと思ってます。

前回(4)】日本の地形モデルメッシュ追加アドオンの開発 (5) 【次回(6)


このブログの人気の投稿

パズドラ 12月のクエスト(Lv12) ネフティスさん、出番です

日本酒 烏輪 緑のたいよう 純米酒 無濾過生原酒

[Blender3.3] mmd_toolsはどれが最新?