def canvas_to_view(self, in_coords, clamp_to_canvas=False): """ Return canvas coordinates in viewport coordinates as a Loc. :param in_coords: Canvas coordinates as a Loc. :param clamp_to_canvas: Whether to clamp coordinates to valid viewport coordinates. :return: Viewport coordinates converted from in_coords. """ canvas_dim = self.get_canvas_dim() tr, bl = self.get_view_coords() if clamp_to_canvas: coords = Loc( MathStat.map_range_clamped(in_coords.x, 0, canvas_dim.x, bl.x, tr.x), MathStat.map_range_clamped(in_coords.y, canvas_dim.y, 0, bl.y, tr.y)) else: coords = Loc( MathStat.map_range(in_coords.x, 0, canvas_dim.x, bl.x, tr.x), MathStat.map_range(in_coords.y, canvas_dim.y, 0, bl.y, tr.y)) return coords
def inp_press(self, event, func=None): """Bind this to a widget on a ButtonPress-X event""" self._isheld = True ##self._held_buttons[event.num] = True self._last_loc = Loc(event.x, event.y) self._orig_press_loc = self._last_loc.copy() # call function if specified with event if func: func(event)
def on_any_mov(self, event): # Cancel old canvas clear timer. if self.atimer is not None: self.after_cancel(self.atimer) # Delete old visualisation preview. self.acanvas.delete("delta") # Create a new line of delta preview. center = Loc(int(self.acanvas.cget("width")), int(self.acanvas.cget("height"))) // 2 offset = event.delta * 50 target = center + offset coords2 = MathStat.lerp(self.last_a_pos, target, self.interp_speed) self.last_a_pos = coords2 self.acanvas.create_line(center, coords2, fill=self.acol, width=5, tags=("delta", "line")) self.acanvas.create_text(center + (0, 80), text=str(round(event.delta, 1)), tags=("delta")) self.acanvas.create_oval(coords2 + 5, coords2 - 5, fill=self.acol, outline=self.acol, tags=("delta")) # Set timer to remove after 50ms. self.atimer = self.after(50, lambda: self.acanvas.delete("line"))
def deferred_spawn_actor(self, actor_class, loc): """ Begin to initialise a new actor in this world, then allow attributes to be set before finishing the spawning process. Warning: It is not safe to call any gameplay functions (eg tick) or anything involving the world because the actor is not officially in the world yet. * actor_class: class of actor to spawn * loc: location to spawn actor at * return: initialised actor object if successful, otherwise None """ # validate actor_class to check it is valid if actor_class is None: return None # initialise new actor object actor_object = actor_class() actor_object.__spawn__(self, Loc(loc)) # return the newly created actor_object for further modification and # to pass in to finish_deferred_spawn_actor return actor_object
def generate_reg_poly(num_sides, **kw): """ Generate a set of vertices for a regular polygon. :param num_sides: (int) Number of sides of the polygon. :keyword center: (Loc) Center point of the polygon, in relative coordinates. Defaults to origin. :keyword radius: (float) Radius of the circle bounds by the polygon's vertices, in world units. Defaults to 1.0. :keyword radial_offset: (float) Angle to rotate the polygon, in radians. Defaults to 0.0. :return: A generator that yields vertices as Loc objects. """ radius = kw.get("radius", 1.0) center = kw.get("center", Loc(0, 0)) # Rotate the polygon so that the bottom has a flat side. radial_offset = GeomHelper.get_poly_start_angle(num_sides) to_add = kw.get("radial_offset") if to_add is not None: radial_offset += to_add for i in range(num_sides): yield GeomHelper.get_nth_vertex_offset(i, num_sides, radius, radial_offset) \ + center
def get_unit_vector(angle): """ Find the unit vector in the direction of the angle. :param angle: (float) Angle of vector, in radians. :return: (Loc) The unit vector. """ return Loc(math.sin(angle), math.cos(angle))
def on_graph_button_release_input(self, event): """Call input events on nodes that are released.""" # Find the node we released. center = Loc(event.x, event.y) found_nodes = [] self.multi_box_trace_for_objects(center, 2, found_nodes) for node in found_nodes: if not node.generate_click_events: continue node.on_release(event)
def get_mouse_screen_position(self): """Returns mouse position in absolute screen coordinates. :rtype: Loc """ if (GameplayStatics.is_game_valid() and self.winfo_exists() and GameplayStatics.game_engine._window.winfo_exists() ): return Loc(self.winfo_pointerx(), self.winfo_pointery()) return None
def get_mouse_viewport_position(self): """Returns mouse position in viewport screen coordinates. :rtype: Loc """ screen_pos = self.get_mouse_screen_position() if screen_pos is None: return return screen_pos - Loc(self.winfo_rootx(), self.winfo_rooty())
def __init__(self): """Set default values.""" super().__init__() ## Amount of viewport padding to give when deciding whether to draw self.drawable_padding = Loc(300, 300) ## Whether to generate cursor over events. self.generate_cursor_over_events = True ## Whether to generate click events (default LMB). self.generate_click_events = True
def __init__(self, name=None, cnf={}, master=None, **kw): """Create an image with NAME. Valid resource names: data, format, file, gamma, height, palette, width. """ # Currently displayed image proportion as an integer fraction. # Store in a list to avoid instant simplification where possible. self.current_frac = Loc(Loc(1, 1), Loc(1, 1)) # Current input values received from using fast mode (continuous). When # scale_continuous_end is called the accurate value will be computed from # these values. self.current_fast_input = (1, 1) # Sensitivity to unprecise easy to compute figures # Greater remainder tolerance means less precision is retained. self.remainder_tolerance = 1 # Cached image data. # Use preloaded base64 data so we don't have to read # from HDD every time (slower) self._imgdata = None # Reference to timer for automatically calling scale_continuous_end # after scaling is finished. self._end_scale_timer = None # Function to be called when scaling is finished and the reference to the # new image must be used somewhere. # :param: Reference to new image self.on_assign_image = None # Attempt to load image data. kw.update(cnf) cnf = {} self._load_image_data(kw) super().__init__(name=name, cnf=cnf, master=master, **kw)
def __draw_grid_origin_lines(self): """Create grid lines for origin lines of x and y.""" graph = self.world dim = graph.get_canvas_dim() line_color = self.origin_line_color.to_hex() # Vertical c1 = graph.view_to_canvas(Loc(0, 0)) if c1.x > 0 and c1.x < dim.x: c1.y = 0 c2 = c1 + (0, dim.y) graph.create_line(c1, c2, fill=line_color, width=3, tags=(self.unique_id, "origin_line_vertical")) # Horizontal c1 = graph.view_to_canvas(Loc(0, 0)) if c1.y > 0 and c1.y < dim.y: c1.x = 0 c2 = c1 + (dim.x, 0) graph.create_line(c1, c2, fill=line_color, width=3, tags=(self.unique_id, "origin_line_horizontal"))
def on_vert_mov(self, event): # Cancel old canvas clear timer. if self.vtimer is not None: self.after_cancel(self.vtimer) # Delete old visualisation preview. self.vcanvas.delete("delta") # Create a new line of delta preview. center = Loc(int(self.vcanvas.cget("width")), int(self.vcanvas.cget("height"))) // 2 # WARNING: The delta will contain x and y components, even if # this function is only called on changes to one axis! offset = Loc(0, event.delta.y * 50) target = center + offset coords2 = MathStat.lerp(self.last_v_pos, target, self.interp_speed) self.last_v_pos = coords2 self.vcanvas.create_line(center, coords2, fill=self.vcol, width=5, tags=("delta", "line")) self.vcanvas.create_text(center + (0, 80), text=str(round(event.delta, 1)), tags=("delta")) self.vcanvas.create_oval(coords2 + 5, coords2 - 5, fill=self.vcol, outline=self.vcol, tags=("delta")) # Set timer to remove after 50ms. self.vtimer = self.after(50, lambda: self.vcanvas.delete("line"))
def __init__(self, master=None, cnf={}, **kw): """Initialiase blueprint graph in widget MASTER.""" # Set default values. ## Offset of viewport from center of the graph in pixels. self._view_offset = Loc(0, 0) ## Relative zoom factor of the viewport. self._zoom_amt = 0.0 ## Set zoom ratio. Default is 3. self._zoom_ratio = 3 ## Inversion scale for graph motion, for x and y independently. ## Should only be 1 or -1 per axis. self.axis_inversion = Loc(1, 1) # Initialise canvas parent. Canvas.__init__(self, master, cnf, **kw) self.config(bg=FColor(240).to_hex()) self.__setup_input_bindings()
def on_graph_motion_input(self, event): """Called when a motion event occurs on the graph.""" # Calculate world displacement between 2 canvas pixels, # but not necessarily touching pixels. canvas_a = Loc(0, 0) # Start from top left corner canvas_b = canvas_a + event.delta # MotionInput's delta in capped to 5 pixels of movement per event. canvas_b *= 5 world_displacement = \ self.canvas_to_view(canvas_a) - self.canvas_to_view(canvas_b) self._view_offset += world_displacement
def inp_motion(self, event, func=None): """Bind this to a widget on a ButtonX-Motion event""" def near_to_zero(val, offset=0.05): return val < offset and val > -offset if self._isheld: # get and set delta new_loc = Loc(event.x, event.y) self._delta = d = new_loc - self._last_loc if self._use_normalisation: self._normalise_delta(d) else: d *= 0.2 # set delta in event object to return in callbacks event.delta = d # ensure the last location is updated for next motion self._last_loc = new_loc # call function if specified with event and delta (in event) if func: func(event) # fire bound events for button invariant bindings if not near_to_zero(d.x): be = self._get_bound_events("X", "Motion") if be: for func in be: func(event) if not near_to_zero(d.y): be = self._get_bound_events("Y", "Motion") if be: for func in be: func(event) if not near_to_zero(d.x) or not near_to_zero(d.y): be = self._get_bound_events("XY", "Motion") if be: for func in be: func(event)
def _normalise_delta(self, in_delta, set_in_place=True): """Normalises in_delta to range (-1, 1). Optionally don't set in place""" # set attributes of passed in list object if necessary if set_in_place: in_delta.x = MathStat.map_range(in_delta.x, -self._normalised_delta_max, self._normalised_delta_max, -1, 1) in_delta.y = MathStat.map_range(in_delta.y, -self._normalised_delta_max, self._normalised_delta_max, -1, 1) return in_delta # otherwise initialise a new Loc object else: return Loc( MathStat.map_range(in_delta.x, -self._normalised_delta_max, self._normalised_delta_max, -1, 1), MathStat.map_range(in_delta.y, -self._normalised_delta_max, self._normalised_delta_max, -1, 1))
def on_graph_pointer_movement_input(self, event): """Call input events on nodes that that are hovered.""" center = Loc(event.x, event.y) found_nodes = [] self.multi_box_trace_for_objects(center, 2, found_nodes) # Use sets for easy intersection and overlaps. found_nodes = set(found_nodes) hovered_nodes = self.render_manager.hovered_nodes # Call mouse leave events on nodes that are no longer hovered. for node in hovered_nodes.difference(found_nodes): if not node.generate_cursor_over_events: continue node.on_end_cursor_over(event) # Call mouse over events on nodes that are newly hovered. for node in found_nodes.difference(hovered_nodes): if not node.generate_cursor_over_events: continue node.on_begin_cursor_over(event) # Save state for next call. self.render_manager.hovered_nodes = found_nodes
def begin_play(self): # Spawn the world render manager first for tick priority. self._render_manager = self.spawn_actor(RenderManager, Loc(0, 0))
class MotionInputTest(GuiTest): # Colors to use for vertical, horizontal and any direction visualisation. hcol = "dark green" vcol = "dark cyan" acol = "dark red" last_h_pos = Loc(0, 0) last_v_pos = Loc(0, 0) last_a_pos = Loc(0, 0) interp_speed = 0.4 _test_name = "Motion Input Visualisation" def on_horiz_mov(self, event): # Cancel old canvas clear timer. if self.htimer is not None: self.after_cancel(self.htimer) # Delete old visualisation preview. self.hcanvas.delete("delta") # Create a new line of delta preview. center = Loc(int(self.hcanvas.cget("width")), int(self.hcanvas.cget("height"))) // 2 # WARNING: The delta will contain x and y components, even if # this function is only called on changes to one axis! offset = Loc(event.delta.x * 50, 0) target = center + offset coords2 = MathStat.lerp(self.last_h_pos, target, self.interp_speed) self.last_h_pos = coords2 self.hcanvas.create_line(center, coords2, fill=self.hcol, width=5, tags=("delta", "line")) self.hcanvas.create_text(center + (0, 80), text=str(round(event.delta, 1)), tags=("delta")) self.hcanvas.create_oval(coords2 + 5, coords2 - 5, fill=self.hcol, outline=self.hcol, tags=("delta")) # Set timer to remove after 50ms. self.htimer = self.after(50, lambda: self.hcanvas.delete("line")) def on_vert_mov(self, event): # Cancel old canvas clear timer. if self.vtimer is not None: self.after_cancel(self.vtimer) # Delete old visualisation preview. self.vcanvas.delete("delta") # Create a new line of delta preview. center = Loc(int(self.vcanvas.cget("width")), int(self.vcanvas.cget("height"))) // 2 # WARNING: The delta will contain x and y components, even if # this function is only called on changes to one axis! offset = Loc(0, event.delta.y * 50) target = center + offset coords2 = MathStat.lerp(self.last_v_pos, target, self.interp_speed) self.last_v_pos = coords2 self.vcanvas.create_line(center, coords2, fill=self.vcol, width=5, tags=("delta", "line")) self.vcanvas.create_text(center + (0, 80), text=str(round(event.delta, 1)), tags=("delta")) self.vcanvas.create_oval(coords2 + 5, coords2 - 5, fill=self.vcol, outline=self.vcol, tags=("delta")) # Set timer to remove after 50ms. self.vtimer = self.after(50, lambda: self.vcanvas.delete("line")) def on_any_mov(self, event): # Cancel old canvas clear timer. if self.atimer is not None: self.after_cancel(self.atimer) # Delete old visualisation preview. self.acanvas.delete("delta") # Create a new line of delta preview. center = Loc(int(self.acanvas.cget("width")), int(self.acanvas.cget("height"))) // 2 offset = event.delta * 50 target = center + offset coords2 = MathStat.lerp(self.last_a_pos, target, self.interp_speed) self.last_a_pos = coords2 self.acanvas.create_line(center, coords2, fill=self.acol, width=5, tags=("delta", "line")) self.acanvas.create_text(center + (0, 80), text=str(round(event.delta, 1)), tags=("delta")) self.acanvas.create_oval(coords2 + 5, coords2 - 5, fill=self.acol, outline=self.acol, tags=("delta")) # Set timer to remove after 50ms. self.atimer = self.after(50, lambda: self.acanvas.delete("line")) # Setup widgets and movement components. def start(self): """Called when initialised to create test widgets.""" # Initialise timer variables to None. (no need to clear canvas yet!) self.vtimer = self.htimer = self.atimer = None Label(self, text="WARNING: The delta will contain x and y components, " "even if that function is only called on changes to one axis!\n" "WARNING 2: Smoothing not included").pack() # Horizontal movement horiz_frame = Labelframe(self, text="Horizontal") horiz_frame.pack(side="left", padx=10, pady=10) # Canvas for previewing delta movement. self.hcanvas = Canvas(horiz_frame, width=200, height=200) self.hcanvas.create_oval(110, 110, 90, 90, fill=self.hcol, outline=self.hcol) self.hcanvas.pack() # Label for dragging from. l = Label(horiz_frame, text="DRAG ME", relief="ridge") l.pack(ipadx=10, ipady=10, padx=20, pady=20) # Create button to reset canvas. Button(horiz_frame, text="Reset", command=lambda: self.hcanvas.delete("delta")).pack(padx=3, pady=3) # Motion input (the actual thing being tested!) m = MotionInput(l) m.bind("<Motion-X>", self.on_horiz_mov) # Vertical movement vert_frame = Labelframe(self, text="Vertical") vert_frame.pack(side="left", padx=10, pady=10) # Canvas for previewing delta movement. self.vcanvas = Canvas(vert_frame, width=200, height=200) self.vcanvas.create_oval(110, 110, 90, 90, fill=self.vcol, outline=self.vcol) self.vcanvas.pack() # Label for dragging from. l = Label(vert_frame, text="DRAG ME", relief="ridge") l.pack(ipadx=10, ipady=10, padx=20, pady=20) # Create button to reset canvas. Button(vert_frame, text="Reset", command=lambda: self.vcanvas.delete("delta")).pack(padx=3, pady=3) # Motion input (the actual thing being tested!) m = MotionInput(l) m.bind("<Motion-Y>", self.on_vert_mov) # Any movement any_frame = Labelframe(self, text="Any Direction") any_frame.pack(side="left", padx=10, pady=10) # Canvas for previewing delta movement. self.acanvas = Canvas(any_frame, width=200, height=200) self.acanvas.create_oval(110, 110, 90, 90, fill=self.acol, outline=self.acol) self.acanvas.pack() # Label for dragging from. l = Label(any_frame, text="DRAG ME", relief="ridge") l.pack(ipadx=10, ipady=10, padx=20, pady=20) # Create button to reset canvas. Button(any_frame, text="Reset", command=lambda: self.acanvas.delete("delta")).pack(padx=3, pady=3) # Motion input (the actual thing being tested!) m = MotionInput(l) m.bind("<Motion-XY>", self.on_any_mov)
def delta(self): return Loc(self._delta)
class MotionInput(object): """Add motion input event to widgets.""" def __init__(self, *args, **kw): """initialise attributes. optionally call bind_to_widget with specified args if args are not empty AVAILABLE KEYWORDS normalise (bool) Whether to use acceleration smoothing on motion. True by default """ self._isheld = False self._delta = (0, 0) self._normalised_delta_max = 5 self._use_normalisation = kw.get("normalise", True) self._bound_events = {} ##self._held_buttons = {} # bind to widget if extra args given if args: self.bind_to_widget(*args) @property def delta(self): return Loc(self._delta) def bind_to_widget(self, in_widget, button="1"): """binds relevant inputs to in_widget, optionally using the specified button (1=LMB, 2=MMB, 3=RMB)""" in_widget.bind("<ButtonPress-%s>" % button, self.inp_press, True) in_widget.bind("<ButtonRelease-%s>" % button, self.inp_release, True) in_widget.bind("<Button%s-Motion>" % button, self.inp_motion, True) # add to held buttons dict (defualt False not held) ##self._held_buttons[button] = False def bind(self, event_code=None, func=None, add=None): """Binds func to be called on event_code. Event codes is written in the format <MODIFIER-MODIFIER-IDENTIFIER>. Available MODIFIERS: Motion Available IDENTIFIERS: X Y XY""" event_code = event_code.replace("<", "").replace(">", "") keys = event_code.split("-") identifier = keys.pop() modifiers = keys # check if the event_code is valid if identifier not in ["X", "Y", "XY"]: return False # fail for m in modifiers: if m not in ["Motion", "Button1", "Button2", "Button3"]: return False # epic fail! # bind the function # create new list for event if not already bound if event_code not in self._bound_events: self._bound_events[event_code] = [func] else: # append to list if necessary if add: self._bound_events[event_code].append(func) # otherwise initialise new list else: self._bound_events[event_code] = [func] return True # success def _get_bound_events(self, identifier=None, *modifiers): """Returns list of bound functions to call for the specified event""" ret_funcs = [] modifiers = set(modifiers) # ensure modifiers are unique # check every bound event code for event_code, func_list in self._bound_events.items(): event_keys = event_code.split("-") event_id = event_keys.pop() event_mods = event_keys all_mods_work = True # check identifier id_works = (identifier is None or identifier is not None and identifier == event_id) # only check modifiers if identifier is correct if id_works: for m in modifiers: if m not in event_mods: all_mods_work = False break # add bound functions if id and modifiers are correct if id_works and all_mods_work: ret_funcs = ret_funcs + func_list # finally return found functions return ret_funcs if ret_funcs else None def _normalise_delta(self, in_delta, set_in_place=True): """Normalises in_delta to range (-1, 1). Optionally don't set in place""" # set attributes of passed in list object if necessary if set_in_place: in_delta.x = MathStat.map_range(in_delta.x, -self._normalised_delta_max, self._normalised_delta_max, -1, 1) in_delta.y = MathStat.map_range(in_delta.y, -self._normalised_delta_max, self._normalised_delta_max, -1, 1) return in_delta # otherwise initialise a new Loc object else: return Loc( MathStat.map_range(in_delta.x, -self._normalised_delta_max, self._normalised_delta_max, -1, 1), MathStat.map_range(in_delta.y, -self._normalised_delta_max, self._normalised_delta_max, -1, 1)) # def _is_held(self, button): # """returns whether the button is held""" # return button in self._held_buttons and self._held_buttons[button] def inp_press(self, event, func=None): """Bind this to a widget on a ButtonPress-X event""" self._isheld = True ##self._held_buttons[event.num] = True self._last_loc = Loc(event.x, event.y) self._orig_press_loc = self._last_loc.copy() # call function if specified with event if func: func(event) def inp_release(self, event=None, func=None): """Bind this to a widget on a ButtonRelease-X event""" self._isheld = False ##self._held_buttons[event.num] = False # call function if specified with event if func: func(event) def inp_motion(self, event, func=None): """Bind this to a widget on a ButtonX-Motion event""" def near_to_zero(val, offset=0.05): return val < offset and val > -offset if self._isheld: # get and set delta new_loc = Loc(event.x, event.y) self._delta = d = new_loc - self._last_loc if self._use_normalisation: self._normalise_delta(d) else: d *= 0.2 # set delta in event object to return in callbacks event.delta = d # ensure the last location is updated for next motion self._last_loc = new_loc # call function if specified with event and delta (in event) if func: func(event) # fire bound events for button invariant bindings if not near_to_zero(d.x): be = self._get_bound_events("X", "Motion") if be: for func in be: func(event) if not near_to_zero(d.y): be = self._get_bound_events("Y", "Motion") if be: for func in be: func(event) if not near_to_zero(d.x) or not near_to_zero(d.y): be = self._get_bound_events("XY", "Motion") if be: for func in be: func(event)
def get_canvas_dim(self): """Return dimensions of canvas in pixels as a Loc.""" return Loc(self.winfo_width(), self.winfo_height())
def get_screen_size_factor(self): """Return the viewport scale factor to ensure the same sized viewport is shown at all scales.""" return MathStat.clamp(max(MathStat.getpercent(self.get_canvas_dim(), Loc(3840, 2160), Loc(640, 480))), 0.5, 8)
def __set_location(self, value): self._location = Loc(value)