Browse Source

Merge pull request #406 from Hoikas/debounce_duplicate_note_popups

Debounce duplicate note popups
pull/408/head
Adam Johnson 9 months ago committed by GitHub
parent
commit
46a72a6cd8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      korman/exporter/convert.py
  2. 92
      korman/exporter/gui.py
  3. 2
      korman/properties/modifiers/anim.py
  4. 2
      korman/properties/modifiers/avatar.py
  5. 2
      korman/properties/modifiers/game_gui.py
  6. 86
      korman/properties/modifiers/gui.py
  7. 2
      korman/properties/modifiers/logic.py
  8. 4
      korman/properties/modifiers/render.py
  9. 2
      korman/properties/modifiers/sound.py

6
korman/exporter/convert.py

@ -229,7 +229,7 @@ class Exporter:
for mod in bl_obj.plasma_modifiers.modifiers: for mod in bl_obj.plasma_modifiers.modifiers:
fn = getattr(mod, "sanity_check", None) fn = getattr(mod, "sanity_check", None)
if fn is not None: if fn is not None:
fn() fn(self)
inc_progress() inc_progress()
self.report.msg("... Age is grinning and holding a spatula. Must be OK, then.") self.report.msg("... Age is grinning and holding a spatula. Must be OK, then.")
@ -502,7 +502,9 @@ class Exporter:
# Wow, recursively generated objects. Aren't you special? # Wow, recursively generated objects. Aren't you special?
with indent(): with indent():
for mod in temporary.plasma_modifiers.modifiers: 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) do_pre_export(temporary)
return temporary return temporary

92
korman/exporter/gui.py

@ -32,6 +32,7 @@ from . import utils
if TYPE_CHECKING: if TYPE_CHECKING:
from .convert import Exporter from .convert import Exporter
from .logger import _ExportLogger as ExportLogger from .logger import _ExportLogger as ExportLogger
from ..properties.modifiers.game_gui import *
class Clipping(NamedTuple): class Clipping(NamedTuple):
@ -48,9 +49,13 @@ class GuiConverter:
if TYPE_CHECKING: if TYPE_CHECKING:
_parent: weakref.ref[Exporter] = ... _parent: weakref.ref[Exporter] = ...
_pages: Dict[str, Any] = ...
_mods_exported: Set[str] = ...
def __init__(self, parent: Optional[Exporter] = None): def __init__(self, parent: Optional[Exporter] = None):
self._parent = weakref.ref(parent) if parent is not None else 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. # Go ahead and prepare the GUI transparent material for future use.
if parent is not None: if parent is not None:
@ -203,6 +208,93 @@ class GuiConverter:
w2c[2, i] *= -1.0 w2c[2, i] *= -1.0
return PostEffectModMatrices(c2w, w2c) 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 @contextmanager
def generate_camera_render_settings(self, scene: bpy.types.Scene) -> Iterator[None]: 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 # Set the render info to basically TV NTSC 4:3, which will set Blender's camera

2
korman/properties/modifiers/anim.py

@ -42,7 +42,7 @@ class ActionModifier:
return None return None
raise ExportError("'{}': Object has an animation modifier but is not animated".format(bo.name)) 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: if not self.id_data.plasma_object.has_animation_data:
raise ExportError("'{}': Has an animation modifier but no animation data.", self.id_data.name) raise ExportError("'{}': Has an animation modifier but no animation data.", self.id_data.name)

2
korman/properties/modifiers/avatar.py

@ -189,7 +189,7 @@ class PlasmaSittingBehavior(idprops.IDPropObjectMixin, PlasmaModifierProperties,
# This should be an empty, really... # This should be an empty, really...
return True 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. # The user absolutely MUST specify a clickable or this won't export worth crap.
if self.clickable_object is None: if self.clickable_object is None:
raise ExportError("'{}': Sitting Behavior's clickable object is invalid".format(self.key_name)) raise ExportError("'{}': Sitting Behavior's clickable object is invalid".format(self.key_name))

2
korman/properties/modifiers/game_gui.py

@ -70,7 +70,7 @@ class _GameGuiMixin:
def requires_dyntext(self) -> bool: def requires_dyntext(self) -> bool:
return False return False
def sanity_check(self): def sanity_check(self, exporter):
age: PlasmaAge = bpy.context.scene.world.plasma_age age: PlasmaAge = bpy.context.scene.world.plasma_age
# Game GUI modifiers must be attached to objects in a GUI page, ONLY # Game GUI modifiers must be attached to objects in a GUI page, ONLY

86
korman/properties/modifiers/gui.py

@ -660,7 +660,7 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
share.link_input(share_anim_stage, "stage", "stage_refs") share.link_input(share_anim_stage, "stage", "stage_refs")
share.link_output(linkingnode, "hosts", "shareBookSeek") share.link_output(linkingnode, "hosts", "shareBookSeek")
def sanity_check(self): def sanity_check(self, exporter):
if self.clickable is None: if self.clickable is None:
raise ExportError("{}: Linking Book modifier requires a clickable!", self.id_data.name) raise ExportError("{}: Linking Book modifier requires a clickable!", self.id_data.name)
if self.seek_point is None: if self.seek_point is None:
@ -724,88 +724,18 @@ class PlasmaNotePopupModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz):
if self.id_data.type == "MESH": if self.id_data.type == "MESH":
return self.id_data 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) page_type = helpers.get_page_type(self.id_data.plasma_object.page)
if page_type != "room": if page_type != "room":
raise ExportError(f"Note Popup modifiers should be in a 'room' page, not a '{page_type}' page!") 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): def pre_export(self, exporter: Exporter, bo: bpy.types.Object):
guidialog_object = utils.create_empty_object(f"{self.gui_page}_NoteDialog") # The GUI converter will debounce duplicate GUI dialogs.
guidialog_object.plasma_object.enabled = True yield from exporter.gui.create_note_gui(self.gui_page, self.gui_camera)
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
# Auto-generate a six-foot cube region around the clickable if none was provided. # Auto-generate a six-foot cube region around the clickable if none was provided.
if self.clickable_region is None: if self.clickable_region is None:

2
korman/properties/modifiers/logic.py

@ -154,7 +154,7 @@ class PlasmaTelescope(PlasmaModifierProperties, PlasmaModifierLogicWiz):
type=bpy.types.Object, type=bpy.types.Object,
poll=idprops.poll_camera_objects) poll=idprops.poll_camera_objects)
def sanity_check(self): def sanity_check(self, exporter):
if self.camera_object is None: if self.camera_object is None:
raise ExportError(f"'{self.id_data.name}': Telescopes must specify a camera!") raise ExportError(f"'{self.id_data.name}': Telescopes must specify a camera!")

4
korman/properties/modifiers/render.py

@ -120,7 +120,7 @@ class PlasmaBlendMod(PlasmaModifierProperties):
for i in (j.blend_onto for j in self.dependencies if j.blend_onto is not None and j.enabled): for i in (j.blend_onto for j in self.dependencies if j.blend_onto is not None and j.enabled):
yield i yield i
def sanity_check(self): def sanity_check(self, exporter):
if self.has_circular_dependency: if self.has_circular_dependency:
raise ExportError("'{}': Circular Render Dependency detected!".format(self.id_data.name)) raise ExportError("'{}': Circular Render Dependency detected!".format(self.id_data.name))
@ -770,7 +770,7 @@ class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicW
def localization_set(self): def localization_set(self):
return "DynaTexts" return "DynaTexts"
def sanity_check(self): def sanity_check(self, exporter):
if self.texture is None: if self.texture is None:
raise ExportError("'{}': Localized Text modifier requires a texture", self.id_data.name) raise ExportError("'{}': Localized Text modifier requires a texture", self.id_data.name)

2
korman/properties/modifiers/sound.py

@ -533,7 +533,7 @@ class PlasmaSoundEmitter(PlasmaModifierProperties):
stereize_left = PointerProperty(type=bpy.types.Object, options={"HIDDEN", "SKIP_SAVE"}) stereize_left = PointerProperty(type=bpy.types.Object, options={"HIDDEN", "SKIP_SAVE"})
stereize_right = 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 modifiers = self.id_data.plasma_modifiers
# Sound emitters can potentially export sounds to more than one emitter SceneObject. Currently, # Sound emitters can potentially export sounds to more than one emitter SceneObject. Currently,

Loading…
Cancel
Save