diff --git a/korman/__init__.py b/korman/__init__.py index 093df40..f7b0df8 100644 --- a/korman/__init__.py +++ b/korman/__init__.py @@ -41,6 +41,7 @@ def register(): nodes.register() operators.register() properties.register() + ui.register() def unregister(): @@ -48,6 +49,7 @@ def unregister(): bpy.utils.unregister_module(__name__) nodes.unregister() operators.unregister() + ui.unregister() if __name__ == "__main__": diff --git a/korman/operators/__init__.py b/korman/operators/__init__.py index ef4e5c4..94b352f 100644 --- a/korman/operators/__init__.py +++ b/korman/operators/__init__.py @@ -15,6 +15,7 @@ from . import op_export as exporter from . import op_lightmap as lightmap +from . import op_mesh as mesh from . import op_modifier as modifier from . import op_nodes as nodes from . import op_sound as sound diff --git a/korman/operators/op_mesh.py b/korman/operators/op_mesh.py new file mode 100644 index 0000000..378eed6 --- /dev/null +++ b/korman/operators/op_mesh.py @@ -0,0 +1,394 @@ +# This file is part of Korman. +# +# Korman 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 3 of the License, or +# (at your option) any later version. +# +# Korman 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 Korman. If not, see . + +import bpy +import bmesh +import math +import mathutils + +class PlasmaMeshOperator: + @classmethod + def poll(cls, context): + return context.scene.render.engine == "PLASMA_GAME" + + +class PlasmaAddLadderMeshOperator(PlasmaMeshOperator, bpy.types.Operator): + bl_idname = "mesh.plasma_ladder_add" + bl_label = "Add Ladder" + bl_category = "Plasma" + bl_description = "Adds a new Plasma Ladder" + bl_options = {"REGISTER", "UNDO"} + + # Allows user to specify their own name stem + ladder_name = bpy.props.StringProperty(name="Name", + description="Ladder name stem", + default="Ladder", + options=set()) + # Basic stats + ladder_height = bpy.props.FloatProperty(name="Height", + description="Height of ladder in feet", + min=6, max=1000, step=200, precision=0, default=6, + unit="LENGTH", subtype="DISTANCE", + options=set()) + ladder_width = bpy.props.FloatProperty(name="Width", + description="Width of ladder in inches", + min=30, max=42, step=100, precision=0, default=30, + options=set()) + rung_height = bpy.props.FloatProperty(name="Rung height", + description="Height of rungs in inches", + min=1, max=6, step=100, precision=0, default=6, + options=set()) + # Template generation + gen_back_guide = bpy.props.BoolProperty(name="Ladder", + description="Generates helper object where ladder back should be placed", + default=True, + options=set()) + gen_ground_guides = bpy.props.BoolProperty(name="Ground", + description="Generates helper objects where ground should be placed", + default=True, + options=set()) + gen_rung_guides = bpy.props.BoolProperty(name="Rungs", + description="Generates helper objects where rungs should be placed", + default=True, + options=set()) + rung_width_type = bpy.props.EnumProperty(name="Rung Width", + description="Type of rungs to generate", + items=[("FULL", "Full Width Rungs", "The rungs cross the entire width of the ladder"), + ("HALF", "Half Width Rungs", "The rungs only cross half the ladder's width, on the side where the avatar will contact them"),], + default="FULL", + options=set()) + # Game options + has_upper_entry = bpy.props.BoolProperty(name="Has Upper Entry Point", + description="Specifies whether the ladder has an upper entry", + default=True, + options=set()) + upper_entry_enabled = bpy.props.BoolProperty(name="Upper Entry Enabled", + description="Specifies whether the ladder's upper entry is enabled by default at Age start", + default=True, + options=set()) + has_lower_entry = bpy.props.BoolProperty(name="Has Lower Entry Point", + description="Specifies whether the ladder has a lower entry", + default=True, + options=set()) + lower_entry_enabled = bpy.props.BoolProperty(name="Lower Entry Enabled", + description="Specifies whether the ladder's lower entry is enabled by default at Age start", + default=True, + options=set()) + + def draw(self, context): + layout = self.layout + space = bpy.context.space_data + + if not space.local_view: + box = layout.box() + box.label("Ladder Name:") + row = box.row() + row.alert = not self.ladder_name + row.prop(self, "ladder_name", text="") + + box = layout.box() + box.label("Geometry:") + row = box.row() + row.alert = self.ladder_height % 2 != 0 + row.prop(self, "ladder_height") + row = box.row() + row.prop(self, "ladder_width") + row = box.row() + row.prop(self, "rung_height") + + box = layout.box() + box.label("Template Guides:") + col = box.column() + col.prop(self, "gen_back_guide") + col.prop(self, "gen_ground_guides") + col.prop(self, "gen_rung_guides") + if self.gen_rung_guides: + col.separator() + col.prop(self, "rung_width_type", text="") + + box = layout.box() + row = box.row() + col = row.column() + col.label("Upper Entry:") + col.row().prop(self, "has_upper_entry", text="Create") + row = col.row() + row.enabled = self.has_upper_entry + row.prop(self, "upper_entry_enabled", text="Enabled") + col.separator() + col.label("Lower Entry:") + col.row().prop(self, "has_lower_entry", text="Create") + row = col.row() + row.enabled = self.has_lower_entry + row.prop(self, "lower_entry_enabled", text="Enabled") + + else: + row = layout.row() + row.label("Warning: Operator does not work in local view mode", icon="ERROR") + + def execute(self, context): + if context.mode == "OBJECT": + self.create_ladder_objects() + else: + self.report({"WARNING"}, "Ladder creation only valid in Object mode") + return {"CANCELLED"} + return {"FINISHED"} + + def create_guide_rungs(self): + bpyscene = bpy.context.scene + cursor_shift = mathutils.Matrix.Translation(bpy.context.scene.cursor_location) + + rung_height_ft = self.rung_height / 12 + rung_width_ft = self.ladder_width / 12 + + if self.rung_width_type == "FULL": + rung_width = rung_width_ft + rung_yoffset = 0.0 + else: + rung_width = rung_width_ft / 2 + rung_yoffset = rung_width_ft / 4 + + rungs_scale = mathutils.Matrix( + ((0.5, 0.0, 0.0), + (0.0, rung_width, 0.0), + (0.0, 0.0, rung_height_ft))) + + for rung_num in range(0, int(self.ladder_height)): + side = "L" if (rung_num % 2) == 0 else "R" + + mesh = bpy.data.meshes.new("{}_Rung_{}_{}".format(self.name_stem, side, rung_num)) + rungs = bpy.data.objects.new("{}_Rung_{}_{}".format(self.name_stem, side, rung_num), mesh) + rungs.hide_render = True + rungs.draw_type = "BOUNDS" + + bpyscene.objects.link(rungs) + bpyscene.objects.active = rungs + rungs.select = True + + bm = bmesh.new() + bmesh.ops.create_cube(bm, size=(1.0), matrix=rungs_scale) + + # Move each rung up, based on: + # its place in the array, aligned to the top of the rung position, shifted up to start at the ladder's base + if (rung_num % 2) == 0: + rung_pos = mathutils.Matrix.Translation((0.5, -rung_yoffset, rung_num + (1.0 - rung_height_ft) + (rung_height_ft / 2))) + else: + rung_pos = mathutils.Matrix.Translation((0.5, rung_yoffset, rung_num + (1.0 - rung_height_ft) + (rung_height_ft / 2))) + bmesh.ops.transform(bm, matrix=cursor_shift, space=rungs.matrix_world, verts=bm.verts) + bmesh.ops.transform(bm, matrix=rung_pos, space=rungs.matrix_world, verts=bm.verts) + bm.to_mesh(mesh) + bm.free() + + def create_guide_back(self): + bpyscene = bpy.context.scene + cursor_shift = mathutils.Matrix.Translation(bpy.context.scene.cursor_location) + + # Create an empty mesh and the object. + name = "{}_Back".format(self.name_stem) + mesh = bpy.data.meshes.new(name) + back = bpy.data.objects.new(name, mesh) + back.hide_render = True + back.draw_type = "BOUNDS" + + # Add the object into the scene. + bpyscene.objects.link(back) + bpyscene.objects.active = back + back.select = True + + # Construct the bmesh and assign it to the blender mesh. + bm = bmesh.new() + ladder_scale = mathutils.Matrix( + ((0.5, 0.0, 0.0), + (0.0, self.ladder_width / 12, 0.0), + (0.0, 0.0, self.ladder_height))) + bmesh.ops.create_cube(bm, size=(1.0), matrix=ladder_scale) + + # Shift the ladder up so that its base is at the 3D cursor + back_pos = mathutils.Matrix.Translation((0.0, 0.0, self.ladder_height / 2)) + bmesh.ops.transform(bm, matrix=cursor_shift, space=back.matrix_world, verts=bm.verts) + bmesh.ops.transform(bm, matrix=back_pos, space=back.matrix_world, verts=bm.verts) + bm.to_mesh(mesh) + bm.free() + + def create_guide_ground(self): + bpyscene = bpy.context.scene + cursor_shift = mathutils.Matrix.Translation(bpy.context.scene.cursor_location) + + for pos in ("Upper", "Lower"): + # Create an empty mesh and the object. + name = "{}_Ground_{}".format(self.name_stem, pos) + mesh = bpy.data.meshes.new(name) + ground = bpy.data.objects.new(name, mesh) + ground.hide_render = True + ground.draw_type = "BOUNDS" + + # Add the object into the scene. + bpyscene.objects.link(ground) + bpyscene.objects.active = ground + ground.select = True + + # Construct the bmesh and assign it to the blender mesh. + bm = bmesh.new() + ground_depth = 3.0 + ground_scale = mathutils.Matrix( + ((ground_depth, 0.0, 0.0), + (0.0, self.ladder_width / 12, 0.0), + (0.0, 0.0, 0.5))) + bmesh.ops.create_cube(bm, size=(1.0), matrix=ground_scale) + + if pos == "Upper": + ground_pos = mathutils.Matrix.Translation((-(ground_depth / 2) + 0.25, 0.0, self.ladder_height + 0.25)) + else: + ground_pos = mathutils.Matrix.Translation(((ground_depth / 2) + 0.25, 0.0, 0.25)) + bmesh.ops.transform(bm, matrix=cursor_shift, space=ground.matrix_world, verts=bm.verts) + bmesh.ops.transform(bm, matrix=ground_pos, space=ground.matrix_world, verts=bm.verts) + bm.to_mesh(mesh) + bm.free() + + def create_upper_entry(self): + bpyscene = bpy.context.scene + cursor_shift = mathutils.Matrix.Translation(bpy.context.scene.cursor_location) + + # Create an empty mesh and the object. + name = "{}_Entry_Upper".format(self.name_stem) + mesh = bpy.data.meshes.new(name) + upper_rgn = bpy.data.objects.new(name, mesh) + upper_rgn.hide_render = True + upper_rgn.draw_type = "WIRE" + + # Add the object into the scene. + bpyscene.objects.link(upper_rgn) + bpyscene.objects.active = upper_rgn + upper_rgn.select = True + upper_rgn.plasma_object.enabled = True + + # Construct the bmesh and assign it to the blender mesh. + bm = bmesh.new() + rgn_scale = mathutils.Matrix( + ((self.ladder_width / 12, 0.0, 0.0), + (0.0, 2.5, 0.0), + (0.0, 0.0, 2.0))) + bmesh.ops.create_cube(bm, size=(1.0), matrix=rgn_scale) + + rgn_pos = mathutils.Matrix.Translation((-1.80, 0.0, 1.5 + self.ladder_height)) + bmesh.ops.transform(bm, matrix=cursor_shift, space=upper_rgn.matrix_world, verts=bm.verts) + bmesh.ops.transform(bm, matrix=rgn_pos, space=upper_rgn.matrix_world, verts=bm.verts) + + bm.to_mesh(mesh) + bm.free() + + origin_to_bottom(upper_rgn) + upper_rgn.rotation_euler[2] = math.radians(90.0) + + bpy.ops.object.plasma_modifier_add(types="laddermod") + laddermod = upper_rgn.plasma_modifiers.laddermod + laddermod.is_enabled = self.lower_entry_enabled + laddermod.num_loops = (self.ladder_height - 6) / 2 + laddermod.direction = "DOWN" + + def create_lower_entry(self): + bpyscene = bpy.context.scene + cursor_shift = mathutils.Matrix.Translation(bpy.context.scene.cursor_location) + + # Create an empty mesh and the object. + name = "{}_Entry_Lower".format(self.name_stem) + mesh = bpy.data.meshes.new(name) + lower_rgn = bpy.data.objects.new(name, mesh) + lower_rgn.hide_render = True + lower_rgn.draw_type = "WIRE" + + # Add the object into the scene. + bpyscene.objects.link(lower_rgn) + bpyscene.objects.active = lower_rgn + lower_rgn.select = True + lower_rgn.plasma_object.enabled = True + + # Construct the bmesh and assign it to the blender mesh. + bm = bmesh.new() + rgn_scale = mathutils.Matrix( + ((self.ladder_width / 12, 0.0, 0.0), + (0.0, 2.5, 0.0), + (0.0, 0.0, 2.0))) + bmesh.ops.create_cube(bm, size=(1.0), matrix=rgn_scale) + + rgn_pos = mathutils.Matrix.Translation((2.70, 0.0, 1.5)) + bmesh.ops.transform(bm, matrix=cursor_shift, space=lower_rgn.matrix_world, verts=bm.verts) + bmesh.ops.transform(bm, matrix=rgn_pos, space=lower_rgn.matrix_world, verts=bm.verts) + + bm.to_mesh(mesh) + bm.free() + + origin_to_bottom(lower_rgn) + lower_rgn.rotation_euler[2] = math.radians(-90.0) + + bpy.ops.object.plasma_modifier_add(types="laddermod") + laddermod = lower_rgn.plasma_modifiers.laddermod + laddermod.is_enabled = self.lower_entry_enabled + laddermod.num_loops = (self.ladder_height - 6) / 2 + laddermod.direction = "UP" + + def create_ladder_objects(self): + for obj in bpy.data.objects: + obj.select = False + + if self.gen_rung_guides: + self.create_guide_rungs() + if self.gen_back_guide: + self.create_guide_back() + if self.gen_ground_guides: + self.create_guide_ground() + + bpy.ops.object.origin_set(type="ORIGIN_CENTER_OF_MASS") + + if self.has_upper_entry: + self.create_upper_entry() + if self.has_lower_entry: + self.create_lower_entry() + + bpy.ops.group.create(name="LadderGroup") + bpy.ops.group.objects_add_active() + + @property + def name_stem(self): + return self.ladder_name if self.ladder_name else "Ladder" + +def origin_to_bottom(obj): + # Modified from https://blender.stackexchange.com/a/42110/3055 + mw = obj.matrix_world + local_verts = [mathutils.Vector(v[:]) for v in obj.bound_box] + x, y, z = 0, 0, 0 + + l = len(local_verts) + y = sum((v.y for v in local_verts)) / l + x = sum((v.x for v in local_verts)) / l + z = min((v.z for v in local_verts)) + + local_origin = mathutils.Vector((x, y, z)) + global_origin = mw * local_origin + + bm = bmesh.new() + bm.from_mesh(obj.data) + + for v in bm.verts: + v.co = v.co - local_origin + + bm.to_mesh(obj.data) + mw.translation = global_origin + + +def register(): + bpy.utils.register_module(__name__) + +def unregister(): + bpy.utils.unregister_module(__name__) diff --git a/korman/properties/modifiers/avatar.py b/korman/properties/modifiers/avatar.py index 81c0457..e854ebf 100644 --- a/korman/properties/modifiers/avatar.py +++ b/korman/properties/modifiers/avatar.py @@ -15,6 +15,7 @@ import bpy from bpy.props import * +import mathutils from PyHSPlasma import * from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz @@ -22,6 +23,66 @@ from ...exporter.explosions import ExportError from ...helpers import find_modifier from ... import idprops + +class PlasmaLadderModifier(PlasmaModifierProperties): + pl_id = "laddermod" + + bl_category = "Avatar" + bl_label = "Ladder" + bl_description = "Climbable Ladder" + bl_icon = "COLLAPSEMENU" + + is_enabled = BoolProperty(name="Enabled", + description="Ladder enabled by default at Age start", + default=True) + direction = EnumProperty(name="Direction", + description="Direction of climb", + items=[("UP", "Up", "The avatar will mount the ladder and climb upward"), + ("DOWN", "Down", "The avatar will mount the ladder and climb downward"),], + default="DOWN") + num_loops = IntProperty(name="Loops", + description="How many full animation loops after the first to play before dismounting", + min=0, default=4) + facing_object = PointerProperty(name="Facing Object", + description="Target object the avatar must be facing through this region to trigger climb (optional)", + type=bpy.types.Object, + poll=idprops.poll_mesh_objects) + + def export(self, exporter, bo, so): + # Create the ladder modifier + mod = exporter.mgr.find_create_object(plAvLadderMod, so=so, name=self.key_name) + mod.type = plAvLadderMod.kBig + mod.loops = self.num_loops + mod.enabled = self.is_enabled + mod.goingUp = self.direction == "UP" + + # Create vector pointing from the Facing Object to the Detector. + # Animation only activates if the avatar is facing it within + # engine-defined (45 degree) tolerance + if self.facing_object is not None: + # Use object if one has been selected + ladderVec = self.facing_object.matrix_world.translation - bo.matrix_world.translation + else: + # Make our own artificial target -1.0 units back on the local Y axis. + world = bo.matrix_world.copy() + world.invert() + target = bo.location - (mathutils.Vector((0.0, 1.0, 0.0)) * world) + ladderVec = target - bo.matrix_local.translation + mod.ladderView = hsVector3(ladderVec.x, ladderVec.y, 0.0) + mod.ladderView.normalize() + + # Generate the detector's physical bounds + det_name = "{}_LadderDetector".format(self.id_data.name) + bounds = "hull" if not bo.plasma_modifiers.collision.enabled else bo.plasma_modifiers.collision.bounds + simIface, physical = exporter.physics.generate_physical(bo, so, bounds, det_name) + physical.memberGroup = plSimDefs.kGroupDetector + physical.reportGroup |= 1 << plSimDefs.kGroupAvatar + + @property + def requires_actor(self): + return True + + sitting_approach_flags = [("kApproachFront", "Front", "Approach from the font"), ("kApproachLeft", "Left", "Approach from the left"), ("kApproachRight", "Right", "Approach from the right"), diff --git a/korman/ui/__init__.py b/korman/ui/__init__.py index 3a865c4..c334f8a 100644 --- a/korman/ui/__init__.py +++ b/korman/ui/__init__.py @@ -14,8 +14,16 @@ # along with Korman. If not, see . from .ui_lamp import * +from .ui_menus import * from .ui_modifiers import * from .ui_object import * from .ui_texture import * from .ui_toolbox import * from .ui_world import * + + +def register(): + ui_menus.register() + +def unregister(): + ui_menus.unregister() diff --git a/korman/ui/modifiers/avatar.py b/korman/ui/modifiers/avatar.py index 31cc508..11db188 100644 --- a/korman/ui/modifiers/avatar.py +++ b/korman/ui/modifiers/avatar.py @@ -17,6 +17,15 @@ import bpy from ...helpers import find_modifier +def laddermod(modifier, layout, context): + layout.label(text="Avatar climbs facing negative Y.") + + layout.prop(modifier, "is_enabled") + layout.prop(modifier, "num_loops") + layout.prop(modifier, "direction") + + layout.prop(modifier, "facing_object", icon="MESH_DATA") + def sittingmod(modifier, layout, context): layout.row().prop(modifier, "approach") diff --git a/korman/ui/ui_menus.py b/korman/ui/ui_menus.py new file mode 100644 index 0000000..1f51912 --- /dev/null +++ b/korman/ui/ui_menus.py @@ -0,0 +1,45 @@ +# This file is part of Korman. +# +# Korman 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 3 of the License, or +# (at your option) any later version. +# +# Korman 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 Korman. If not, see . + +from ..operators.op_mesh import * + +class PlasmaMenu: + @classmethod + def poll(cls, context): + return context.scene.render.engine == "PLASMA_GAME" + + +class PlasmaAddMenu(PlasmaMenu, bpy.types.Menu): + bl_idname = "menu.plasma_add" + bl_label = "Plasma" + bl_description = "Add a Plasma premade" + + def draw(self, context): + layout = self.layout + + layout.operator("mesh.plasma_ladder_add", text="Ladder", icon="COLLAPSEMENU") + + +def build_plasma_menu(self, context): + if context.scene.render.engine != "PLASMA_GAME": + return + self.layout.separator() + self.layout.menu("menu.plasma_add", icon="URL") + +def register(): + bpy.types.INFO_MT_add.append(build_plasma_menu) + +def unregister(): + bpy.types.INFO_MT_add.remove(build_plasma_menu)