Skip to content

Commit

Permalink
Merge pull request #406 from Hoikas/debounce_duplicate_note_popups
Browse files Browse the repository at this point in the history
Debounce duplicate note popups
  • Loading branch information
Hoikas committed Feb 19, 2024
2 parents b67a1a6 + 20ccfa8 commit 46a72a6
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 87 deletions.
6 changes: 4 additions & 2 deletions korman/exporter/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ def _check_sanity(self):
for mod in bl_obj.plasma_modifiers.modifiers:
fn = getattr(mod, "sanity_check", None)
if fn is not None:
fn()
fn(self)
inc_progress()
self.report.msg("... Age is grinning and holding a spatula. Must be OK, then.")

Expand Down Expand Up @@ -502,7 +502,9 @@ def _(temporary, parent):
# Wow, recursively generated objects. Aren't you special?
with indent():
for mod in temporary.plasma_modifiers.modifiers:
mod.sanity_check()
fn = getattr(mod, "sanity_check", None)
if fn is not None:
fn(self)
do_pre_export(temporary)
return temporary

Expand Down
92 changes: 92 additions & 0 deletions korman/exporter/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
if TYPE_CHECKING:
from .convert import Exporter
from .logger import _ExportLogger as ExportLogger
from ..properties.modifiers.game_gui import *


class Clipping(NamedTuple):
Expand All @@ -48,9 +49,13 @@ class GuiConverter:

if TYPE_CHECKING:
_parent: weakref.ref[Exporter] = ...
_pages: Dict[str, Any] = ...
_mods_exported: Set[str] = ...

def __init__(self, parent: Optional[Exporter] = None):
self._parent = weakref.ref(parent) if parent is not None else None
self._pages = {}
self._mods_exported = set()

# Go ahead and prepare the GUI transparent material for future use.
if parent is not None:
Expand Down Expand Up @@ -203,6 +208,93 @@ def convert_post_effect_matrices(self, camera_matrix: mathutils.Matrix) -> PostE
w2c[2, i] *= -1.0
return PostEffectModMatrices(c2w, w2c)

def check_pre_export(self, name: str, **kwargs):
previous = self._pages.setdefault(name, kwargs)
if previous != kwargs:
diff = set(previous.items()) - set(kwargs.items())
raise ExportError(f"GUI Page '{name}' has target modifiers with conflicting settings:\n{diff}")

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
Expand Down
2 changes: 1 addition & 1 deletion korman/properties/modifiers/anim.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def blender_action(self):
return None
raise ExportError("'{}': Object has an animation modifier but is not animated".format(bo.name))

def sanity_check(self) -> None:
def sanity_check(self, exporter) -> None:
if not self.id_data.plasma_object.has_animation_data:
raise ExportError("'{}': Has an animation modifier but no animation data.", self.id_data.name)

Expand Down
2 changes: 1 addition & 1 deletion korman/properties/modifiers/avatar.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ def requires_actor(self):
# This should be an empty, really...
return True

def sanity_check(self):
def sanity_check(self, exporter):
# The user absolutely MUST specify a clickable or this won't export worth crap.
if self.clickable_object is None:
raise ExportError("'{}': Sitting Behavior's clickable object is invalid".format(self.key_name))
2 changes: 1 addition & 1 deletion korman/properties/modifiers/game_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def is_game_gui_control(cls) -> bool:
def requires_dyntext(self) -> bool:
return False

def sanity_check(self):
def sanity_check(self, exporter):
age: PlasmaAge = bpy.context.scene.world.plasma_age

# Game GUI modifiers must be attached to objects in a GUI page, ONLY
Expand Down
86 changes: 8 additions & 78 deletions korman/properties/modifiers/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,7 @@ def _create_moul_nodes(self, clickable_object, nodes, linkingnode, age_name):
share.link_input(share_anim_stage, "stage", "stage_refs")
share.link_output(linkingnode, "hosts", "shareBookSeek")

def sanity_check(self):
def sanity_check(self, exporter):
if self.clickable is None:
raise ExportError("{}: Linking Book modifier requires a clickable!", self.id_data.name)
if self.seek_point is None:
Expand Down Expand Up @@ -724,88 +724,18 @@ def clickable_object(self) -> Optional[bpy.types.Object]:
if self.id_data.type == "MESH":
return self.id_data

def sanity_check(self):
def sanity_check(self, exporter: Exporter):
page_type = helpers.get_page_type(self.id_data.plasma_object.page)
if page_type != "room":
raise ExportError(f"Note Popup modifiers should be in a 'room' page, not a '{page_type}' page!")

# It's OK if multiple note popups point to the same GUI page,
# they just need to have the same camera.
exporter.gui.check_pre_export(self.gui_page, pl_id="note_popup", camera=self.gui_camera)

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:
Expand Down
2 changes: 1 addition & 1 deletion korman/properties/modifiers/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ class PlasmaTelescope(PlasmaModifierProperties, PlasmaModifierLogicWiz):
type=bpy.types.Object,
poll=idprops.poll_camera_objects)

def sanity_check(self):
def sanity_check(self, exporter):
if self.camera_object is None:
raise ExportError(f"'{self.id_data.name}': Telescopes must specify a camera!")

Expand Down
4 changes: 2 additions & 2 deletions korman/properties/modifiers/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def iter_dependencies(self):
for i in (j.blend_onto for j in self.dependencies if j.blend_onto is not None and j.enabled):
yield i

def sanity_check(self):
def sanity_check(self, exporter):
if self.has_circular_dependency:
raise ExportError("'{}': Circular Render Dependency detected!".format(self.id_data.name))

Expand Down Expand Up @@ -770,7 +770,7 @@ def _create_nodes(self, bo, tree, *, age_name, version, material=None, clear_col
def localization_set(self):
return "DynaTexts"

def sanity_check(self):
def sanity_check(self, exporter):
if self.texture is None:
raise ExportError("'{}': Localized Text modifier requires a texture", self.id_data.name)

Expand Down
2 changes: 1 addition & 1 deletion korman/properties/modifiers/sound.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ class PlasmaSoundEmitter(PlasmaModifierProperties):
stereize_left = PointerProperty(type=bpy.types.Object, options={"HIDDEN", "SKIP_SAVE"})
stereize_right = PointerProperty(type=bpy.types.Object, options={"HIDDEN", "SKIP_SAVE"})

def sanity_check(self):
def sanity_check(self, exporter):
modifiers = self.id_data.plasma_modifiers

# Sound emitters can potentially export sounds to more than one emitter SceneObject. Currently,
Expand Down

0 comments on commit 46a72a6

Please sign in to comment.