Update the blender map editor for 2.80+

This commit is contained in:
Bill Currie 2020-04-15 00:24:20 +09:00
parent 54f2e417dc
commit 7c922d2320
6 changed files with 468 additions and 347 deletions

View File

@ -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()

View File

@ -19,7 +19,8 @@
# <pep8 compliant>
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),
)

View File

@ -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__()

View File

@ -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'}

294
tools/io_qfmap/init.py Normal file
View File

@ -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 #####
# <pep8 compliant>
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),
)

View File

@ -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