From 7c922d2320cd5cd930282e4912889cbd65b3376b Mon Sep 17 00:00:00 2001 From: Bill Currie Date: Wed, 15 Apr 2020 00:24:20 +0900 Subject: [PATCH] Update the blender map editor for 2.80+ --- tools/io_qfmap/__init__.py | 339 +++++++--------------------------- tools/io_qfmap/entity.py | 89 +++++---- tools/io_qfmap/entityclass.py | 2 + tools/io_qfmap/import_map.py | 87 +++++---- tools/io_qfmap/init.py | 294 +++++++++++++++++++++++++++++ tools/io_qfmap/map.py | 4 +- 6 files changed, 468 insertions(+), 347 deletions(-) create mode 100644 tools/io_qfmap/init.py diff --git a/tools/io_qfmap/__init__.py b/tools/io_qfmap/__init__.py index 953750b63..2cabf517f 100644 --- a/tools/io_qfmap/__init__.py +++ b/tools/io_qfmap/__init__.py @@ -24,7 +24,7 @@ bl_info = { "name": "Quake map format", "author": "Bill Currie", - "blender": (2, 6, 3), + "blender": (2, 80, 0), "api": 35622, "location": "File > Import-Export", "description": "Import-Export Quake maps", @@ -34,286 +34,83 @@ bl_info = { # "support": 'OFFICIAL', "category": "Import-Export"} -# To support reload properly, try to access a package var, if it's there, -# reload everything -if "bpy" in locals(): - import imp - if "import_map" in locals(): - imp.reload(import_map) - if "export_map" in locals(): - imp.reload(export_map) - -from pprint import pprint +submodule_names = ( + "entity", + "entityclass", + "export_map", + "import_map", + "init", + "map", + "qfplist", + "quakechr", + "quakepal", + "wad", +) import bpy -from bpy.props import BoolProperty, FloatProperty, StringProperty, EnumProperty -from bpy.props import FloatVectorProperty, PointerProperty -from bpy_extras.io_utils import ExportHelper, ImportHelper, path_reference_mode, axis_conversion -from bpy.app.handlers import persistent +from bpy.props import PointerProperty +from bpy.utils import register_class, unregister_class -from .entityclass import EntityClassDict, EntityClassError -from . import entity -from . import import_map -from . import export_map +import importlib +import sys -def ecm_draw(self, context): - layout = self.layout - for item in self.menu_items: - if type(item[1]) is str: - ec = context.scene.qfmap.entity_classes[item[1]] - if ec.size: - icon = 'OBJECT_DATA' - else: - icon = 'MESH_DATA' - op = layout.operator("object.add_entity", text=item[0], icon=icon) - op.entclass=item[1] - if ec.comment: - pass - else: - layout.menu(item[1].bl_idname) +registered_submodules = [] -class EntityClassMenu: - @classmethod - def clear(cls): - while cls.menu_items: - if type(cls.menu_item[0][1]) is not str: - bpy.utils.unregister_class(cls.menu_items[0][1]) - cls.menu_items[0][1].clear() - del cls.menu_items[0] - @classmethod - def build(cls, menudict, name="INFO_MT_entity_add", label="entity"): - items = list(menudict.items()) - items.sort() - menu_items = [] - for i in items: - i = list(i) - if type(i[1]) is dict: - if i[0]: - nm = "_".join((name, i[0])) - else: - nm = name - i[1] = cls.build(i[1], nm, i[0]) - menu_items.append(i) - attrs = {} - attrs["menu_items"] = menu_items - attrs["draw"] = ecm_draw - attrs["bl_idname"] = name - attrs["bl_label"] = label - attrs["clear"] = cls.clear - menu = type(name, (bpy.types.Menu,), attrs) - bpy.utils.register_class(menu) - return menu +# When the addon is reloaded, this module gets reloaded, however none +# of the other modules from this addon get reloaded. As a result, they +# don't call register_submodules (only run when the module is loaded) and +# thus they don't end up registering everything. +# +# This is set before any loading starts (in register), to a set of all the +# names of the modules loaded as of when loading starts. While doing the +# module loading, check if a module is present in this list. If so, reload +# it and remove it from the set (to prevent it from getting reloaded twice). +preloaded_modules = None -@persistent -def scene_load_handler(dummy): - for scene in bpy.data.scenes: - if hasattr(scene, "qfmap"): - scene.qfmap.script_update(bpy.context) - -class MapeditMessage(bpy.types.Operator): - bl_idname = "qfmapedit.message" - bl_label = "Message" - type = StringProperty() - message = StringProperty() - - def execute(self, context): - self.report({'INFO'}, message) - return {'FINISHED'} - def invoke(self, context, event): - wm = context.window_manager - return wm.invoke_popup(self, width=400, height=200) - def draw(self, context): - self.layout.label(self.type, icon='ERROR') - self.layout.label(self.message) - -def scan_entity_classes(context): - qfmap = context.scene.qfmap - if not qfmap.dirpath: - return - qfmap.entity_classes.from_source_tree(qfmap.dirpath) - name = context.scene.name + '-EntityClasses' - if name in bpy.data.texts: - txt = bpy.data.texts[name] - else: - txt = bpy.data.texts.new(name) - txt.from_string(qfmap.entity_classes.to_plist()) - qfmap.script = name - -def parse_entity_classes(context): - context.scene.qfmap.script_update(context) - -def ec_dir_update(self, context): - try: - scan_entity_classes(context) - except EntityClassError as err: - self.dirpath = "" - bpy.ops.qfmapedit.message('INVOKE_DEFAULT', type="Error", - message="Entity Class Error: %s" % err) - -def ec_script_update(self, context): - self.script_update(context) - -class AddEntity(bpy.types.Operator): - '''Add an entity''' - bl_idname = "object.add_entity" - bl_label = "Entity" - entclass = StringProperty(name = "entclass") - - def execute(self, context): - keywords = self.as_keywords() - return entity.add_entity(self, context, **keywords) - -class QFEntityClassScan(bpy.types.Operator): - '''Rescan the specified QuakeC source tree''' - bl_idname = "scene.scan_entity_classes" - bl_label = "RELOAD" - - def execute(self, context): - scan_entity_classes(context) - return {'FINISHED'} - -class QFEntityClassParse(bpy.types.Operator): - '''Reparse the specified entity class script''' - bl_idname = "scene.parse_entity_classes" - bl_label = "RELOAD" - - def execute(self, context): - parse_entity_classes(context) - return {'FINISHED'} - -class QFEntityClasses(bpy.types.PropertyGroup): - wadpath = StringProperty( - name="wadpath", - description="Path to search for wad files", - subtype='DIR_PATH') - dirpath = StringProperty( - name="dirpath", - description="Path to qc source tree", - subtype='DIR_PATH', - update=ec_dir_update) - script = StringProperty( - name="script", - description="entity class storage", - update=ec_script_update) - entity_classes = EntityClassDict() - ecm = EntityClassMenu.build({}) - entity_targets = {} - target_entities = [] - - def script_update(self, context): - if self.script in bpy.data.texts: - script = bpy.data.texts[self.script].as_string() - self.entity_classes.from_plist(script) - menudict = {} - entclasses = self.entity_classes.keys() - for ec in entclasses: - ecsub = ec.split("_") - d = menudict - for sub in ecsub[:-1]: - if sub not in d: - d[sub] = {} - elif type(d[sub]) is str: - d[sub] = {"":d[sub]} - d = d[sub] - sub = ecsub[-1] - if sub in d: - d[sub][""] = ec - else: - d[sub] = ec - self.__class__.ecm = EntityClassMenu.build(menudict) - -class OBJECT_PT_QFECPanel(bpy.types.Panel): - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' - bl_context = 'scene' - bl_label = 'QF Entity Classes' - - @classmethod - def poll(cls, context): - return True - - def draw(self, context): - layout = self.layout - scene = context.scene - row = layout.row() - layout.prop(scene.qfmap, "wadpath") - row = layout.row() - row.prop(scene.qfmap, "dirpath") - row.operator("scene.scan_entity_classes", text="", icon="FILE_REFRESH") - row = layout.row() - row.prop(scene.qfmap, "script") - row.operator("scene.parse_entity_classes", text="", icon="FILE_REFRESH") - -class ImportPoints(bpy.types.Operator, ImportHelper): - '''Load a Quake points File''' - bl_idname = "import_mesh.quake_points" - bl_label = "Import points" - - filename_ext = ".pts" - filter_glob = StringProperty(default="*.pts", options={'HIDDEN'}) - - def execute(self, context): - keywords = self.as_keywords (ignore=("filter_glob",)) - return import_map.import_pts(self, context, **keywords) - -class ImportMap(bpy.types.Operator, ImportHelper): - '''Load a Quake map File''' - bl_idname = "import_mesh.quake_map" - bl_label = "Import map" - - filename_ext = ".map" - filter_glob = StringProperty(default="*.map", options={'HIDDEN'}) - - def execute(self, context): - keywords = self.as_keywords (ignore=("filter_glob",)) - return import_map.import_map(self, context, **keywords) - -class ExportMap(bpy.types.Operator, ExportHelper): - '''Save a Quake map File''' - - bl_idname = "export_mesh.quake_map" - bl_label = "Export map" - - filename_ext = ".map" - filter_glob = StringProperty(default="*.map", options={'HIDDEN'}) - - @classmethod - def poll(cls, context): - return True - - def execute(self, context): - keywords = self.as_keywords (ignore=("check_existing", "filter_glob")) - return export_map.export_map(self, context, **keywords) - -def menu_func_import(self, context): - self.layout.operator(ImportMap.bl_idname, text="Quake map (.map)") - self.layout.operator(ImportPoints.bl_idname, text="Quake points (.pts)") - -def menu_func_export(self, context): - self.layout.operator(ExportMap.bl_idname, text="Quake map (.map)") - -def menu_func_add(self, context): - self.layout.menu(context.scene.qfmap.ecm.bl_idname, icon='PLUGIN') +def register_submodules(name, submodule_names): + global preloaded_modules + module = __import__(name=name, fromlist=submodule_names) + submodules = [getattr(module, name) for name in submodule_names] + for mod in submodules: + # Look through the modules present when register was called. If this + # module was already loaded, then reload it. + if mod.__name__ in preloaded_modules: + mod = importlib.reload(mod) + # Prevent the module from getting reloaded more than once + preloaded_modules.remove(mod.__name__) + m = [(),(),()] + if hasattr(mod, "classes_to_register"): + m[0] = mod.classes_to_register + for cls in mod.classes_to_register: + register_class(cls) + if hasattr(mod, "menus_to_register"): + m[1] = mod.menus_to_register + for menu in mod.menus_to_register: + menu[0].append(menu[1]) + if hasattr(mod, "custom_properties_to_register"): + for prop in mod.custom_properties_to_register: + setattr(prop[0], prop[1], PointerProperty(type=prop[2])) + if hasattr(mod, "handlers_to_register"): + m[2] = mod.handlers_to_register + for handler in mod.handlers_to_register: + getattr(bpy.app.handlers, handler[0]).append(handler[1]) + if m[0] or m[1] or m[2]: + registered_submodules.append(m) def register(): - bpy.utils.register_module(__name__) - - bpy.types.Scene.qfmap = PointerProperty(type=QFEntityClasses) - - bpy.types.INFO_MT_file_import.append(menu_func_import) - bpy.types.INFO_MT_file_export.append(menu_func_export) - bpy.types.INFO_MT_add.append(menu_func_add) - - bpy.app.handlers.load_post.append(scene_load_handler) - entity.register() - + global preloaded_modules + preloaded_modules = set(sys.modules.keys()) + register_submodules(__name__, submodule_names) + preloaded_modules = None def unregister(): - bpy.utils.unregister_module(__name__) - - bpy.types.INFO_MT_file_import.remove(menu_func_import) - bpy.types.INFO_MT_file_export.remove(menu_func_export) - bpy.types.INFO_MT_add.remove(menu_func_add) + for mod in reversed(registered_submodules): + for handler in reversed(mod[2]): + getattr(bpy.app.handlers, handler[0]).remove(handler[1]) + for menu in reversed(mod[1]): + menu[0].remove(menu[1]) + for cls in reversed(mod[0]): + unregister_class(cls) if __name__ == "__main__": register() diff --git a/tools/io_qfmap/entity.py b/tools/io_qfmap/entity.py index 23f151f7e..86a316234 100644 --- a/tools/io_qfmap/entity.py +++ b/tools/io_qfmap/entity.py @@ -19,7 +19,8 @@ # -import bpy, bgl +import bpy, bgl, gpu +from gpu_extras.batch import batch_for_shader from bpy.props import BoolProperty, FloatProperty, StringProperty, EnumProperty from bpy.props import BoolVectorProperty, CollectionProperty, PointerProperty from bpy.props import FloatVectorProperty, IntProperty @@ -27,7 +28,8 @@ from mathutils import Vector from .entityclass import EntityClass -def draw_callback(self, context): + +def build_batch(qfmap): def obj_location(obj): ec = None if obj.qfentity.classname in entity_classes: @@ -38,13 +40,13 @@ def draw_callback(self, context): for i in range(8): loc += Vector(obj.bound_box[i]) return obj.location + loc/8.0 - qfmap = context.scene.qfmap entity_classes = qfmap.entity_classes entity_targets = qfmap.entity_targets target_entities = qfmap.target_entities - bgl.glLineWidth(3) ents = 0 targs = 0 + verts = [] + colors = [] for obj in target_entities: #obj = bpy.data.objects[objname] qfentity = obj.qfentity @@ -54,27 +56,37 @@ def draw_callback(self, context): ec = entity_classes[qfentity.classname] target = None killtarget = None - for field in qfentity.fields: - if field.name == "target" and field.value: - target = field.value - if field.name == "killtarget" and field.value: - killtarget = field.value + if "target" in qfentity.fields: + target = qfentity.fields["target"].value + if "killtarget" in qfentity.fields: + killtarget = qfentity.fields["killtarget"].value targetlist = [target, killtarget] if target == killtarget: del targetlist[1] for tname in targetlist: if tname and tname in entity_targets: targets = entity_targets[tname] - bgl.glColor4f(ec.color[0], ec.color[1], ec.color[2], 1) + color = (ec.color[0], ec.color[1], ec.color[2], 1) for ton in targets: targs += 1 to = bpy.data.objects[ton] - bgl.glBegin(bgl.GL_LINE_STRIP) - loc = obj_location(obj) - bgl.glVertex3f(loc.x, loc.y, loc.z) - loc = obj_location(to) - bgl.glVertex3f(loc.x, loc.y, loc.z) - bgl.glEnd() + start = obj_location(obj) + end = obj_location(to) + + verts.append(start) + colors.append(color) + verts.append(end) + colors.append(color) + return {"pos": verts, "color": colors} + +def draw_callback(self, context): + #FIXME horribly inefficient + qfmap = context.scene.qfmap + content = build_batch(qfmap) + shader = gpu.shader.from_builtin('3D_SMOOTH_COLOR') + batch = batch_for_shader(shader, 'LINES', content) + bgl.glLineWidth(3) + batch.draw(shader) bgl.glLineWidth(1) class VIEW3D_PT_QFEntityRelations(bpy.types.Panel): @@ -103,27 +115,27 @@ class VIEW3D_PT_QFEntityRelations(bpy.types.Panel): class OBJECT_UL_EntityField_list(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - layout.label(item.name) - layout.label(item.value) + layout.label(text=item.name) + layout.label(text=item.value) def qfentity_items(self, context): qfmap = context.scene.qfmap entclasses = qfmap.entity_classes eclist = list(entclasses.keys()) eclist.sort() - enum = (('', "--", ""),) + enum = (('.', "--", ""),) enum += tuple(map(lambda ec: (ec, ec, ""), eclist)) return enum class QFEntityProp(bpy.types.PropertyGroup): - value = StringProperty(name="") - template_list_controls = StringProperty(default="value", options={'HIDDEN'}) + value: StringProperty(name="") + template_list_controls: StringProperty(default="value", options={'HIDDEN'}) class QFEntity(bpy.types.PropertyGroup): - classname = EnumProperty(items = qfentity_items, name = "Entity Class") - flags = BoolVectorProperty(size=12) - fields = CollectionProperty(type=QFEntityProp, name="Fields") - field_idx = IntProperty() + classname: EnumProperty(items = qfentity_items, name = "Entity Class") + flags: BoolVectorProperty(size=12) + fields: CollectionProperty(type=QFEntityProp, name="Fields") + field_idx: IntProperty() class QFEntpropAdd(bpy.types.Operator): '''Add an entity field/value pair''' @@ -188,7 +200,7 @@ class OBJECT_PT_EntityPanel(bpy.types.Panel): box=layout.box() lines = reflow_text(ec.comment, 40) for l in lines: - box.label(l) + box.label(text=l) row = layout.row() for c in range(3): col = row.column() @@ -201,8 +213,8 @@ class OBJECT_PT_EntityPanel(bpy.types.Panel): col.template_list("OBJECT_UL_EntityField_list", "", qfentity, "fields", qfentity, "field_idx", rows=3) col = row.column(align=True) - col.operator("object.entprop_add", icon='ZOOMIN', text="") - col.operator("object.entprop_remove", icon='ZOOMOUT', text="") + col.operator("object.entprop_add", icon='ADD', text="") + col.operator("object.entprop_remove", icon='REMOVE', text="") if len(qfentity.fields) > qfentity.field_idx >= 0: row = layout.row() field = qfentity.fields[qfentity.field_idx] @@ -246,8 +258,7 @@ def entity_box(entityclass): mesh = bpy.data.meshes.new(name) mesh.from_pydata(verts, [], faces) mat = bpy.data.materials.new(name) - mat.diffuse_color = color - mat.use_raytrace = False + mat.diffuse_color = color + (1,) mesh.materials.append(mat) return mesh @@ -258,7 +269,7 @@ def set_entity_props(obj, ent): qfe.classname = ent.d["classname"] except TypeError: #FIXME hmm, maybe an enum wasn't the most brilliant idea? - qfe.classname + qfe.classname = '' if "spawnflags" in ent.d: flags = int(float(ent.d["spawnflags"])) for i in range(12): @@ -296,5 +307,17 @@ def add_entity(self, context, entclass): context.user_preferences.edit.use_global_undo = True return {'FINISHED'} -def register(): - bpy.types.Object.qfentity = PointerProperty(type=QFEntity) +classes_to_register = ( + VIEW3D_PT_QFEntityRelations, + OBJECT_UL_EntityField_list, + QFEntityProp, + QFEntity, + QFEntpropAdd, + QFEntpropRemove, + OBJECT_PT_EntityPanel, +) +menus_to_register = ( +) +custom_properties_to_register = ( + (bpy.types.Object, "qfentity", QFEntity), +) diff --git a/tools/io_qfmap/entityclass.py b/tools/io_qfmap/entityclass.py index f659c5d53..c7b9cbd0e 100644 --- a/tools/io_qfmap/entityclass.py +++ b/tools/io_qfmap/entityclass.py @@ -140,6 +140,8 @@ class EntityClassDict: def __len__(self): return self.entity_classes.__len__() def __getitem__(self, key): + if key == '.': + return EntityClass.null() return self.entity_classes.__getitem__(key) def __iter__(self): return self.entity_classes.__iter__() diff --git a/tools/io_qfmap/import_map.py b/tools/io_qfmap/import_map.py index 7d22912a0..c247d4920 100644 --- a/tools/io_qfmap/import_map.py +++ b/tools/io_qfmap/import_map.py @@ -61,9 +61,8 @@ def load_material(tx): if tx.name in bpy.data.materials: return bpy.data.materials[tx.name] mat = bpy.data.materials.new(tx.name) - mat.diffuse_color = (1, 1, 1) + mat.diffuse_color = (1, 1, 1, 1) mat.specular_intensity = 0 - mat.use_raytrace = False if tx.image: tex = bpy.data.textures.new(tx.name, 'IMAGE') tex.extension = 'REPEAT' @@ -77,19 +76,23 @@ def load_material(tx): return mat def load_textures(texdefs, wads): + class MT: + def __init__(self, x, y): + self.width = x + self.height = y for tx in texdefs: if hasattr(tx, "miptex"): continue - try: - tx.miptex = wads[0].getData(tx.name) - tx.image = load_image(tx) - except KeyError: - class MT: - def __init__(self, x, y): - self.width = x - self.height = y + if not wads or not wads[0]: tx.miptex = MT(64,64) tx.image = None + else: + try: + tx.miptex = wads[0].getData(tx.name) + tx.image = load_image(tx) + except KeyError: + tx.miptex = MT(64,64) + tx.image = None tx.material = load_material(tx) def build_uvs(verts, faces, texdefs): @@ -110,7 +113,7 @@ def process_entity(ent, wads): classname = ent.d["classname"] name = classname if "classname" in ent.d and ent.d["classname"][:5] == "light": - light = bpy.data.lamps.new("light", 'POINT') + light = bpy.data.lights.new("light", 'POINT') if "light" in ent.d: light.distance = float(ent.d["light"]) elif "_light" in ent.d: @@ -150,7 +153,7 @@ def process_entity(ent, wads): tx.matindex = len(mesh.materials) mesh.materials.append(tx.material) mesh.from_pydata(verts, [], faces) - uvlay = mesh.uv_textures.new(name) + """uvlay = mesh.uv_textures.new(name) uvloop = mesh.uv_layers[0] for i, texpoly in enumerate(uvlay.data): poly = mesh.polygons[i] @@ -159,7 +162,7 @@ def process_entity(ent, wads): texpoly.image = tx.image poly.material_index = tx.matindex for j, k in enumerate(poly.loop_indices): - uvloop.data[k].uv = uv[j] + uvloop.data[k].uv = uv[j]"""#FIXME mesh.update() obj = bpy.data.objects.new(name, mesh) else: @@ -189,50 +192,52 @@ def process_entity(ent, wads): del ent.d["angles"] obj.rotation_mode = 'XZY' obj.rotation_euler = angles * pi / 180 - bpy.context.scene.objects.link(obj) - bpy.context.scene.objects.active=obj - obj.select = True + bpy.context.layer_collection.collection.objects.link(obj) + bpy.context.view_layer.objects.active = obj + obj.select_set(True) set_entity_props(obj, ent) def import_map(operator, context, filepath): - bpy.context.user_preferences.edit.use_global_undo = False - - for obj in bpy.context.scene.objects: - obj.select = False + undo = bpy.context.preferences.edit.use_global_undo + bpy.context.preferences.edit.use_global_undo = False try: + for obj in bpy.context.scene.objects: + obj.select_set(False) entities = parse_map (filepath) except MapError as err: operator.report({'ERROR'}, repr(err)) return {'CANCELLED'} - wads=[] - if entities: - if "_wad" in entities[0].d: - wads = entities[0].d["_wad"].split(";") - elif "wad" in entities[0].d: - wads = entities[0].d["wad"].split(";") - wadpath = bpy.context.scene.qfmap.wadpath - for i in range(len(wads)): - try: - wads[i] = WadFile.load(os.path.join(wadpath, wads[i])) - except IOError: + else: + wads=[] + if entities: + if "_wad" in entities[0].d: + wads = entities[0].d["_wad"].split(";") + elif "wad" in entities[0].d: + wads = entities[0].d["wad"].split(";") + wadpath = bpy.context.scene.qfmap.wadpath + for i in range(len(wads)): try: - wads[i] = WadFile.load(os.path.join(wadpath, - os.path.basename(wads[i]))) + wads[i] = WadFile.load(os.path.join(wadpath, wads[i])) except IOError: - #give up - operator.report({'INFO'}, "Cant't find %s" % wads[i]) - wads[i] = None - for ent in entities: - process_entity(ent, wads) - bpy.context.user_preferences.edit.use_global_undo = True + try: + wads[i] = WadFile.load(os.path.join(wadpath, + os.path.basename(wads[i]))) + except IOError: + #give up + operator.report({'INFO'}, "Cant't find %s" % wads[i]) + wads[i] = None + for ent in entities: + process_entity(ent, wads) + finally: + bpy.context.preferences.edit.use_global_undo = undo return {'FINISHED'} def import_pts(operator, context, filepath): bpy.context.user_preferences.edit.use_global_undo = False for obj in bpy.context.scene.objects: - obj.select = False + obj.select_set(False) lines = open(filepath, "rt").readlines() verts = [None] * len(lines) @@ -248,6 +253,6 @@ def import_pts(operator, context, filepath): obj = bpy.data.objects.new("leak points", mesh) bpy.context.scene.objects.link(obj) bpy.context.scene.objects.active=obj - obj.select = True + obj.select_set(True) bpy.context.user_preferences.edit.use_global_undo = True return {'FINISHED'} diff --git a/tools/io_qfmap/init.py b/tools/io_qfmap/init.py new file mode 100644 index 000000000..199bb71fa --- /dev/null +++ b/tools/io_qfmap/init.py @@ -0,0 +1,294 @@ +# vim:ts=4:et +# ##### 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 ##### + +# + +import bpy +from bpy.props import BoolProperty, FloatProperty, StringProperty, EnumProperty +from bpy.props import FloatVectorProperty, PointerProperty +from bpy_extras.io_utils import ExportHelper, ImportHelper, path_reference_mode, axis_conversion +from bpy.app.handlers import persistent + +from .entityclass import EntityClassDict, EntityClassError +from . import entity +from . import import_map +from . import export_map + +def ecm_draw(self, context): + layout = self.layout + for item in self.menu_items: + if type(item[1]) is str: + ec = context.scene.qfmap.entity_classes[item[1]] + if ec.size: + icon = 'OBJECT_DATA' + else: + icon = 'MESH_DATA' + op = layout.operator("object.add_entity", text=item[0], icon=icon) + op.entclass=item[1] + if ec.comment: + pass + else: + layout.menu(item[1].bl_idname) + +class EntityClassMenu: + @classmethod + def clear(cls): + while cls.menu_items: + if type(cls.menu_item[0][1]) is not str: + bpy.utils.unregister_class(cls.menu_items[0][1]) + cls.menu_items[0][1].clear() + del cls.menu_items[0] + @classmethod + def build(cls, menudict, name="INFO_MT_entity_add", label="entity"): + items = list(menudict.items()) + items.sort() + menu_items = [] + for i in items: + i = list(i) + if type(i[1]) is dict: + if i[0]: + nm = "_".join((name, i[0])) + else: + nm = name + i[1] = cls.build(i[1], nm, i[0]) + menu_items.append(i) + attrs = {} + attrs["menu_items"] = menu_items + attrs["draw"] = ecm_draw + attrs["bl_idname"] = name + attrs["bl_label"] = label + attrs["clear"] = cls.clear + menu = type(name, (bpy.types.Menu,), attrs) + bpy.utils.register_class(menu) + return menu + +@persistent +def scene_load_handler(dummy): + for scene in bpy.data.scenes: + if hasattr(scene, "qfmap"): + scene.qfmap.script_update(bpy.context) + +class MapeditMessage(bpy.types.Operator): + bl_idname = "qfmapedit.message" + bl_label = "Message" + type: StringProperty() + message: StringProperty() + + def execute(self, context): + self.report({'INFO'}, message) + return {'FINISHED'} + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_popup(self, width=400, height=200) + def draw(self, context): + self.layout.label(self.type, icon='ERROR') + self.layout.label(self.message) + +def scan_entity_classes(context): + qfmap = context.scene.qfmap + if not qfmap.dirpath: + return + qfmap.entity_classes.from_source_tree(qfmap.dirpath) + name = context.scene.name + '-EntityClasses' + if name in bpy.data.texts: + txt = bpy.data.texts[name] + else: + txt = bpy.data.texts.new(name) + txt.from_string(qfmap.entity_classes.to_plist()) + qfmap.script = name + +def parse_entity_classes(context): + context.scene.qfmap.script_update(context) + +def ec_dir_update(self, context): + try: + scan_entity_classes(context) + except EntityClassError as err: + self.dirpath = "" + bpy.ops.qfmapedit.message('INVOKE_DEFAULT', type="Error", + message="Entity Class Error: %s" % err) + +def ec_script_update(self, context): + self.script_update(context) + +class AddEntity(bpy.types.Operator): + '''Add an entity''' + bl_idname = "object.add_entity" + bl_label = "Entity" + entclass: StringProperty(name = "entclass") + + def execute(self, context): + keywords = self.as_keywords() + return entity.add_entity(self, context, **keywords) + +class QFEntityClassScan(bpy.types.Operator): + '''Rescan the specified QuakeC source tree''' + bl_idname = "scene.scan_entity_classes" + bl_label = "RELOAD" + + def execute(self, context): + scan_entity_classes(context) + return {'FINISHED'} + +class QFEntityClassParse(bpy.types.Operator): + '''Reparse the specified entity class script''' + bl_idname = "scene.parse_entity_classes" + bl_label = "RELOAD" + + def execute(self, context): + parse_entity_classes(context) + return {'FINISHED'} + +class QFEntityClasses(bpy.types.PropertyGroup): + wadpath: StringProperty( + name="wadpath", + description="Path to search for wad files", + subtype='DIR_PATH') + dirpath: StringProperty( + name="dirpath", + description="Path to qc source tree", + subtype='DIR_PATH', + update=ec_dir_update) + script: StringProperty( + name="script", + description="entity class storage", + update=ec_script_update) + entity_classes = EntityClassDict() + ecm = EntityClassMenu.build({}) + entity_targets = {} + target_entities = [] + + def script_update(self, context): + if self.script in bpy.data.texts: + script = bpy.data.texts[self.script].as_string() + self.entity_classes.from_plist(script) + menudict = {} + entclasses = self.entity_classes.keys() + for ec in entclasses: + ecsub = ec.split("_") + d = menudict + for sub in ecsub[:-1]: + if sub not in d: + d[sub] = {} + elif type(d[sub]) is str: + d[sub] = {"":d[sub]} + d = d[sub] + sub = ecsub[-1] + if sub in d: + d[sub][""] = ec + else: + d[sub] = ec + self.__class__.ecm = EntityClassMenu.build(menudict) + +class OBJECT_PT_QFECPanel(bpy.types.Panel): + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + #bl_context = 'scene' + bl_category = "View" + bl_label = 'QF Entity Classes' + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout + scene = context.scene + row = layout.row() + layout.prop(scene.qfmap, "wadpath") + row = layout.row() + row.prop(scene.qfmap, "dirpath") + row.operator("scene.scan_entity_classes", text="", icon="FILE_REFRESH") + row = layout.row() + row.prop(scene.qfmap, "script") + row.operator("scene.parse_entity_classes", text="", icon="FILE_REFRESH") + +class ImportPoints(bpy.types.Operator, ImportHelper): + '''Load a Quake points File''' + bl_idname = "import_mesh.quake_points" + bl_label = "Import points" + + filename_ext = ".pts" + filter_glob: StringProperty(default="*.pts", options={'HIDDEN'}) + + def execute(self, context): + keywords = self.as_keywords (ignore=("filter_glob",)) + return import_map.import_pts(self, context, **keywords) + +class ImportMap(bpy.types.Operator, ImportHelper): + '''Load a Quake map File''' + bl_idname = "import_mesh.quake_map" + bl_label = "Import map" + + filename_ext = ".map" + filter_glob: StringProperty(default="*.map", options={'HIDDEN'}) + + def execute(self, context): + keywords = self.as_keywords (ignore=("filter_glob",)) + return import_map.import_map(self, context, **keywords) + +class ExportMap(bpy.types.Operator, ExportHelper): + '''Save a Quake map File''' + + bl_idname = "export_mesh.quake_map" + bl_label = "Export map" + + filename_ext = ".map" + filter_glob: StringProperty(default="*.map", options={'HIDDEN'}) + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + keywords = self.as_keywords (ignore=("check_existing", "filter_glob")) + return export_map.export_map(self, context, **keywords) + +def menu_func_import(self, context): + self.layout.operator(ImportMap.bl_idname, text="Quake map (.map)") + self.layout.operator(ImportPoints.bl_idname, text="Quake points (.pts)") + +def menu_func_export(self, context): + self.layout.operator(ExportMap.bl_idname, text="Quake map (.map)") + +def menu_func_add(self, context): + self.layout.menu(context.scene.qfmap.ecm.bl_idname, icon='PLUGIN') + +classes_to_register = ( + MapeditMessage, + AddEntity, + QFEntityClassScan, + QFEntityClassParse, + QFEntityClasses, + OBJECT_PT_QFECPanel, + ImportPoints, + ImportMap, + ExportMap, +) +menus_to_register = ( + (bpy.types.TOPBAR_MT_file_import, menu_func_import), + (bpy.types.TOPBAR_MT_file_export, menu_func_export), + (bpy.types.VIEW3D_MT_add, menu_func_add), +) +custom_properties_to_register = ( + (bpy.types.Scene, "qfmap", QFEntityClasses), +) +handlers_to_register = ( + ("load_post", scene_load_handler), +) diff --git a/tools/io_qfmap/map.py b/tools/io_qfmap/map.py index b0820de69..e47b7563a 100644 --- a/tools/io_qfmap/map.py +++ b/tools/io_qfmap/map.py @@ -38,8 +38,8 @@ class Texinfo: norm = s_vec.cross(t_vec) q = Quaternion(norm, rotate * pi / 180) self.vecs = [None] * 2 - self.vecs[0] = (q * s_vec / scale[0], s_offs) - self.vecs[1] = (q * t_vec / scale[1], t_offs) + self.vecs[0] = (q @ s_vec / scale[0], s_offs) + self.vecs[1] = (q @ t_vec / scale[1], t_offs) def __cmp__(self, other): return self.name == other.name and self.vecs == other.vecs @classmethod