#cython: profile=False # Copyright 2004-2019 Tom Rothamel # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from __future__ import print_function from renpy.display.matrix import Matrix, Matrix2D from renpy.display.matrix cimport Matrix, Matrix2D # This is required to get these to re-export despite having been defined # in cython. globals()["Matrix"] = Matrix globals()["Matrix2D"] = Matrix2D import collections import pygame_sdl2 as pygame import threading import renpy import gc import math # We grab the blit lock each time it is necessary to blit # something. This allows call to the pygame.transform functions to # disable blitting, should it prove necessary. blit_lock = threading.Condition() # This is a dictionary containing all the renders that we know of. It's a # map from displayable to dictionaries containing the render of that # displayable. render_cache = collections.defaultdict(dict) # The queue of redraws. A list of (time, displayable) pairs. redraw_queue = [ ] # The render returned from render_screen. screen_render = None # A list of renders the system knows about, and thinks are still alive. cdef list live_renders live_renders = [ ] # A copy of renpy.display.interface.frame_time, for speed reasons. cdef double frame_time frame_time = 0 # Are we doing a per_frame update? per_frame = False # Are we rendering for the purpose of sizing something. sizing = False # This is true if we're using a renderer that supports models, # false otherwise. models = False def adjust_render_cache_times(old_time, new_time): """ This adjusts the render cache such that if a render starts at old_time, it really started at new_time. """ for id_d, renders in render_cache.iteritems(): # Check to see if we have a render with st_base = old_time. If so, # we need to rebase it. for k in renders: if k[2] == old_time: break else: continue new_renders = { } for k, v in renders.iteritems(): w, h, st_base, at_base = k if st_base == old_time: st_base = new_time if at_base == old_time: at_base = new_time new_renders[(w, h, st_base, at_base)] = v render_cache[id_d] = new_renders def free_memory(): """ Frees memory used by the render system. """ global screen_render screen_render = None mark_sweep() render_cache.clear() # This can hang onto a render. renpy.display.interface.surftree = None def check_at_shutdown(): """ This is called at shutdown time to check that everything went okay. The big thing it checks for is memory leaks. """ if not renpy.config.developer: return free_memory() gc.collect() l = gc.get_objects() count = 0 objects = gc.get_objects() for i in objects: if isinstance(i, Render): count += 1 if count: raise Exception("%d Renders are alive at shutdown. This is probably a memory leak bug in Ren'Py." % count) # The number of things being rendered at the moment. cdef int rendering rendering = 0 # The st and at of the current call to render. render_st = 0.0 render_at = 0.0 cdef bint render_is_ready render_is_ready = 0 def render_ready(): global render_is_ready render_is_ready = 1 # These are good until the next call to render. render_width = 0 render_height = 0 cpdef render(d, object widtho, object heighto, double st, double at): """ :doc: udd_utility :args: (d, width, height, st, at) Causes a displayable to be rendered, and a renpy.Render object to be returned. `d` The displayable to render. `width`, `height` The width and height available for the displayable to render into. `st`, `at` The shown and animation timebases. Renders returned by this object may be cached, and should not be modified once they have been retrieved. """ global rendering global render_width global render_height global render_st global render_at cdef float width, height cdef float orig_width, orig_height cdef tuple orig_wh, wh cdef dict render_cache_d cdef Render rv if not render_is_ready: if renpy.config.developer: raise Exception("Displayables may not be rendered during the init phase.") orig_wh = (widtho, heighto, frame_time-st, frame_time-at) render_width = widtho render_height = heighto id_d = id(d) render_cache_d = render_cache[id_d] rv = render_cache_d.get(orig_wh, None) if rv is not None: return rv orig_width = width = widtho orig_height = height = heighto style = d.style xmaximum = style.xmaximum ymaximum = style.ymaximum if xmaximum is not None: if isinstance(xmaximum, float): width = width * xmaximum else: width = min(xmaximum, width) if ymaximum is not None: if isinstance(ymaximum, float): height = height * ymaximum else: height = min(ymaximum, height) if width < 0: width = 0 if height < 0: height = 0 if orig_width != width or orig_height != height: widtho = width heighto = height wh = (widtho, heighto, frame_time-st, frame_time-at) rv = render_cache_d.get(wh, None) if rv is not None: return rv else: wh = orig_wh renpy.plog(2, "start render {!r}", d) try: rendering += 1 old_st = render_st old_at = render_at render_st = st render_at = at rv = d.render(widtho, heighto, st, at) finally: rendering -= 1 render_st = old_st render_at = old_at if rv.__class__ is not Render: raise Exception("{!r}.render() must return a Render.".format(d)) rv.render_of.append(d) if d._clipping: renpy.plog(4, "before clipping") rv = rv.subsurface((0, 0, rv.width, rv.height), focus=True) rv.render_of.append(d) renpy.plog(4, "after clipping") if not sizing: # This lookup is needed because invalidations are possible. render_cache_d = render_cache[id_d] render_cache_d[wh] = rv if wh is not orig_wh: render_cache_d[orig_wh] = rv renpy.plog(2, "end render {!r}", d) return rv def render_for_size(d, width, height, st, at): """ This returns a render of `d` that's useful for getting the size or screen location, but not for actual rendering. """ global sizing id_d = id(d) orig_wh = (width, height, frame_time-st, frame_time-at) render_cache_d = render_cache[id_d] rv = render_cache_d.get(orig_wh, None) if rv is not None: return rv old_sizing = sizing sizing = True try: return render(d, width, height, st, at) finally: sizing = old_sizing def invalidate(d): """ Removes d from the render cache. If we're not in a redraw, triggers a redraw to start. """ if (not rendering) and (not per_frame) and (not sizing): redraw(d, 0) return for v in render_cache[id(d)].values(): v.kill_cache() def check_redraws(): """ Returns true if a redraw is required, and False otherwise. """ redraw_queue.sort() now = renpy.display.core.get_time() for when, d in redraw_queue: id_d = id(d) if id_d not in render_cache: continue if when <= now: return True return False def process_redraws(): """ Removes any pending redraws from the redraw queue. """ global redraw_queue redraw_queue.sort() now = renpy.display.core.get_time() rv = False new_redraw_queue = [ ] seen = set() for t in redraw_queue: when, d = t id_d = id(d) if id_d in seen: continue seen.add(id_d) if id_d not in render_cache: continue if when <= now: # Remove this displayable and all its parents from the # render cache. But don't kill them yet, as that will kill the # children that we want to reuse. for v in render_cache[id_d].values(): v.kill_cache() rv = True else: new_redraw_queue.append(t) redraw_queue = new_redraw_queue return rv def redraw_time(): """ Returns the time at which the next redraw is scheduled. """ if redraw_queue: return redraw_queue[0][0] return None def redraw(d, when): """ :doc: udd_utility Causes the displayable `d` to be redrawn after `when` seconds have elapsed. """ if not renpy.game.interface: return if per_frame: invalidate(d) return redraw_queue.append((when + renpy.game.interface.frame_time, d)) IDENTITY = Matrix2D(1, 0, 0, 1) def take_focuses(focuses): """ Adds a list of rectangular focus regions to the focuses list. """ screen_render.take_focuses( 0, 0, screen_render.width, screen_render.height, IDENTITY, 0, 0, None, focuses) # The result of focus_at_point for a modal render. This overrides any # specific focus from below us. Modal = renpy.object.Sentinel("Modal") def focus_at_point(x, y): """ Returns a focus object corresponding to the uppermost displayable at point, or None if nothing focusable is at point. """ if screen_render is None: return None cf = screen_render.focus_at_point(x, y, None) if cf is None or cf is Modal: return None else: d, arg, screen = cf return renpy.display.focus.Focus(d, arg, None, None, None, None, screen) def mutated_surface(surf): """ Called to indicate that the given surface has changed. """ renpy.display.draw.mutated_surface(surf) def render_screen(root, width, height): """ Renders `root` (a displayable) as the root of a screen with the given `width` and `height`. """ global screen_render global invalidated global frame_time interact_time = renpy.display.interface.interact_time frame_time = renpy.display.interface.frame_time if interact_time is None: st = 0 else: st = frame_time - interact_time rv = render(root, width, height, st, st) screen_render = rv invalidated = False rv.is_opaque() return rv def mark_sweep(): """ This performs mark-and-sweep garbage collection on the live_renders list. """ global live_renders cdef list worklist cdef int i cdef Render r, j worklist = [ ] if screen_render is not None: worklist.append(screen_render) i = 0 while i < len(worklist): r = worklist[i] for j in r.depends_on_list: if not j.mark: j.mark = True worklist.append(j) i += 1 if screen_render is not None: screen_render.mark = True for r in live_renders: if not r.mark: r.kill_cache() else: r.mark = False live_renders = worklist def compute_subline(sx0, sw, cx0, cw): """ Given a source line (start sx0, width sw) and a crop line (cx0, cw), return three things: * The offset of the portion of the source line that overlaps with the crop line, relative to the crop line. * The offset of the portion of the source line that overlaps with the the crop line, relative to the source line. * The length of the overlap in pixels. (can be <= 0) """ sx1 = sx0 + sw cx1 = cx0 + cw if sx0 > cx0: start = sx0 else: start = cx0 offset = start - cx0 crop = start - sx0 if sx1 < cx1: width = sx1 - start else: width = cx1 - start return offset, crop, width # Possible operations that can be done as part of a render. BLIT = 0 DISSOLVE = 1 IMAGEDISSOLVE = 2 PIXELLATE = 3 FLATTEN = 4 cdef class Render: def __init__(Render self, float width, float height, draw_func=None, layer_name=None, bint opaque=False): #@DuplicatedSignature """ Creates a new render corresponding to the given widget with the specified width and height. If `layer_name` is given, then this render corresponds to a layer. """ # The mark bit, used for mark/sweep-style garbage collection of # renders. self.mark = False # Is has this render been removed from the cache? self.cache_killed = False self.width = width self.height = height self.layer_name = layer_name # A list of (surface/render, xoffset, yoffset, focus, main) tuples, ordered from # back to front. self.children = [ ] # Forward is used to transform from screen coordinates to child # coordinates. # Reverse is used to transform from child coordinates to screen # coordinates. # # For performance reasons, these aren't used to transform the # x and y offsets found in self.children. Those offsets should # be of the (0, 0) point in the child coordinate space. self.forward = None self.reverse = None # This is used to adjust the alpha of children of this render. self.alpha = 1 # The over blending factor. When this is 1.0, blends only use the # over operation. When set to 0.0, we get additive blending. self.over = 1.0 # If true, children of this render use nearest-neighbor texture # lookup. If false, bilinear, if None, from the parent. self.nearest = None # A list of focus regions in this displayable. self.focuses = None # Other renders that we should pass focus onto. self.pass_focuses = None # The ScreenDisplayable this is a render of. self.focus_screen = None # The displayable(s) that this is a render of. (Set by render) self.render_of = [ ] # If set, this is a function that's called to draw this render # instead of the default. self.draw_func = draw_func # Is this displayable opaque? (May be set on init, or later on # if we have opaque children.) This may be True, False, or None # to indicate we don't know yet. self.opaque = opaque # A list of our visible children. (That is, children above and # including our uppermost opaque child.) If nothing is opaque, # includes all children. self.visible_children = self.children # Should children be clipped to a rectangle? self.xclipping = False self.yclipping = False # Are we modal? self.modal = False # Are we a text input? self.text_input = False # gl, sw # The set of renders that either have us as children, or depend on # us. self.parents = set() # The renders we depend on, including our children. self.depends_on_list = [ ] # The operation we're performing. (BLIT, DISSOLVE, OR IMAGE_DISSOLVE) self.operation = BLIT # The fraction of the operation that is complete. self.operation_complete = 0.0 # Should the dissolve operations preserve alpha? self.operation_alpha = False # The parameter to the operation. self.operation_parameter = 0 # Caches of the texture created by rendering this surface. self.surface = None self.alpha_surface = None # Cache of the texture created by rendering this surface at half size. # (This is set in gldraw.) self.half_cache = None # gl2 # The mesh. If this is not None, the children are all rendered to Textures, # and used to form a model. If this is True, the Mesh is taken from the first # child's Texture, otherwise this must be a Mesh. self.mesh = None # A tuple of shaders that will be used when rendering, or None. self.shaders = None # A dictionary containing uniforms that will be used when rendering, or # None. self.uniforms = None # Properties that are used for rendering. self.properties = None # Used to cache the result of rendering this Render to a texture. self.cached_texture = None # Used to cache the model. self.cached_model = None # Have the textures been loaded? self.loaded = False live_renders.append(self) def __repr__(self): #@DuplicatedSignature return "<{}Render {:x} of {!r}>".format( ("dead " if self.cache_killed else ""), id(self), self.render_of) def __getstate__(self): #@DuplicatedSignature if renpy.config.developer: raise Exception("Can't pickle a Render.") else: return { } def __setstate__(self, state): #@DuplicatedSignature return cpdef int blit(Render self, source, tuple pos, object focus=True, object main=True, object index=None): """ Blits `source` (a Render or Surface) to this Render, offset by xo and yo. If `focus` is true, then focuses are added from the child to the parent. This will only blit on integer pixel boundaries. """ (xo, yo) = pos if source is self: raise Exception("Blitting to self.") xo = int(xo) yo = int(yo) if index is None: self.children.append((source, xo, yo, focus, main)) else: self.children.insert(index, (source, xo, yo, focus, main)) if isinstance(source, Render): self.depends_on_list.append(source) source.parents.add(self) return 0 cpdef int subpixel_blit(Render self, source, tuple pos, object focus=True, object main=True, object index=None): """ Blits `source` (a Render or Surface) to this Render, offset by xo and yo. If `focus` is true, then focuses are added from the child to the parent. This blits at fractional pixel boundaries. """ (xo, yo) = pos xo = float(xo) yo = float(yo) if index is None: self.children.append((source, xo, yo, focus, main)) else: self.children.insert(index, (source, xo, yo, focus, main)) if isinstance(source, Render): self.depends_on_list.append(source) source.parents.add(self) return 0 cpdef int absolute_blit(Render self, source, tuple pos, object focus=True, object main=True, object index=None): """ Blits `source` (a Render or Surface) to this Render, offset by xo and yo. If `focus` is true, then focuses are added from the child to the parent. This blits at fractional pixel boundaries. """ (xo, yo) = pos xo = renpy.display.core.absolute(xo) yo = renpy.display.core.absolute(yo) if index is None: self.children.append((source, xo, yo, focus, main)) else: self.children.insert(index, (source, xo, yo, focus, main)) if isinstance(source, Render): self.depends_on_list.append(source) source.parents.add(self) return 0 def get_size(self): """ Returns the size of this Render, a mostly ficticious value that's taken from the inputs to the constructor. (As in, we don't clip to this size.) """ return self.width, self.height def render_to_texture(self, alpha=True): """ Returns a texture constructed from this render. This may return a cached texture, if one has already been rendered. `alpha` is a hint that controls if the surface should have alpha or not. This returns a texture that's at the drawable resolution, which may be bigger than the virtual resolution. Use renpy.display.draw.draw_to_virt and draw.virt_to_draw to convert between the two resolutions. (For example, multiply reverse by draw_to_virt to scale this down for blitting.) """ if alpha: if self.alpha_surface is not None: return self.alpha_surface else: if self.surface is not None: return self.surface rv = renpy.display.draw.render_to_texture(self, alpha) # Stash and return the surface. if alpha: self.alpha_surface = rv else: self.surface = rv return rv pygame_surface = render_to_texture def subsurface(self, rect, focus=False): """ Returns a subsurface of this render. If `focus` is true, then the focuses are copied from this render to the child. """ (x, y, w, h) = rect x = int(x) y = int(y) w = int(w) h = int(h) rv = Render(w, h) reverse = self.reverse # This code doesn't work. We need to do the clipping in the screen # space, or it's too easy to get overlaps or lines. (At some point, # we should optimize things and only clip when necessary.) # if False and ((reverse is not None) and (reverse.xdx != 1.0 or reverse.ydy != 1.0) and # (reverse.xdx > 0.0 and # reverse.xdy == 0.0 and # reverse.ydx == 0.0 and # reverse.ydy > 0.0)): # # # When rectangle-aligned but not 1:1, transform the rectangle and # # keep cropping. # # w, h = self.forward.transform(w + x, h + y) # x, y = self.forward.transform(x, y) # # # x = int(x) # y = int(y) # w = int(w) - x + 1 # h = int(h) - y # # if (w <= 0) or (h <= 0): # return rv # # xdx = 1.0 * rect[2] / w # ydy = 1.0 * rect[3] / h # # rv.reverse = Matrix2D(xdx, 0.0, 0.0, ydy) # rv.forward = Matrix2D(1.0 / xdx, 0.0, 0.0, 1.0 / ydy) if ((reverse is not None) and (reverse.xdx != 1.0 or reverse.xdy != 0.0 or reverse.ydx != 0.0 or reverse.ydy != 1.0)): # This doesn't actually make a subsurface, as we can't easily do # so for non-rectangle-aligned renders. rv.xclipping = True rv.yclipping = True # Try to avoid clipping if a surface fits entirely inside the # rectangle. if (reverse.xdx > 0.0 and reverse.xdy == 0.0 and reverse.ydx == 0.0 and reverse.ydy > 0.0): tx, ty = self.forward.transform(x, y) tw, th = self.forward.transform(w + x, h + y) rw, rh = self.forward.transform(self.width, self.height) if (tx <= 0) and (tw >= rw): rv.xclipping = False if (ty <= 0) and (th >= rh): rv.yclipping = False rv.blit(self, (-x, -y), focus=focus, main=True) return rv # This is the path that executes for rectangle-aligned surfaces, # making an actual subsurface. for child, cx, cy, cfocus, cmain in self.children: childw, childh = child.get_size() xo, cx, cw = compute_subline(cx, childw, x, w) yo, cy, ch = compute_subline(cy, childh, y, h) if cw <= 0 or ch <= 0 or w - xo <= 0 or h - yo <= 0: continue if cx < 0 or cx >= childw or cy < 0 or cy >= childh: continue offset = (xo, yo) crop = None try: if isinstance(child, Render): if child.xclipping: cropw = cw else: cropw = w - xo if child.yclipping: croph = ch else: croph = h - yo crop = (cx, cy, cropw, croph) newchild = child.subsurface(crop, focus=focus) newchild.width = cw newchild.height = ch newchild.render_of = child.render_of[:] else: crop = (cx, cy, cw, ch) newchild = child.subsurface(crop) renpy.display.draw.mutated_surface(newchild) except: raise Exception("Creating subsurface failed. child size = ({}, {}), crop = {!r}".format(childw, childh, crop)) rv.blit(newchild, offset, focus=cfocus, main=cmain) if focus and self.focuses: for (d, arg, xo, yo, fw, fh, mx, my, mask) in self.focuses: if xo is None: rv.add_focus(d, arg, xo, yo, fw, fh, mx, my, mask) continue xo, cx, fw = compute_subline(xo, fw, x, w) yo, cy, fh = compute_subline(yo, fh, y, h) if fw <= 0 or fh <= 0: continue if mx is not None: mw, mh = mask.get_size() mx, mcx, mw = compute_subline(mx, mw, x, w) my, mcy, mh = compute_subline(my, mh, y, h) if mw <= 0 or mh <= 0: mx = None my = None mask = None else: mask = mask.subsurface((mcx, mcy, mw, mh)) rv.add_focus(d, arg, xo, yo, fw, fh, mx, my, mask) rv.depends_on(self) rv.alpha = self.alpha rv.operation = self.operation rv.operation_alpha = self.operation_alpha rv.operation_complete = self.operation_complete rv.nearest = self.nearest rv.text_input = self.text_input return rv def depends_on(self, source, focus=False): """ Used to indicate that this render depends on another render. Useful, for example, if we use pygame_surface to make a surface, and then blit that surface into another render. """ if source is self: raise Exception("Render depends on itself.") self.depends_on_list.append(source) source.parents.add(self) if focus: if self.pass_focuses is None: self.pass_focuses = [ source ] else: self.pass_focuses.append(source) def kill_cache(self): """ Removes this render and its transitive parents from the cache. """ if self.cache_killed: return self.cache_killed = True for i in self.parents: i.kill_cache() self.parents.clear() for i in self.depends_on_list: if not i.cache_killed: i.parents.discard(self) for ro in self.render_of: id_ro = id(ro) cache = render_cache[id_ro] for k, v in cache.items(): if v is self: del cache[k] if not cache: del render_cache[id_ro] self.render_of = None self.focuses = None self.pass_focuses = None def kill(self): """ Retained for compatibility, but does not need to be called. """ def add_focus(self, d, arg=None, x=0, y=0, w=None, h=None, mx=None, my=None, mask=None): """ This is called to indicate a region of the screen that can be focused. `d` - the displayable that is being focused. `arg` - an argument. The rest of the parameters are a rectangle giving the portion of this region corresponding to the focus. If they are all None, than this focus is assumed to be the singular full-screen focus. """ if isinstance(mask, Render) and mask is not self: self.depends_on(mask) t = (d, arg, x, y, w, h, mx, my, mask) if self.focuses is None: self.focuses = [ t ] else: self.focuses.append(t) def take_focuses(self, cminx, cminy, cmaxx, cmaxy, reverse, x, y, screen, focuses): #@DuplicatedSignature """ This adds to focuses Focus objects corresponding to the focuses added to this object and its children, transformed into screen coordinates. `cminx`, `cminy`, `cmaxx`, `cmaxy` The clipping rectangle. `reverse` The transform from render to screen coordinates. `x`, `y` The offset of the upper-left corner of the render. `screen` The screen this is a render of, or None if this is not part of a screen. `focuses` The list of focuses to add to. """ if self.focus_screen is not None: screen = self.focus_screen if self.modal: focuses[:] = [ ] if self.reverse: reverse = reverse * self.reverse if self.focuses: for (d, arg, xo, yo, w, h, mx, my, mask) in self.focuses: if xo is None: focuses.append(renpy.display.focus.Focus(d, arg, None, None, None, None, screen)) continue x1, y1 = reverse.transform(xo, yo) x2, y2 = reverse.transform(xo + w, yo + h) minx = min(x1, x2) + x miny = min(y1, y2) + y maxx = max(x1, x2) + x maxy = max(y1, y2) + y minx = max(minx, cminx) miny = max(miny, cminy) maxx = min(maxx, cmaxx) maxy = min(maxy, cmaxy) if minx >= maxx or miny >= maxy: continue focuses.append(renpy.display.focus.Focus(d, arg, minx, miny, maxx - minx, maxy - miny, screen)) if self.xclipping: cminx = max(cminx, x) cmaxx = min(cmaxx, x + self.width) if self.yclipping: cminy = max(cminy, y) cmaxy = min(cmaxx, x + self.height) for child, xo, yo, focus, main in self.children: if not focus or not isinstance(child, Render): continue xo, yo = reverse.transform(xo, yo) child.take_focuses(cminx, cminy, cmaxx, cmaxy, reverse, x + xo, y + yo, screen, focuses) if self.pass_focuses: for child in self.pass_focuses: child.take_focuses(cminx, cminy, cmaxx, cmaxy, reverse, x, y, screen, focuses) def focus_at_point(self, x, y, screen): #@DuplicatedSignature """ This returns the focus of this object at the given point. """ if self.focus_screen is not None: screen = self.focus_screen if self.xclipping: if x < 0 or x >= self.width: return None if self.yclipping: if y < 0 or y >= self.height: return None if self.operation == IMAGEDISSOLVE: if not self.children[0][0].is_pixel_opaque(x, y): return None rv = None if self.focuses: for (d, arg, xo, yo, w, h, mx, my, mask) in self.focuses: if xo is None: continue elif mx is not None: cx = x - mx cy = y - my if self.forward: cx, cy = self.forward.transform(cx, cy) if isinstance(mask, Render): if mask.is_pixel_opaque(cx, cy): rv = d, arg, screen else: if mask(cx, cy): rv = d, arg, screen elif xo <= x < xo + w and yo <= y < yo + h: rv = d, arg, screen for child, xo, yo, focus, main in self.children: if not focus or not isinstance(child, Render): continue cx = x - xo cy = y - yo if self.forward: cx, cy = self.forward.transform(cx, cy) cf = child.focus_at_point(cx, cy, screen) if cf is not None: rv = cf if self.pass_focuses: for child in self.pass_focuses: cf = child.focus_at_point(x, y, screen) if cf is not None: rv = cf if rv is None and self.modal: rv = Modal return rv def main_displayables_at_point(self, x, y, layers, depth=None): """ Returns the displayable at `x`, `y` on one of the layers in the set or list `layers`. """ rv = [ ] if x < 0 or y < 0 or x >= self.width or y >= self.height: return rv is_screen = False if depth is not None: for d in self.render_of: rv.append((depth, self.width, self.height, d)) depth += 1 if isinstance(d, renpy.display.screen.ScreenDisplayable): is_screen = True elif self.layer_name in layers: depth = 0 for (child, xo, yo, focus, main) in self.children: if not main or not isinstance(child, Render): continue cx = x - xo cy = y - yo if self.forward: cx, cy = self.forward.transform(cx, cy) if is_screen: # Ignore the fixed at the root of every screen. cf = child.main_displayables_at_point(cx, cy, layers, depth - 1) rv.extend(cf[1:]) else: cf = child.main_displayables_at_point(cx, cy, layers, depth) rv.extend(cf) return rv def is_opaque(self): """ Returns true if this displayable is opaque, or False otherwise. Also sets self.visible_children. """ if self.opaque is not None: return self.opaque # A rotated image is never opaque. (This isn't actually true, but it # saves us from the expensive calculations require to prove it is.) if self.forward: self.opaque = False return False rv = False vc = [ ] for i in self.children: child, xo, yo, focus, main = i if xo <= 0 and yo <= 0: cw, ch = child.get_size() if cw + xo < self.width or ch + yo < self.height: if child.is_opaque(): vc = [ ] rv = True vc.append(i) self.visible_children = vc self.opaque = rv return rv def is_pixel_opaque(self, x, y): """ Determine if the pixel at x and y is opaque or not. """ if x < 0 or y < 0 or x >= self.width or y >= self.height: return False if self.is_opaque(): return True return renpy.display.draw.is_pixel_opaque(self, x, y) def fill(self, color): """ Fills this Render with the given color. """ color = renpy.easy.color(color) solid = renpy.display.imagelike.Solid(color) surf = render(solid, self.width, self.height, 0, 0) self.blit(surf, (0, 0), focus=False, main=False) def canvas(self): """ Returns a canvas object that draws to this Render. """ surf = renpy.display.pgrender.surface((self.width, self.height), True) mutated_surface(surf) self.blit(surf, (0, 0)) return Canvas(surf) def screen_rect(self, double sx, double sy, Matrix transform): """ Returns the rectangle, in screen-space coordinates, that will be covered by this render when it's drawn to the screen at sx, sy, with the transform `transform`. """ if transform is None: transform = IDENTITY cdef double w = self.width cdef double h = self.height cdef double xdx = transform.xdx cdef double xdy = transform.xdy cdef double ydx = transform.ydx cdef double ydy = transform.ydy # Transform the vertex coordinates to screen-space. cdef double x0 = sx cdef double y0 = sy cdef double x1 = w * xdx + sx cdef double y1 = w * ydx + sy cdef double x2 = h * xdy + sx cdef double y2 = h * ydy + sy cdef double x3 = w * xdx + h * xdy + sx cdef double y3 = w * ydx + h * ydy + sy cdef double minx = min(x0, x1, x2, x3) cdef double maxx = max(x0, x1, x2, x3) cdef double miny = min(y0, y1, y2, y3) cdef double maxy = max(y0, y1, y2, y3) return ( int(minx), int(miny), int(math.ceil(maxx - minx)), int(math.ceil(maxy - miny)), ) def place(self, d, x=0, y=0, width=None, height=None, st=None, at=None, render=None, main=True): """ Documented in udd.rst. """ if width is None: width = self.width if height is None: height = self.height if render is None: if st is None: st = render_st if at is None: at = render_at render = renpy.display.render.render(d, width, height, st, at) d.place(self, x, y, width, height, render, main=main) def zoom(self, xzoom, yzoom): """ Sets the zoom factor applied to this displayable's children. """ if self.reverse is None: self.reverse = IDENTITY self.forward = IDENTITY self.reverse *= Matrix2D(xzoom, 0, 0, yzoom) if xzoom and yzoom: self.forward *= Matrix2D(1.0 / xzoom, 0, 0, 1.0 / yzoom) else: self.forward *= Matrix2D(0, 0, 0, 0) class Canvas(object): def __init__(self, surf): #@DuplicatedSignature self.surf = surf def rect(self, color, rect, width=0): try: blit_lock.acquire() pygame.draw.rect(self.surf, renpy.easy.color(color), rect, width) finally: blit_lock.release() def polygon(self, color, pointlist, width=0): try: blit_lock.acquire() pygame.draw.polygon(self.surf, renpy.easy.color(color), pointlist, width) finally: blit_lock.release() def circle(self, color, pos, radius, width=0): try: blit_lock.acquire() pygame.draw.circle(self.surf, renpy.easy.color(color), pos, radius, width) finally: blit_lock.release() def ellipse(self, color, rect, width=0): try: blit_lock.acquire() pygame.draw.ellipse(self.surf, renpy.easy.color(color), rect, width) finally: blit_lock.release() def arc(self, color, rect, start_angle, stop_angle, width=1): try: blit_lock.acquire() pygame.draw.arc(self.surf, renpy.easy.color(color), rect, start_angle, stop_angle, width) finally: blit_lock.release() def line(self, color, start_pos, end_pos, width=1): try: blit_lock.acquire() pygame.draw.line(self.surf, renpy.easy.color(color), start_pos, end_pos, width) finally: blit_lock.release() def lines(self, color, closed, pointlist, width=1): try: blit_lock.acquire() pygame.draw.lines(self.surf, renpy.easy.color(color), closed, pointlist, width) finally: blit_lock.release() def aaline(self, color, startpos, endpos, blend=1): try: blit_lock.acquire() pygame.draw.aaline(self.surf, renpy.easy.color(color), startpos, endpos, blend) finally: blit_lock.release() def aalines(self, color, closed, pointlist, blend=1): try: blit_lock.acquire() pygame.draw.aalines(self.surf, renpy.easy.color(color), closed, pointlist, blend) finally: blit_lock.release() def get_surface(self): return self.surf