mirror of
https://git.code.sf.net/p/quake/quakeforge
synced 2025-03-21 18:01:15 +00:00
Update the blender map editor for 2.80+
This commit is contained in:
parent
54f2e417dc
commit
7c922d2320
6 changed files with 468 additions and 347 deletions
|
@ -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()
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -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__()
|
||||
|
|
|
@ -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
294
tools/io_qfmap/init.py
Normal 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),
|
||||
)
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue