diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 6001b3a..64e0d4e 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -415,7 +415,13 @@ class Exporter: @functools.singledispatch def handle_temporary(temporary, parent): - raise RuntimeError("Temporary object of type '{}' generated by '{}' was unhandled".format(temporary.__class__, parent.name)) + # Maybe this was an embedded context manager? + if hasattr(temporary, "__enter__"): + ctx_temporary = self.exit_stack.enter_context(temporary) + if ctx_temporary is not None: + handle_temporary(ctx_temporary, parent) + else: + raise RuntimeError("Temporary object of type '{}' generated by '{}' was unhandled".format(temporary.__class__, parent.name)) @handle_temporary.register(bpy.types.Object) def _(temporary, parent): diff --git a/korman/exporter/utils.py b/korman/exporter/utils.py index 296228a..188c490 100644 --- a/korman/exporter/utils.py +++ b/korman/exporter/utils.py @@ -13,12 +13,14 @@ # You should have received a copy of the GNU General Public License # along with Korman. If not, see . +from __future__ import annotations + import bmesh import bpy import mathutils -from typing import Callable, Iterator, Tuple from contextlib import contextmanager +from typing import * from PyHSPlasma import * @@ -54,46 +56,73 @@ def quaternion(blquat): return hsQuat(blquat.x, blquat.y, blquat.z, blquat.w) -@contextmanager -def bmesh_temporary_object(name : str, factory : Callable, page_name : str=None): - """Creates a temporary object and mesh that exists only for the duration of - the context""" - mesh = bpy.data.meshes.new(name) - obj = bpy.data.objects.new(name, mesh) - obj.draw_type = "WIRE" - if page_name is not None: - obj.plasma_object.page = page_name - bpy.context.scene.objects.link(obj) - - bm = bmesh.new() - try: - factory(bm) - bm.to_mesh(mesh) - yield obj - finally: - bm.free() - bpy.context.scene.objects.unlink(obj) +class BMeshObject: + def __init__(self, name: str, managed: bool = True): + self._managed = managed + self._bmesh = None + self._mesh = bpy.data.meshes.new(name) + self._obj = bpy.data.objects.new(name, self._mesh) + self._obj.draw_type = "WIRE" + bpy.context.scene.objects.link(self._obj) + + def __del__(self): + if self._managed: + bpy.context.scene.objects.unlink(self._obj) + bpy.data.meshes.remove(self._mesh) + + def __enter__(self) -> bmesh.types.BMesh: + if self._mesh is not None: + self._bmesh = bmesh.new() + self._bmesh.from_mesh(self._mesh) + return self._bmesh + + def __exit__(self, type, value, traceback): + if self._bmesh is not None: + self._bmesh.to_mesh(self._mesh) + self._bmesh.free() + self._bmesh = None + + def __getattr__(self, name: str) -> Any: + return getattr(self._obj, name) + + @property + def object(self) -> bpy.types.Object: + return self._obj + + def release(self) -> bpy.types.Object: + self._managed = False + return self._obj + + +def create_cube_region(name: str, size: float, owner_object: bpy.types.Object) -> bpy.types.Object: + """Create a cube shaped region object""" + region_object = BMeshObject(name) + region_object.plasma_object.enabled = True + region_object.plasma_object.page = owner_object.plasma_object.page + region_object.hide_render = True + with region_object as bm: + bmesh.ops.create_cube(bm, size=(size)) + bmesh.ops.transform( + bm, + matrix=mathutils.Matrix.Translation( + owner_object.matrix_world.translation - region_object.matrix_world.translation + ), + space=region_object.matrix_world, verts=bm.verts + ) + return region_object.release() @contextmanager -def bmesh_object(name: str) -> Iterator[Tuple[bpy.types.Object, bmesh.types.BMesh]]: - """Creates an object and mesh that will be removed if the context is exited - due to an error""" - mesh = bpy.data.meshes.new(name) - obj = bpy.data.objects.new(name, mesh) - obj.draw_type = "WIRE" - bpy.context.scene.objects.link(obj) - - bm = bmesh.new() - try: - yield obj, bm - except: - bpy.context.scene.objects.unlink(obj) - bpy.data.meshes.remove(mesh) - raise +def pre_export_optional_cube_region(source, attr: str, name: str, size: float, owner_object: bpy.types.Object) -> Optional[bpy.types.Object]: + if getattr(source, attr) is None: + region_object = create_cube_region(name, size, owner_object) + setattr(source, attr, region_object) + try: + yield region_object + finally: + source.property_unset(attr) else: - bm.to_mesh(mesh) - finally: - bm.free() + # contextlib.contextmanager requires for us to yield. Sad. + yield @contextmanager def temporary_mesh_object(source : bpy.types.Object) -> bpy.types.Object: diff --git a/korman/operators/op_mesh.py b/korman/operators/op_mesh.py index 6d00d71..3db0ab8 100644 --- a/korman/operators/op_mesh.py +++ b/korman/operators/op_mesh.py @@ -100,15 +100,15 @@ class PlasmaAddFlareOperator(PlasmaMeshOperator, bpy.types.Operator): flare_root.plasma_modifiers.viewfacemod.preset_options = "Sprite" # Create a textured Plane - with utils.bmesh_object("{}_Visible".format(self.name_stem)) as (flare_plane, bm): - flare_plane.hide_render = True - flare_plane.plasma_object.enabled = True - bpyscene.objects.active = flare_plane - + flare_plane = utils.BMeshObject(f"{self.name_stem}_Visible", managed=False) + flare_plane.hide_render = True + flare_plane.plasma_object.enabled = True + bpyscene.objects.active = flare_plane + with flare_plane as bm: # Make the actual plane mesh, facing away from the empty bmesh.ops.create_grid(bm, size=(0.5 + self.flare_distance * 0.5), matrix=mathutils.Matrix.Rotation(math.radians(180.0), 4, 'X')) bmesh.ops.transform(bm, matrix=mathutils.Matrix.Translation((0.0, 0.0, -self.flare_distance)), space=flare_plane.matrix_world, verts=bm.verts) - bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY") + bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY") # Give the plane a basic UV unwrap, so that it's texture-ready bpy.ops.object.editmode_toggle() diff --git a/korman/properties/modifiers/gui.py b/korman/properties/modifiers/gui.py index cefeef5..e959a56 100644 --- a/korman/properties/modifiers/gui.py +++ b/korman/properties/modifiers/gui.py @@ -224,36 +224,30 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz if version not in our_versions: # We aren't needed here exporter.report.port("Object '{}' has a JournalMod not enabled for export to the selected engine. Skipping.", - bo.name, version, indent=2) + bo.name, version) return - if self.clickable_region is None: - with utils.bmesh_object("{}_Journal_ClkRgn".format(self.key_name)) as (rgn_obj, bm): - bmesh.ops.create_cube(bm, size=(6.0)) - bmesh.ops.transform(bm, matrix=mathutils.Matrix.Translation(bo.matrix_world.translation - rgn_obj.matrix_world.translation), - space=rgn_obj.matrix_world, verts=bm.verts) - rgn_obj.plasma_object.enabled = True - rgn_obj.hide_render = True - yield rgn_obj - else: - # Use the region provided - rgn_obj = self.clickable_region + # Generate the clickable region if it was not provided + yield utils.pre_export_optional_cube_region( + self, "clickable_region", + f"{bo.name}_Journal_ClkRgn", 6.0, bo + ) # Generate the logic nodes - yield self.convert_logic(bo, age_name=exporter.age_name, rgn_obj=rgn_obj, version=version) + yield self.convert_logic(bo, age_name=exporter.age_name, version=version) - def logicwiz(self, bo, tree, age_name, rgn_obj, version): + def logicwiz(self, bo, tree, age_name, version): # Assign journal script based on target version journal_pfm = journal_pfms[version] journalnode = self._create_python_file_node(tree, journal_pfm["filename"], journal_pfm["attribs"]) if version <= pvPots: - self._create_pots_nodes(bo, tree.nodes, journalnode, age_name, rgn_obj) + self._create_pots_nodes(bo, tree.nodes, journalnode, age_name) else: - self._create_moul_nodes(bo, tree.nodes, journalnode, age_name, rgn_obj) + self._create_moul_nodes(bo, tree.nodes, journalnode, age_name) - def _create_pots_nodes(self, clickable_object, nodes, journalnode, age_name, rgn_obj): + def _create_pots_nodes(self, clickable_object, nodes, journalnode, age_name): clickable_region = nodes.new("PlasmaClickableRegionNode") - clickable_region.region_object = rgn_obj + clickable_region.region_object = self.clickable_region facing_object = nodes.new("PlasmaFacingTargetNode") facing_object.directional = False @@ -281,9 +275,9 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz height.link_output(journalnode, "pfm", "BookHeight") height.value_float = self.book_scale_h / 100.0 - def _create_moul_nodes(self, clickable_object, nodes, journalnode, age_name, rgn_obj): + def _create_moul_nodes(self, clickable_object, nodes, journalnode, age_name): clickable_region = nodes.new("PlasmaClickableRegionNode") - clickable_region.region_object = rgn_obj + clickable_region.region_object = self.clickable_region facing_object = nodes.new("PlasmaFacingTargetNode") facing_object.directional = False @@ -471,33 +465,21 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz return # Auto-generate a six-foot cube region around the clickable if none was provided. - if self.clickable_region is None: - with utils.bmesh_object("{}_LinkingBook_ClkRgn".format(self.key_name)) as (rgn_obj, bm): - bmesh.ops.create_cube(bm, size=(6.0)) - rgn_offset = mathutils.Matrix.Translation(self.clickable.matrix_world.translation - rgn_obj.matrix_world.translation) - bmesh.ops.transform(bm, matrix=rgn_offset, space=rgn_obj.matrix_world, verts=bm.verts) - rgn_obj.plasma_object.enabled = True - rgn_obj.hide_render = True - yield rgn_obj - else: - rgn_obj = self.clickable_region + yield utils.pre_export_optional_cube_region( + self, "clickable_region", + f"{self.key_name}_LinkingBook_ClkRgn", 6.0, + self.clickable + ) + # Auto-generate a ten-foot cube region around the clickable if none was provided. if self.shareable: - if self.share_region is None: - with utils.bmesh_object("{}_LinkingBook_ShareRgn".format(self.key_name)) as (share_region, bm): - # Generate a cube for the share region. - bmesh.ops.create_cube(bm, size=(10.0)) - share_region_offset = mathutils.Matrix.Translation(self.clickable.matrix_world.translation - share_region.matrix_world.translation) - bmesh.ops.transform(bm, matrix=share_region_offset, space=share_region.matrix_world, verts=bm.verts) - share_region.plasma_object.enabled = True - share_region.hide_render = True - yield share_region - else: - share_region = self.share_region - else: - share_region = None + yield utils.pre_export_optional_cube_region( + self, "share_region", + f"{self.key_name}_LinkingBook_ShareRgn", 10.0, + self.clickable + ) - yield self.convert_logic(bo, age_name=exporter.age_name, version=exporter.mgr.getVer(), click_region=rgn_obj, share_region=share_region) + yield self.convert_logic(bo, age_name=exporter.age_name, version=exporter.mgr.getVer()) def export(self, exporter, bo, so): if self._check_version(pvPrime, pvPots): @@ -514,19 +496,19 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz if self.seek_point is not None: yield self.seek_point.name - def logicwiz(self, bo, tree, age_name, version, click_region, share_region): + def logicwiz(self, bo, tree, age_name, version): # Assign linking book script based on target version linking_pfm = linking_pfms[version] linkingnode = self._create_python_file_node(tree, linking_pfm["filename"], linking_pfm["attribs"]) if version <= pvPots: - self._create_pots_nodes(bo, tree.nodes, linkingnode, age_name, click_region) + self._create_pots_nodes(bo, tree.nodes, linkingnode, age_name) else: - self._create_moul_nodes(bo, tree.nodes, linkingnode, age_name, click_region, share_region) + self._create_moul_nodes(bo, tree.nodes, linkingnode, age_name) - def _create_pots_nodes(self, clickable_object, nodes, linkingnode, age_name, clk_region): + def _create_pots_nodes(self, clickable_object, nodes, linkingnode, age_name): # Clickable clickable_region = nodes.new("PlasmaClickableRegionNode") - clickable_region.region_object = clk_region + clickable_region.region_object = self.clickable_region clickable = nodes.new("PlasmaClickableNode") clickable.clickable_object = self.clickable @@ -601,10 +583,10 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz responder.link_output(responder_state, "state_refs", "resp") responder.link_output(linkingnode, "keyref", "respOneShot") - def _create_moul_nodes(self, clickable_object, nodes, linkingnode, age_name, clk_region, share_region): + def _create_moul_nodes(self, clickable_object, nodes, linkingnode, age_name): # Clickable clickable_region = nodes.new("PlasmaClickableRegionNode") - clickable_region.region_object = clk_region + clickable_region.region_object = self.clickable_region clickable = nodes.new("PlasmaClickableNode") clickable.clickable_object = self.clickable @@ -657,10 +639,10 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz linking_panel_name.link_output(linkingnode, "pfm", "TargetAge") # Share MSB - if share_region is not None: + if self.shareable: # Region share_msb_region = nodes.new("PlasmaVolumeSensorNode") - share_msb_region.region_object = share_region + share_msb_region.region_object = self.share_region for i in share_msb_region.inputs: i.allow = True share_msb_region.link_output(linkingnode, "satisfies", "shareRegion") diff --git a/korman/properties/modifiers/logic.py b/korman/properties/modifiers/logic.py index ddd4c0b..15e19ec 100644 --- a/korman/properties/modifiers/logic.py +++ b/korman/properties/modifiers/logic.py @@ -158,22 +158,17 @@ class PlasmaTelescope(PlasmaModifierProperties, PlasmaModifierLogicWiz): raise ExportError(f"'{self.id_data.name}': Telescopes must specify a camera!") def pre_export(self, exporter, bo): - if self.clickable_region is None: - with utils.bmesh_object(f"{self.key_name}_Telescope_ClkRgn") as (rgn_obj, bm): - bmesh.ops.create_cube(bm, size=(6.0)) - bmesh.ops.transform(bm, matrix=mathutils.Matrix.Translation(bo.matrix_world.translation - rgn_obj.matrix_world.translation), - space=rgn_obj.matrix_world, verts=bm.verts) - rgn_obj.plasma_object.enabled = True - rgn_obj.hide_render = True - yield rgn_obj - else: - # Use the region provided - rgn_obj = self.clickable_region + # Generate a six-foot cube region if none was provided. + yield utils.pre_export_optional_cube_region( + self, "clickable_region", + f"{self.key_name}_Telescope_ClkRgn", 6.0, + bo + ) # Generate the logic nodes - yield self.convert_logic(bo, rgn_obj=rgn_obj) + yield self.convert_logic(bo) - def logicwiz(self, bo, tree, rgn_obj): + def logicwiz(self, bo, tree): nodes = tree.nodes # Create Python Node @@ -188,7 +183,7 @@ class PlasmaTelescope(PlasmaModifierProperties, PlasmaModifierLogicWiz): # Region telescoperegion = nodes.new("PlasmaClickableRegionNode") - telescoperegion.region_object = rgn_obj + telescoperegion.region_object = self.clickable_region telescoperegion.link_output(telescopeclick, "satisfies", "region") # Telescope Camera