From 3c5e2d61d52719318412badf3f46dfc08a96e6ea Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 18 Feb 2024 18:11:17 -0500 Subject: [PATCH] Debounce GUI object duplication from the Note Popup modifier. The modifier was originally written with the assumption that exactly one modifier controlled exactly one page. Of course, artists began using multiple Note Popup modifiers to show the same GUI page in multiple places. So, be sure that we only export the GUI objects once. --- korman/exporter/gui.py | 84 ++++++++++++++++++++++++++++++ korman/properties/modifiers/gui.py | 78 +-------------------------- 2 files changed, 86 insertions(+), 76 deletions(-) diff --git a/korman/exporter/gui.py b/korman/exporter/gui.py index 0a0a117..10c9842 100644 --- a/korman/exporter/gui.py +++ b/korman/exporter/gui.py @@ -32,6 +32,7 @@ from . import utils if TYPE_CHECKING: from .convert import Exporter from .logger import _ExportLogger as ExportLogger + from ..properties.modifiers.game_gui import * class Clipping(NamedTuple): @@ -48,9 +49,11 @@ class GuiConverter: if TYPE_CHECKING: _parent: weakref.ref[Exporter] = ... + _mods_exported: Set[str] = ... def __init__(self, parent: Optional[Exporter] = None): self._parent = weakref.ref(parent) if parent is not None else None + self._mods_exported = set() # Go ahead and prepare the GUI transparent material for future use. if parent is not None: @@ -203,6 +206,87 @@ class GuiConverter: w2c[2, i] *= -1.0 return PostEffectModMatrices(c2w, w2c) + def create_note_gui(self, gui_page: str, gui_camera: bpy.types.Object): + if not gui_page in self._mods_exported: + guidialog_object = utils.create_empty_object(f"{gui_page}_NoteDialog") + guidialog_object.plasma_object.enabled = True + guidialog_object.plasma_object.page = gui_page + yield guidialog_object + + guidialog_mod: PlasmaGameGuiDialogModifier = guidialog_object.plasma_modifiers.gui_dialog + guidialog_mod.enabled = True + guidialog_mod.is_modal = True + if gui_camera is not None: + guidialog_mod.camera_object = gui_camera + else: + # Abuse the GUI Dialog's lookat caLculation to make us a camera that looks at everything + # the artist has placed into the GUI page. We want to do this NOW because we will very + # soon be adding more objects into the GUI page. + camera_object = yield utils.create_camera_object(f"{gui_page}_GUICamera") + camera_object.data.angle = math.radians(45.0) + camera_object.data.lens_unit = "FOV" + + visible_objects = [ + i for i in self._parent().get_objects(gui_page) + if i.type == "MESH" and i.data.materials + ] + camera_object.matrix_world = self.calc_camera_matrix( + bpy.context.scene, + visible_objects, + camera_object.data.angle + ) + clipping = self.calc_clipping( + camera_object.matrix_world, + bpy.context.scene, + visible_objects, + camera_object.data.angle + ) + camera_object.data.clip_start = clipping.hither + camera_object.data.clip_end = clipping.yonder + guidialog_mod.camera_object = camera_object + + # Begin creating the object for the clickoff plane. We want to yield it immediately + # to the exporter in case something goes wrong during the export, allowing the stale + # object to be cleaned up. + click_plane_object = utils.BMeshObject(f"{gui_page}_Exit") + click_plane_object.matrix_world = guidialog_mod.camera_object.matrix_world + click_plane_object.plasma_object.enabled = True + click_plane_object.plasma_object.page = gui_page + yield click_plane_object + + # We have a camera on guidialog_mod.camera_object. We will now use it to generate the + # points for the click-off plane button. + # TODO: Allow this to be configurable to 4:3, 16:9, or 21:9? + with ExitStack() as stack: + stack.enter_context(self.generate_camera_render_settings(bpy.context.scene)) + toggle = stack.enter_context(helpers.GoodNeighbor()) + + # Temporarily adjust the clipping plane out to the farthest point we can find to ensure + # that the click-off button ecompasses everything. This is a bit heavy-handed, but if + # you want more refined control, you won't be using this helper. + clipping = max((guidialog_mod.camera_object.data.clip_start, guidialog_mod.camera_object.data.clip_end)) + toggle.track(guidialog_mod.camera_object.data, "clip_start", clipping - 0.1) + view_frame = guidialog_mod.camera_object.data.view_frame(bpy.context.scene) + + click_plane_object.data.materials.append(self.transparent_material) + with click_plane_object as click_plane_mesh: + verts = [click_plane_mesh.verts.new(i) for i in view_frame] + face = click_plane_mesh.faces.new(verts) + # TODO: Ensure the face is pointing toward the camera! + # I feel like we should be fine by assuming that Blender returns the viewframe + # verts in the correct order, but this is Blender... So test that assumption carefully. + # TODO: Apparently not! + face.normal_flip() + + # We've now created the mesh object - handle the GUI Button stuff + click_plane_object.plasma_modifiers.gui_button.enabled = True + + # NOTE: We will be using xDialogToggle.py, so we use a special tag ID instead of the + # close dialog procedure. + click_plane_object.plasma_modifiers.gui_control.tag_id = 99 + + self._mods_exported.add(gui_page) + @contextmanager def generate_camera_render_settings(self, scene: bpy.types.Scene) -> Iterator[None]: # Set the render info to basically TV NTSC 4:3, which will set Blender's camera diff --git a/korman/properties/modifiers/gui.py b/korman/properties/modifiers/gui.py index 3efa136..8a24eee 100644 --- a/korman/properties/modifiers/gui.py +++ b/korman/properties/modifiers/gui.py @@ -730,82 +730,8 @@ class PlasmaNotePopupModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz): raise ExportError(f"Note Popup modifiers should be in a 'room' page, not a '{page_type}' page!") def pre_export(self, exporter: Exporter, bo: bpy.types.Object): - guidialog_object = utils.create_empty_object(f"{self.gui_page}_NoteDialog") - guidialog_object.plasma_object.enabled = True - guidialog_object.plasma_object.page = self.gui_page - yield guidialog_object - - guidialog_mod: PlasmaGameGuiDialogModifier = guidialog_object.plasma_modifiers.gui_dialog - guidialog_mod.enabled = True - guidialog_mod.is_modal = True - if self.gui_camera: - guidialog_mod.camera_object = self.gui_camera - else: - # Abuse the GUI Dialog's lookat caLculation to make us a camera that looks at everything - # the artist has placed into the GUI page. We want to do this NOW because we will very - # soon be adding more objects into the GUI page. - camera_object = yield utils.create_camera_object(f"{self.key_name}_GUICamera") - camera_object.data.angle = math.radians(45.0) - camera_object.data.lens_unit = "FOV" - - visible_objects = [ - i for i in exporter.get_objects(self.gui_page) - if i.type == "MESH" and i.data.materials - ] - camera_object.matrix_world = exporter.gui.calc_camera_matrix( - bpy.context.scene, - visible_objects, - camera_object.data.angle - ) - clipping = exporter.gui.calc_clipping( - camera_object.matrix_world, - bpy.context.scene, - visible_objects, - camera_object.data.angle - ) - camera_object.data.clip_start = clipping.hither - camera_object.data.clip_end = clipping.yonder - guidialog_mod.camera_object = camera_object - - # Begin creating the object for the clickoff plane. We want to yield it immediately - # to the exporter in case something goes wrong during the export, allowing the stale - # object to be cleaned up. - click_plane_object = utils.BMeshObject(f"{self.key_name}_Exit") - click_plane_object.matrix_world = guidialog_mod.camera_object.matrix_world - click_plane_object.plasma_object.enabled = True - click_plane_object.plasma_object.page = self.gui_page - yield click_plane_object - - # We have a camera on guidialog_mod.camera_object. We will now use it to generate the - # points for the click-off plane button. - # TODO: Allow this to be configurable to 4:3, 16:9, or 21:9? - with ExitStack() as stack: - stack.enter_context(exporter.gui.generate_camera_render_settings(bpy.context.scene)) - toggle = stack.enter_context(helpers.GoodNeighbor()) - - # Temporarily adjust the clipping plane out to the farthest point we can find to ensure - # that the click-off button ecompasses everything. This is a bit heavy-handed, but if - # you want more refined control, you won't be using this helper. - clipping = max((guidialog_mod.camera_object.data.clip_start, guidialog_mod.camera_object.data.clip_end)) - toggle.track(guidialog_mod.camera_object.data, "clip_start", clipping - 0.1) - view_frame = guidialog_mod.camera_object.data.view_frame(bpy.context.scene) - - click_plane_object.data.materials.append(exporter.gui.transparent_material) - with click_plane_object as click_plane_mesh: - verts = [click_plane_mesh.verts.new(i) for i in view_frame] - face = click_plane_mesh.faces.new(verts) - # TODO: Ensure the face is pointing toward the camera! - # I feel like we should be fine by assuming that Blender returns the viewframe - # verts in the correct order, but this is Blender... So test that assumption carefully. - # TODO: Apparently not! - face.normal_flip() - - # We've now created the mesh object - handle the GUI Button stuff - click_plane_object.plasma_modifiers.gui_button.enabled = True - - # NOTE: We will be using xDialogToggle.py, so we use a special tag ID instead of the - # close dialog procedure. - click_plane_object.plasma_modifiers.gui_control.tag_id = 99 + # The GUI converter will debounce duplicate GUI dialogs. + yield from exporter.gui.create_note_gui(self.gui_page, self.gui_camera) # Auto-generate a six-foot cube region around the clickable if none was provided. if self.clickable_region is None: