class BillboardDisplay(InstructionGroup): def __init__(self, pos, texture, size_x=1.0, size_y=1.0, intensity=1.0, Tr=1.0): super(BillboardDisplay, self).__init__() self.intensity = intensity self.Tr = Tr self.translate = Translate() self.rotate_azi = Rotate(origin=(0,0,0), axis=(0,1,0)) self.rotate_ele = Rotate(origin=(0,0,0), axis=(1,0,0)) self.scale = Scale() self.color_instruction = InstructionGroup() self.mesh = Mesh( texture=texture, vertices=rectangle_nurb.vertices, indices=rectangle_nurb.indices, fmt=rectangle_nurb.vertex_format, mode='triangles' ) self.add(PushMatrix()) self.add(self.translate) self.add(self.rotate_azi) self.add(self.rotate_ele) self.add(self.scale) self.add(self.color_instruction) self.add(self.mesh) self.add(PopMatrix()) self.set_color(intensity=intensity, Tr=Tr) self.set_size(size_x, size_y) self.set_pos(pos) self.set_texture(texture) def set_texture(self, texture): self.texture = texture self.mesh.texture = texture def set_rotate(self, angles): self.rotate_azi.angle = angles[0] * 180/np.pi self.rotate_ele.angle = angles[1] * 180/np.pi def set_size(self, size_x, size_y): self.scale.xyz = (size_x, size_y, 1) def set_pos(self, pos): self.translate.xyz = pos def set_color(self, intensity=1.0, Tr=1.0): self.color_instruction.clear() self.color_instruction.add( ChangeState( Kd=(0.6, 0.6, 0.6), Ka=(0.8, 0.8, 0.8), Ks=(0.9, 0.9, 0.9), Tr=Tr, Ns=1.0, intensity=intensity ) )
class NurbDisplay(InstructionGroup): def __init__(self, nurb, pos, size=1.0, color=(0.0, 1.0, 0.0), tr=1.0, intensity=1.0): super(NurbDisplay, self).__init__() self.color = color self.tr = tr self.intensity = intensity self.translate = Translate() self.scale = Scale() self.color_instruction = InstructionGroup() self.add(PushMatrix()) self.add(self.translate) self.add(self.scale) self.add(self.color_instruction) self.add(self.make_mesh(nurb)) self.add(PopMatrix()) self.set_color() self.set_size(size) self.set_pos(pos) def make_mesh(self, m): return Mesh(vertices=m.vertices, indices=m.indices, fmt=m.vertex_format, mode='triangles') def set_size(self, size): self.scale.xyz = (size, size, size) def set_pos(self, pos): self.translate.xyz = pos def set_color(self, color=None, tr=None, intensity=None): if tr is not None: self.tr = tr if intensity is not None: self.intensity = intensity if color is not None: self.color = color self.color_instruction.clear() self.color_instruction.add( ChangeState(Kd=self.color, Ka=self.color, Ks=(0.3, 0.3, 0.3), Tr=self.tr, Ns=1.0, intensity=self.intensity))
class NurbDisplay(InstructionGroup): def __init__(self, nurb, pos, size=1.0, color=(0.0,1.0,0.0), tr=1.0, intensity=1.0): super(NurbDisplay, self).__init__() self.color = color self.tr = tr self.intensity = intensity self.translate = Translate() self.scale = Scale() self.color_instruction = InstructionGroup() self.add(PushMatrix()) self.add(self.translate) self.add(self.scale) self.add(self.color_instruction) self.add(self.make_mesh(nurb)) self.add(PopMatrix()) self.set_color() self.set_size(size) self.set_pos(pos) def make_mesh(self, m): return Mesh( vertices=m.vertices, indices=m.indices, fmt=m.vertex_format, mode='triangles' ) def set_size(self, size): self.scale.xyz = (size, size, size) def set_pos(self, pos): self.translate.xyz = pos def set_color(self, color=None, tr=None, intensity=None): if tr is not None: self.tr = tr if intensity is not None: self.intensity = intensity if color is not None: self.color = color self.color_instruction.clear() self.color_instruction.add( ChangeState( Kd=self.color, Ka=self.color, Ks=(0.3, 0.3, 0.3), Tr=self.tr, Ns=1.0, intensity=self.intensity ) )
class BackgroundLabel(Label): def __init__(self, **kwargs): super(BackgroundLabel, self).__init__(**kwargs) self.IG = InstructionGroup() self.background_color = (0, 0, 0, 1) self.color = (1, 1, 1, 1) def Set_Background_Color(self): self.opacity = 1 self.IG.clear() self.IG.add(Color(rgba=(self.background_color))) self.IG.add(Rectangle(size=(self.size), pos=(self.pos))) self.canvas.before.add(self.IG)
class LinePlot(Widget): viewport = ListProperty(None) line_width = NumericProperty(5.) line_color = ListProperty([0,.8,1]) border_width = NumericProperty(5) border_color = ListProperty([.3,.3,.3]) flattened_points = ListProperty(None) tick_distance_x = NumericProperty(None) tick_distance_y = NumericProperty(None) tick_color = ListProperty([.3,.3,.3]) select_circle = ListProperty([0,0,0]) def __init__(self, points, **kwargs): # calculate some basic information about the data self.points = points self.points_x = zip(*points)[0] self.points_y = zip(*points)[1] # if we could figure out how to draw an arbitrary number of ticks in kv, this would be cleaner self.ticks = InstructionGroup() self.tick_translate = Translate() super(LinePlot, self).__init__(**kwargs) self.canvas.insert(0, self.ticks) self.viewport = (min(self.points_x), min(self.points_y), max(self.points_x), max(self.points_y)) # recalculate viewport when size changes def on_size(self, instance, value): self.on_viewport(None, self.viewport) def on_pos(self, instance, value): self.tick_translate.xy = self.x, self.y def on_viewport(self, instance, value): if value is None or len(value) != 4: return print value self.vp_width_convert = float(self.width)/(value[2] - value[0]) self.vp_height_convert = float(self.height)/(value[3] - value[1]) # calculate the actual display points based on the viewport and self.size self.display_points = [self.to_display_point(*p) for p in self.points if value[0] <= p[0] <= value[2] and value[1] <= p[1] <= value[3]] self.flattened_points = [item for sublist in self.display_points for item in sublist] self.draw_ticks() # it would be real nice if we could figure out how to do this in kv def draw_ticks(self): self.ticks.clear() self.ticks.add(Color(*self.tick_color, mode='rgb')) self.ticks.add(PushMatrix()) self.ticks.add(self.tick_translate) if self.tick_distance_x is not None: first_x_tick = self.tick_distance_x*(int(self.viewport[0]/self.tick_distance_x) + 1) for x in drange(first_x_tick, self.viewport[2], self.tick_distance_x): start = self.to_display_point(x, self.viewport[1]) stop = self.to_display_point(x, self.viewport[3]) self.ticks.add(Line(points=[start[0], start[1], stop[0], stop[1]])) if self.tick_distance_y is not None: first_y_tick = self.tick_distance_y*(int(self.viewport[1]/self.tick_distance_y) + 1) for y in drange(first_y_tick, self.viewport[3], self.tick_distance_y): start = self.to_display_point(self.viewport[0], y) stop = self.to_display_point(self.viewport[2], y) self.ticks.add(Line(points=[start[0], start[1], stop[0], stop[1]])) self.ticks.add(PopMatrix()) def to_display_point(self, x, y): return (self.vp_width_convert*(x-self.viewport[0]), self.vp_height_convert*(y-self.viewport[1])) def select_point(self, x, y): # get point from self.displaypoints that is closest to x distances = [abs((x - self.x) - d[0]) for d in self.display_points] idx = min(xrange(len(distances)),key=distances.__getitem__) self.select_circle = [self.display_points[idx][0], self.display_points[idx][1], 20] return self.points[idx] def to_xy(self, x, y): xt = (x - self.x)/self.vp_width_convert+self.viewport[0] yt = (y - self.y)/self.vp_height_convert+self.viewport[1] return (xt, yt)
class VisualEditor(FloatLayout): grid_lay = ObjectProperty(None) snippet_area = ObjectProperty(None) connections = ListProperty([]) def __init__(self, **kwargs): global app Clock.schedule_once(self.init_grid_lay, 1) Clock.schedule_interval(self.update, 1 / 30) super(VisualEditor, self).__init__(**kwargs) Window.bind(on_motion=self.on_motion) self.connection_starter = None self.connection_ender = None app.editor = self self.line_group = None def get_state(self): return (self.connections, self.snippet_area.children) def update(self, dt): if self.snippet_area and self.line_group is None: self.line_group = InstructionGroup() self.snippet_area.canvas.add(self.line_group) if self.line_group: if len(self.connections) > 0: self.line_group.clear() for conn in self.connections: self.line_group.add(Color(*conn.get_color())) self.line_group.add( Line(points=(conn.input.get_pos() + conn.output.get_pos()), width=1)) else: self.line_group.clear() def try_to_make_connection(self): if self.connection_starter and self.connection_ender and \ self.connection_starter.conn_type == self.connection_ender.conn_type and \ self.connection_starter.parent is not self.connection_ender.parent: for conn in self.connections: if conn.input == self.connection_starter and conn.output == self.connection_ender: break else: self.connections.append( Connection(self.connection_starter, self.connection_ender, self.connection_ender.conn_type)) self.connection_starter = None self.connection_ender = None def on_motion(self, etype, stm, touch): mult = 0 if touch.is_mouse_scrolling: if touch.button == 'scrolldown': mult = 0.1 if touch.button == 'scrollup': mult = -0.1 else: return if self.collide_point(touch.x, touch.y): for child in self.snippet_area.children: if child.collide_point(touch.x, touch.y): break else: if touch.is_mouse_scrolling: for child in self.snippet_area.children: tv = qvec.VecNd(tuple(child.pos)) - \ qvec.VecNd(tuple(touch.pos)) tv *= mult child.pos = (qvec.VecNd(child.pos) + tv).to_tuple() def on_touch_down(self, touch): if super().on_touch_down(touch): return True if touch.button == MOVE_BUTTON and self.collide_point( touch.x, touch.y): for child in self.snippet_area.children: if child.collide_point(touch.x, touch.y): break else: touch.grab(self) return True return super().on_touch_down(touch) def on_touch_move(self, touch): if super().on_touch_move(touch): return True if touch.button == MOVE_BUTTON: if touch.grab_current is self: for child in self.snippet_area.children: child.pos[0] += touch.dx child.pos[1] += touch.dy return True def on_touch_up(self, touch): if super().on_touch_up(touch): return True if touch.button == MOVE_BUTTON and self.collide_point( touch.x, touch.y): if touch.grab_current is self: touch.ungrab(self) return True def button_factory(self, snippet): def f(*args): snp = snippet() snp.editor = self self.snippet_area.add_widget(snp) snp.pos = (self.snippet_area.height / 2, self.snippet_area.width / 2) b = Button(valign='middle', text_size=(100, 40), size_hint_y=None, height=40, text=l18n.get(snippet.get_snippet_name())) b.bind(on_press=f) return b def init_grid_lay(self, *args): global snippets for snippet in snippets: self.add_child_to_grid_lay(self.button_factory(snippet)) def add_child_to_grid_lay(self, child): if self.grid_lay: self.grid_lay.add_widget(child) target_height = 0 for child in self.grid_lay.children: target_height += child.height self.grid_lay.height = target_height def update_connections(self): for conn in self.connections[:]: if not (conn.input.parent.valid and conn.output.parent.valid): self.connections.remove(conn) def remove_snippet(self, snippet): snippet.invalidate() self.snippet_area.remove_widget(snippet) self.update_connections() def generate(self): generate(self.get_state())
class LinePlot(Widget): viewport = ListProperty(None) line_width = NumericProperty(5.) line_color = ListProperty([0, .8, 1]) border_width = NumericProperty(5) border_color = ListProperty([.3, .3, .3]) flattened_points = ListProperty(None) tick_distance_x = NumericProperty(None) tick_distance_y = NumericProperty(None) tick_color = ListProperty([.3, .3, .3]) select_circle = ListProperty([0, 0, 0]) def __init__(self, points, **kwargs): # calculate some basic information about the data self.points = points self.points_x = zip(*points)[0] self.points_y = zip(*points)[1] # if we could figure out how to draw an arbitrary number of ticks in kv, this would be cleaner self.ticks = InstructionGroup() self.tick_translate = Translate() super(LinePlot, self).__init__(**kwargs) self.canvas.insert(0, self.ticks) self.viewport = (min(self.points_x), min(self.points_y), max(self.points_x), max(self.points_y)) # recalculate viewport when size changes def on_size(self, instance, value): self.on_viewport(None, self.viewport) def on_pos(self, instance, value): self.tick_translate.xy = self.x, self.y def on_viewport(self, instance, value): if value is None or len(value) != 4: return print value self.vp_width_convert = float(self.width) / (value[2] - value[0]) self.vp_height_convert = float(self.height) / (value[3] - value[1]) # calculate the actual display points based on the viewport and self.size self.display_points = [ self.to_display_point(*p) for p in self.points if value[0] <= p[0] <= value[2] and value[1] <= p[1] <= value[3] ] self.flattened_points = [ item for sublist in self.display_points for item in sublist ] self.draw_ticks() # it would be real nice if we could figure out how to do this in kv def draw_ticks(self): self.ticks.clear() self.ticks.add(Color(*self.tick_color, mode='rgb')) self.ticks.add(PushMatrix()) self.ticks.add(self.tick_translate) if self.tick_distance_x is not None: first_x_tick = self.tick_distance_x * ( int(self.viewport[0] / self.tick_distance_x) + 1) for x in drange(first_x_tick, self.viewport[2], self.tick_distance_x): start = self.to_display_point(x, self.viewport[1]) stop = self.to_display_point(x, self.viewport[3]) self.ticks.add( Line(points=[start[0], start[1], stop[0], stop[1]])) if self.tick_distance_y is not None: first_y_tick = self.tick_distance_y * ( int(self.viewport[1] / self.tick_distance_y) + 1) for y in drange(first_y_tick, self.viewport[3], self.tick_distance_y): start = self.to_display_point(self.viewport[0], y) stop = self.to_display_point(self.viewport[2], y) self.ticks.add( Line(points=[start[0], start[1], stop[0], stop[1]])) self.ticks.add(PopMatrix()) def to_display_point(self, x, y): return (self.vp_width_convert * (x - self.viewport[0]), self.vp_height_convert * (y - self.viewport[1])) def select_point(self, x, y): # get point from self.displaypoints that is closest to x distances = [abs((x - self.x) - d[0]) for d in self.display_points] idx = min(xrange(len(distances)), key=distances.__getitem__) self.select_circle = [ self.display_points[idx][0], self.display_points[idx][1], 20 ] return self.points[idx] def to_xy(self, x, y): xt = (x - self.x) / self.vp_width_convert + self.viewport[0] yt = (y - self.y) / self.vp_height_convert + self.viewport[1] return (xt, yt)
class TextureStack(Widget): """Several textures superimposed on one another, and possibly offset by some amount. In 2D games where characters can wear different clothes or hold different equipment, their graphics are often composed of several graphics layered on one another. This widget simplifies the management of such compositions. """ texs = ListProperty() """Texture objects""" offxs = ListProperty() """x-offsets. The texture at the same index will be moved to the right by the number of pixels in this list. """ offys = ListProperty() """y-offsets. The texture at the same index will be moved upward by the number of pixels in this list. """ group = ObjectProperty() """My ``InstructionGroup``, suitable for addition to whatever ``canvas``.""" def _get_offsets(self): return zip(self.offxs, self.offys) def _set_offsets(self, offs): offxs = [] offys = [] for x, y in offs: offxs.append(x) offys.append(y) self.offxs, self.offys = offxs, offys offsets = AliasProperty( _get_offsets, _set_offsets, bind=('offxs', 'offys') ) """List of (x, y) tuples by which to offset the corresponding texture.""" _texture_rectangles = DictProperty({}) """Private. Rectangle instructions for each of the textures, keyed by the texture. """ def __init__(self, **kwargs): """Make triggers and bind.""" kwargs['size_hint'] = (None, None) self.translate = Translate(0, 0) self.group = InstructionGroup() super().__init__(**kwargs) self.bind(offxs=self.on_pos, offys=self.on_pos) def on_texs(self, *args): """Make rectangles for each of the textures and add them to the canvas.""" if not self.canvas or not self.texs: Clock.schedule_once(self.on_texs, 0) return texlen = len(self.texs) # Ensure each property is the same length as my texs, padding # with 0 as needed for prop in ('offxs', 'offys'): proplen = len(getattr(self, prop)) if proplen > texlen: setattr(self, prop, getattr(self, prop)[:proplen-texlen]) if texlen > proplen: propval = list(getattr(self, prop)) propval += [0] * (texlen - proplen) setattr(self, prop, propval) self.group.clear() self._texture_rectangles = {} w = h = 0 (x, y) = self.pos self.translate.x = x self.translate.y = y self.group.add(PushMatrix()) self.group.add(self.translate) for tex, offx, offy in zip(self.texs, self.offxs, self.offys): rect = Rectangle( pos=(offx, offy), size=tex.size, texture=tex ) self._texture_rectangles[tex] = rect self.group.add(rect) tw = tex.width + offx th = tex.height + offy if tw > w: w = tw if th > h: h = th self.size = (w, h) self.group.add(PopMatrix()) self.canvas.add(self.group) def on_pos(self, *args): """Translate all the rectangles within this widget to reflect the widget's position. """ (x, y) = self.pos self.translate.x = x self.translate.y = y def clear(self): """Clear my rectangles and ``texs``.""" self.group.clear() self._texture_rectangles = {} self.texs = [] self.size = [1, 1] def insert(self, i, tex): """Insert the texture into my ``texs``, waiting for the creation of the canvas if necessary. """ if not self.canvas: Clock.schedule_once( lambda dt: self.insert(i, tex), 0) return self.texs.insert(i, tex) def append(self, tex): """``self.insert(len(self.texs), tex)``""" self.insert(len(self.texs), tex) def __delitem__(self, i): """Remove a texture and its rectangle""" tex = self.texs[i] try: rect = self._texture_rectangles[tex] self.canvas.remove(rect) del self._texture_rectangles[tex] except KeyError: pass del self.texs[i] def __setitem__(self, i, v): """First delete at ``i``, then insert there""" if len(self.texs) > 0: self._no_upd_texs = True self.__delitem__(i) self._no_upd_texs = False self.insert(i, v) def pop(self, i=-1): """Delete the texture at ``i``, and return it.""" return self.texs.pop(i)
class GcodeViewerScreen(Screen): current_z = NumericProperty(0) select_mode = BooleanProperty(False) twod_mode = BooleanProperty(False) laser_mode = BooleanProperty(False) valid = BooleanProperty(False) def __init__(self, comms=None, **kwargs): super(GcodeViewerScreen, self).__init__(**kwargs) self.app = App.get_running_app() self.last_file_pos = None self.canv = InstructionGroup() self.bind(pos=self._redraw, size=self._redraw) self.last_target_layer = 0 self.tx = 0 self.ty = 0 self.scale = 1.0 self.comms = comms self.twod_mode = self.app.is_cnc self.rval = 0.0 def loading(self, ll=1): self.valid = False self.li = Image(source='img/image-loading.gif') self.add_widget(self.li) self.ids.surface.canvas.remove(self.canv) threading.Thread(target=self._load_file, args=(ll, )).start() @mainthread def _loaded(self): Logger.debug("GcodeViewerScreen: in _loaded. ok: {}".format( self._loaded_ok)) self.remove_widget(self.li) self.li = None self.ids.surface.canvas.add(self.canv) self.valid = self._loaded_ok if self._loaded_ok: # not sure why we need to do this self.ids.surface.top = Window.height if self.app.is_connected: self.app.bind(wpos=self.update_tool) def _load_file(self, ll): self._loaded_ok = False try: self.parse_gcode_file(self.app.gcode_file, ll, True) except Exception: print(traceback.format_exc()) mb = MessageBox( text='File not found: {}'.format(self.app.gcode_file)) mb.open() self._loaded() def _redraw(self, instance, value): self.ids.surface.canvas.remove(self.canv) self.ids.surface.canvas.add(self.canv) def clear(self): self.app.unbind(wpos=self.update_tool) if self.li: self.remove_widget(self.li) self.li = None if self.select_mode: self.stop_cursor(0, 0) self.select_mode = False self.ids.select_mode_but.state = 'normal' self.valid = False self.is_visible = False self.canv.clear() self.ids.surface.canvas.remove(self.canv) self.last_target_layer = 0 # reset scale and translation m = Matrix() m.identity() self.ids.surface.transform = m # not sure why we need to do this self.ids.surface.top = Window.height def next_layer(self): self.loading(self.last_target_layer + 1) def prev_layer(self): n = 1 if self.last_target_layer <= 1 else self.last_target_layer - 1 self.loading(n) def print(self): self.app.main_window._start_print() # ---------------------------------------------------------------------- # Return center x,y,z,r for arc motions 2,3 and set self.rval # Cribbed from bCNC # ---------------------------------------------------------------------- def motionCenter(self, gcode, plane, xyz_cur, xyz_val, ival, jval, kval=0.0): if self.rval > 0.0: if plane == XY: x = xyz_cur[0] y = xyz_cur[1] xv = xyz_val[0] yv = xyz_val[1] elif plane == XZ: x = xyz_cur[0] y = xyz_cur[2] xv = xyz_val[0] yv = xyz_val[2] else: x = xyz_cur[1] y = xyz_cur[2] xv = xyz_val[1] yv = xyz_val[2] ABx = xv - x ABy = yv - y Cx = 0.5 * (x + xv) Cy = 0.5 * (y + yv) AB = math.sqrt(ABx**2 + ABy**2) try: OC = math.sqrt(self.rval**2 - AB**2 / 4.0) except Exception: OC = 0.0 if gcode == 2: OC = -OC # CW if AB != 0.0: return Cx - OC * ABy / AB, Cy + OC * ABx / AB else: # Error!!! return x, y else: # Center xc = xyz_cur[0] + ival yc = xyz_cur[1] + jval zc = xyz_cur[2] + kval self.rval = math.sqrt(ival**2 + jval**2 + kval**2) if plane == XY: return xc, yc elif plane == XZ: return xc, zc else: return yc, zc extract_gcode = re.compile(r"(G|X|Y|Z|I|J|K|E|S)(-?\d*\.?\d*\.?)") def parse_gcode_file(self, fn, target_layer=0, one_layer=False): # open file parse gcode and draw Logger.debug("GcodeViewerScreen: parsing file {}".format(fn)) lastpos = [self.app.wpos[0], self.app.wpos[1], -1] # XYZ, set to initial tool position lastz = None lastdeltaz = None laste = 0 lasts = 1 layer = -1 last_gcode = -1 points = [] max_x = float('nan') max_y = float('nan') min_x = float('nan') min_y = float('nan') has_e = False plane = XY rel_move = False self.is_visible = True if self.laser_mode: self.twod_mode = True # laser mode implies 2D mode self.last_target_layer = target_layer # reset scale and translation m = Matrix() m.identity() self.ids.surface.transform = m # remove all instructions from canvas self.canv.clear() self.canv.add(PushMatrix()) modal_g = 0 cnt = 0 found_layer = False x = lastpos[0] y = lastpos[1] z = lastpos[2] with open(fn) as f: # if self.last_file_pos: # # jump to last read position # f.seek(self.last_file_pos) # self.last_file_pos= None # print('Jumped to Saved position: {}'.format(self.last_file_pos)) for ln in f: cnt += 1 ln = ln.strip() if not ln: continue if ln.startswith(';'): continue if ln.startswith('('): continue p = ln.find(';') if p >= 0: ln = ln[:p] matches = self.extract_gcode.findall(ln) # this handles multiple G codes on one line gcodes = [] d = {} for m in matches: #print(m) if m[0] == 'G' and 'G' in d: # we have another G code on the same line gcodes.append(d) d = {} d[m[0]] = float(m[1]) gcodes.append(d) for d in gcodes: if not d: continue Logger.debug("GcodeViewerScreen: d={}".format(d)) # handle modal commands if 'G' not in d and ('X' in d or 'Y' in d or 'Z' in d or 'S' in d): d['G'] = modal_g gcode = int(d['G']) # G92 E0 resets E if 'G' in d and gcode == 92 and 'E' in d: laste = float(d['E']) has_e = True if 'G' in d and (gcode == 91 or gcode == 90): rel_move = gcode == 91 # only deal with G0/1/2/3 if gcode > 3: continue modal_g = gcode # see if it is 3d printing (ie has an E axis on a G1) if not has_e and ('E' in d and 'G' in d and gcode == 1): has_e = True if rel_move: x += 0 if 'X' not in d else float(d['X']) y += 0 if 'Y' not in d else float(d['Y']) z += 0 if 'Z' not in d else float(d['Z']) else: x = lastpos[0] if 'X' not in d else float(d['X']) y = lastpos[1] if 'Y' not in d else float(d['Y']) z = lastpos[2] if 'Z' not in d else float(d['Z']) i = 0.0 if 'I' not in d else float(d['I']) j = 0.0 if 'J' not in d else float(d['J']) self.rval = 0.0 if 'R' not in d else float(d['R']) e = laste if 'E' not in d else float(d['E']) s = lasts if 'S' not in d else float(d['S']) if not self.twod_mode: # handle layers (when Z changes) if z == -1: # no z seen yet layer = -1 continue if lastz is None: # first layer lastz = z layer = 1 if z != lastz: # count layers layer += 1 lastz = z # wait until we get to the requested layer if layer != target_layer: lastpos[2] = z continue if layer > target_layer and one_layer: # FIXME for some reason this does not work, -- not counting layers #self.last_file_pos= f.tell() #print('Saved position: {}'.format(self.last_file_pos)) break self.current_z = z found_layer = True Logger.debug( "GcodeViewerScreen: x= {}, y= {}, z= {}, s= {}".format( x, y, z, s)) # find bounding box if math.isnan(min_x) or x < min_x: min_x = x if math.isnan(min_y) or y < min_y: min_y = y if math.isnan(max_x) or x > max_x: max_x = x if math.isnan(max_y) or y > max_y: max_y = y # accumulating vertices is more efficient but we need to flush them at some point # Here we flush them if we encounter a new G code like G3 following G1 if last_gcode != gcode: # flush vertices if points: self.canv.add(Color(0, 0, 0)) self.canv.add( Line(points=points, width=1, cap='none', joint='none')) points = [] last_gcode = gcode # in slicer generated files there is no G0 so we need a way to know when to draw, so if there is an E then draw else don't if gcode == 0: #print("move to: {}, {}, {}".format(x, y, z)) # draw moves in dashed red self.canv.add(Color(1, 0, 0)) self.canv.add( Line(points=[lastpos[0], lastpos[1], x, y], width=1, dash_offset=1, cap='none', joint='none')) elif gcode == 1: if ('X' in d or 'Y' in d): if self.laser_mode and s <= 0.01: # do not draw non cutting lines if points: # draw accumulated points upto this point self.canv.add(Color(0, 0, 0)) self.canv.add( Line(points=points, width=1, cap='none', joint='none')) points = [] # for 3d printers (has_e) only draw if there is an E elif not has_e or 'E' in d: # if a CNC gcode file or there is an E in the G1 (3d printing) #print("draw to: {}, {}, {}".format(x, y, z)) # collect points but don't draw them yet if len(points) < 2: points.append(lastpos[0]) points.append(lastpos[1]) points.append(x) points.append(y) else: # a G1 with no E, treat as G0 and draw moves in red #print("move to: {}, {}, {}".format(x, y, z)) if points: # draw accumulated points upto this point self.canv.add(Color(0, 0, 0)) self.canv.add( Line(points=points, width=1, cap='none', joint='none')) points = [] # now draw the move in red self.canv.add(Color(1, 0, 0)) self.canv.add( Line(points=[lastpos[0], lastpos[1], x, y], width=1, cap='none', joint='none')) else: # A G1 with no X or Y, maybe E only move (retract) or Z move (layer change) if points: # draw accumulated points upto this point self.canv.add(Color(0, 0, 0)) self.canv.add( Line(points=points, width=1, cap='none', joint='none')) points = [] elif gcode in [2, 3]: # CW=2,CCW=3 circle # code cribbed from bCNC xyz = [] xyz.append((lastpos[0], lastpos[1], lastpos[2])) uc, vc = self.motionCenter(gcode, plane, lastpos, [x, y, z], i, j) if plane == XY: u0 = lastpos[0] v0 = lastpos[1] w0 = lastpos[2] u1 = x v1 = y w1 = z elif plane == XZ: u0 = lastpos[0] v0 = lastpos[2] w0 = lastpos[1] u1 = x v1 = z w1 = y gcode = 5 - gcode # flip 2-3 when XZ plane is used else: u0 = lastpos[1] v0 = lastpos[2] w0 = lastpos[0] u1 = y v1 = z w1 = x phi0 = math.atan2(v0 - vc, u0 - uc) phi1 = math.atan2(v1 - vc, u1 - uc) try: sagitta = 1.0 - CNC_accuracy / self.rval except ZeroDivisionError: sagitta = 0.0 if sagitta > 0.0: df = 2.0 * math.acos(sagitta) df = min(df, math.pi / 4.0) else: df = math.pi / 4.0 if gcode == 2: if phi1 >= phi0 - 1e-10: phi1 -= 2.0 * math.pi ws = (w1 - w0) / (phi1 - phi0) phi = phi0 - df while phi > phi1: u = uc + self.rval * math.cos(phi) v = vc + self.rval * math.sin(phi) w = w0 + (phi - phi0) * ws phi -= df if plane == XY: xyz.append((u, v, w)) elif plane == XZ: xyz.append((u, w, v)) else: xyz.append((w, u, v)) else: if phi1 <= phi0 + 1e-10: phi1 += 2.0 * math.pi ws = (w1 - w0) / (phi1 - phi0) phi = phi0 + df while phi < phi1: u = uc + self.rval * math.cos(phi) v = vc + self.rval * math.sin(phi) w = w0 + (phi - phi0) * ws phi += df if plane == XY: xyz.append((u, v, w)) elif plane == XZ: xyz.append((u, w, v)) else: xyz.append((w, u, v)) xyz.append((x, y, z)) # plot the points points = [] for t in xyz: x1, y1, z1 = t points.append(x1) points.append(y1) max_x = max(x1, max_x) min_x = min(x1, min_x) max_y = max(y1, max_y) min_y = min(y1, min_y) self.canv.add(Color(0, 0, 0)) self.canv.add( Line(points=points, width=1, cap='none', joint='none')) points = [] # always remember last position lastpos = [x, y, z] laste = e lasts = s if not found_layer: # we hit the end of file before finding the layer we want Logger.info( "GcodeViewerScreen: last layer was at {}".format(lastz)) self.last_target_layer -= 1 return # flush any points not yet drawn if points: # draw accumulated points upto this point self.canv.add(Color(0, 0, 0)) self.canv.add( Line(points=points, width=1, cap='none', joint='none')) points = [] # center the drawing and scale it dx = max_x - min_x dy = max_y - min_y if dx == 0 or dy == 0: Logger.warning( "GcodeViewerScreen: size is bad, maybe need 2D mode") return dx += 4 dy += 4 Logger.debug("GcodeViewerScreen: dx= {}, dy= {}".format(dx, dy)) # add in the translation to center object self.tx = -min_x - dx / 2 self.ty = -min_y - dy / 2 self.canv.insert(1, Translate(self.tx, self.ty)) Logger.debug("GcodeViewerScreen: tx= {}, ty= {}".format( self.tx, self.ty)) # scale the drawing to fit the screen if abs(dx) > abs(dy): scale = self.ids.surface.width / abs(dx) if abs(dy) * scale > self.ids.surface.height: scale *= self.ids.surface.height / (abs(dy) * scale) else: scale = self.ids.surface.height / abs(dy) if abs(dx) * scale > self.ids.surface.width: scale *= self.ids.surface.width / (abs(dx) * scale) Logger.debug("GcodeViewerScreen: scale= {}".format(scale)) self.scale = scale self.canv.insert(1, Scale(scale)) # translate to center of canvas self.offs = self.ids.surface.center self.canv.insert( 1, Translate(self.ids.surface.center[0], self.ids.surface.center[1])) Logger.debug("GcodeViewerScreen: cx= {}, cy= {}".format( self.ids.surface.center[0], self.ids.surface.center[1])) Logger.debug("GcodeViewerScreen: sx= {}, sy= {}".format( self.ids.surface.size[0], self.ids.surface.size[1])) # axis Markers self.canv.add(Color(0, 1, 0, mode='rgb')) self.canv.add( Line(points=[0, -10, 0, self.ids.surface.height / scale], width=1, cap='none', joint='none')) self.canv.add( Line(points=[-10, 0, self.ids.surface.width / scale, 0], width=1, cap='none', joint='none')) # tool position marker if self.app.is_connected: x = self.app.wpos[0] y = self.app.wpos[1] r = (10.0 / self.ids.surface.scale) / scale self.canv.add(Color(1, 0, 0, mode='rgb', group="tool")) self.canv.add(Line(circle=(x, y, r), group="tool")) # self.canv.add(Rectangle(pos=(x, y-r/2), size=(1/scale, r), group="tool")) # self.canv.add(Rectangle(pos=(x-r/2, y), size=(r, 1/scale), group="tool")) self.canv.add(PopMatrix()) self._loaded_ok = True Logger.debug("GcodeViewerScreen: done loading") def update_tool(self, i, v): if not self.is_visible or not self.app.is_connected: return # follow the tool path #self.canv.remove_group("tool") x = v[0] y = v[1] r = (10.0 / self.ids.surface.scale) / self.scale g = self.canv.get_group("tool") if g: g[2].circle = (x, y, r) # g[4].pos= x, y-r/2 # g[6].pos= x-r/2, y def transform_to_wpos(self, posx, posy): ''' convert touch coords to local scatter widget coords, relative to lower bottom corner ''' pos = self.ids.surface.to_widget(posx, posy) # convert to original model coordinates (mm), need to take into account scale and translate wpos = ((pos[0] - self.offs[0]) / self.scale - self.tx, (pos[1] - self.offs[1]) / self.scale - self.ty) return wpos def transform_to_spos(self, posx, posy): ''' inverse transform of model coordinates to scatter coordinates ''' pos = ((((posx + self.tx) * self.scale) + self.offs[0]), (((posy + self.ty) * self.scale) + self.offs[1])) spos = self.ids.surface.to_window(*pos) #print("pos= {}, spos= {}".format(pos, spos)) return spos def moved(self, w, touch): # we scaled or moved the scatter so need to reposition cursor # TODO it would be nice if the cursor stayed where it was relative to the model during a move or scale # NOTE right now we can't move or scale while cursor is on # if self.select_mode: # x, y= (self.crossx[0].pos[0], self.crossx[1].pos[1]) # self.stop_cursor(x, y) # self.start_cursor(x, y) # hide tool marker self.canv.remove_group('tool') def start_cursor(self, x, y): tx, ty = self.transform_to_wpos(x, y) label = CoreLabel(text="{:1.2f},{:1.2f}".format(tx, ty)) label.refresh() texture = label.texture px, py = (x, y) with self.ids.surface.canvas.after: Color(0, 0, 1, mode='rgb', group='cursor_group') self.crossx = [ Rectangle(pos=(px, 0), size=(1, self.height), group='cursor_group'), Rectangle(pos=(0, py), size=(self.width, 1), group='cursor_group'), Line(circle=(px, py, 20), group='cursor_group'), Rectangle(texture=texture, pos=(px - texture.size[0] / 2, py - 40), size=texture.size, group='cursor_group') ] def move_cursor_by(self, dx, dy): x, y = (self.crossx[0].pos[0] + dx, self.crossx[1].pos[1] + dy) self.crossx[0].pos = x, 0 self.crossx[1].pos = 0, y self.crossx[2].circle = (x, y, 20) tx, ty = self.transform_to_wpos(x, y) label = CoreLabel(text="{:1.2f},{:1.2f}".format(tx, ty)) label.refresh() texture = label.texture self.crossx[3].texture = texture self.crossx[3].pos = x - texture.size[0] / 2, y - 40 def stop_cursor(self, x=0, y=0): self.ids.surface.canvas.after.remove_group('cursor_group') self.crossx = None def on_touch_down(self, touch): #print(self.ids.surface.bbox) if self.ids.view_window.collide_point(touch.x, touch.y): # if within the scatter window if self.select_mode: touch.grab(self) return True elif touch.is_mouse_scrolling: # Allow mouse scroll wheel to zoom in/out if touch.button == 'scrolldown': # zoom in if self.ids.surface.scale < 100: rescale = 1.1 self.ids.surface.apply_transform( Matrix().scale(rescale, rescale, rescale), post_multiply=True, anchor=self.ids.surface.to_widget(*touch.pos)) elif touch.button == 'scrollup': # zoom out if self.ids.surface.scale > 0.01: rescale = 0.8 self.ids.surface.apply_transform( Matrix().scale(rescale, rescale, rescale), post_multiply=True, anchor=self.ids.surface.to_widget(*touch.pos)) self.moved(None, touch) return True return super(GcodeViewerScreen, self).on_touch_down(touch) def on_touch_move(self, touch): if self.select_mode: if touch.grab_current is not self: return False dx = touch.dpos[0] dy = touch.dpos[1] self.move_cursor_by(dx, dy) return True else: return super(GcodeViewerScreen, self).on_touch_move(touch) def on_touch_up(self, touch): if touch.grab_current is self: touch.ungrab(self) return True return super(GcodeViewerScreen, self).on_touch_up(touch) def select(self, on): if not on and self.select_mode: self.stop_cursor() self.select_mode = False elif on and not self.select_mode: x, y = self.center self.start_cursor(x, y) self.select_mode = True def move_gantry(self): if not self.select_mode: return self.select_mode = False self.ids.select_mode_but.state = 'normal' # convert to original model coordinates (mm), need to take into account scale and translate x, y = (self.crossx[0].pos[0], self.crossx[1].pos[1]) self.stop_cursor(x, y) wpos = self.transform_to_wpos(x, y) if self.comms: self.comms.write('G0 X{:1.2f} Y{:1.2f}\n'.format(wpos[0], wpos[1])) else: print('Move Gantry to: {:1.2f}, {:1.2f}'.format(wpos[0], wpos[1])) print('G0 X{:1.2f} Y{:1.2f}'.format(wpos[0], wpos[1])) def set_wcs(self): if not self.select_mode: return self.select_mode = False self.ids.select_mode_but.state = 'normal' # convert to original model coordinates (mm), need to take into account scale and translate x, y = (self.crossx[0].pos[0], self.crossx[1].pos[1]) self.stop_cursor(x, y) wpos = self.transform_to_wpos(x, y) if self.comms: self.comms.write('G10 L20 P0 X{:1.2f} Y{:1.2f}\n'.format( wpos[0], wpos[1])) else: print('Set WCS to: {:1.2f}, {:1.2f}'.format(wpos[0], wpos[1])) print('G10 L20 P0 X{:1.2f} Y{:1.2f}'.format(wpos[0], wpos[1])) def set_type(self, t): if t == '3D': self.twod_mode = False self.laser_mode = False elif t == '2D': self.twod_mode = True self.laser_mode = False elif t == 'Laser': self.twod_mode = True self.laser_mode = True self.loading(0 if self.twod_mode else 1)
class ScatterBase(ScatterLayout): name = StringProperty() _hold_triggered = BooleanProperty(False) selected = BooleanProperty(False) scale_min = NumericProperty(.2) do_rotation = BooleanProperty(False) do_scale = BooleanProperty(False) do_translation = BooleanProperty(False) parent_width = NumericProperty() parent_height = NumericProperty() # Check if the touch collides with any of the children widgets def collide_point(self, x, y): local_x,local_y = self.to_local(x, y) for child in self.content.walk(restrict=True): if child==self.content: continue if child.collide_point(local_x, local_y): return True return False def get_full_bbox_parent(self, *args): all_corners = [] # Can also iterate through self.content.walk(restrict=True), but takes longer for child in self.content.children: if child == self.content: continue all_corners += [child.pos, (child.x, child.top), (child.right, child.y), (child.right, child.top)] all_corners_parent = [self.to_parent(*point) for point in all_corners] xmin = min([point[0] for point in all_corners_parent]) ymin = min([point[1] for point in all_corners_parent]) xmax = max([point[0] for point in all_corners_parent]) ymax = max([point[1] for point in all_corners_parent]) return (xmin, ymin), (xmax-xmin, ymax-ymin) full_bbox_parent = AliasProperty(get_full_bbox_parent, None) def on_touch_down(self, touch): if self.collide_point(*touch.pos): self._hold_triggered = False # If this is the "selected widget", treat as a movable scatter if self.selected: return super(ScatterBase, self).on_touch_down(touch) # Only detect on_hold if there is no currently selected widget if not self.selected: Clock.schedule_once(self.on_hold, .6) def on_touch_up(self,touch): # Unschedule on_hold for short presses if not self._hold_triggered: Clock.unschedule(self.on_hold) if self.selected: super(ScatterBase, self).on_touch_up(touch) def on_hold(self, *args): app = App.get_running_app() # 1. If already selected, do nothing if self.selected: return # 2. If a selected widget already exists, do nothing if not app.root.manager.layout_screen.selected_widget: self._hold_triggered = True app.root.manager.layout_screen.edit_widget(self) def on_transform_with_touch(self,*args): self.check_widget() # When widget is changed, check to make sure it is still in bounds of mirror def check_widget(self, *args): (bbox_x,bbox_y),(bbox_width,bbox_height) = self.full_bbox_parent # 1. Size check if bbox_width > self.parent_width: widget_to_bbox_ratio = bbox_width/self.parent_width self.scale = self.scale/widget_to_bbox_ratio if bbox_height > self.parent_height: widget_to_bbox_ratio = bbox_height/self.parent_height self.scale = self.scale/widget_to_bbox_ratio # 2. Translation check - Make sure widget is within mirror bbox_right = bbox_x+bbox_width bbox_top = bbox_y+bbox_height if bbox_x < 0: self.x -= bbox_x if bbox_right > self.parent_width: self.x += self.parent_width - bbox_right if bbox_y < 0: self.y -= bbox_y if bbox_top > self.parent_height: self.y += self.parent_height - bbox_top def select(self, *args): # 0. Set as selected self.selected = True # 1. Locate outermost bounding box of widget all_corners = [] for widget in self.content.walk(restrict=True): if widget == self.content: continue all_corners += [widget.pos, (widget.x, widget.top), (widget.right, widget.y), (widget.right, widget.top)] xmin = min([point[0] for point in all_corners]) ymin = min([point[1] for point in all_corners]) xmax = max([point[0] for point in all_corners]) ymax = max([point[1] for point in all_corners]) # 2. Draw outline around widget self.outline = InstructionGroup() self.outline.clear() self.outline.add(Color(.67, .816, .95, 1)) self.outline.add(Line(points=[xmin, ymin, xmax, ymin, xmax, ymax, xmin, ymax], width=2, joint='none', close=True)) self.canvas.add(self.outline) # 3. Bring to front, make movable self.do_translation = True self.do_scale = True if self.rotation != 0: self.do_rotation = True self.auto_bring_to_front = True def unselect(self, *args): # 0. Deselect self.selected = False # 1. Remove outline self.canvas.remove(self.outline) # 2. Lock widget self.do_translation = False self.do_scale = False self.do_rotation = False self.auto_bring_to_front = False
class PrinterAnimation(RelativeLayout): padding = NumericProperty(40) printer_actual_dimensions = ListProperty([10, 10, 80]) printer_current_actual_height = NumericProperty(0.0) print_area_height = NumericProperty(1) print_area_width = NumericProperty(1) print_area_left = NumericProperty(0) print_area_bottom = NumericProperty(40) container_padding = NumericProperty(0) container_left = NumericProperty(0) container_width = NumericProperty(0) container_bottom = NumericProperty(0) container_height = NumericProperty(0) laser_size = ListProperty([40, 40]) resin_height = NumericProperty(20) water_height = NumericProperty(20) scale = NumericProperty(1.0) resin_color = ListProperty([0.0, 0.8, 0.0, 0.6]) water_color = ListProperty([0.2, 0.2, 1.0, 0.6]) container_color = ListProperty([1.0, 1.0, 1.0, 1.0]) laser_color_edge2 = ListProperty([0.0, 0.0, 0.5, 1.0]) laser_color_edge = ListProperty([0.0, 0.0, 1.0, 1.0]) laser_color = ListProperty([0.7, 1.0, 1.0, 1.0]) drip_history = ListProperty() laser_points = ListProperty() middle_x = NumericProperty(52) laser_pos = NumericProperty(60) laser_speed = NumericProperty(1) refresh_rate = NumericProperty(1.0) def __init__(self, **kwargs): super(PrinterAnimation, self).__init__(**kwargs) self.drip_time_range = 5 self.waiting_for_drips = True self.refresh_rate = App.get_running_app().refresh_rate self._gl_setup() self.axis_history = [] self.drips = 0 self.line_x = [] self.line_y = [] self.last_height = 0.0 self.min_height = 0.0 self.last_x_min = 0.0 self.last_x_max = 1.0 self.is_on_canvas = False def on_printer_actual_dimensions(self, instance, value): self.min_height = self.printer_actual_dimensions[2] / 400.0 self.on_size(None) def _gl_setup(self): self.drip_texture = CoreImage("resources/images/drop.png", mipmap=True).texture self.drips_instruction = InstructionGroup() self.model_instruction = InstructionGroup() print(self.canvas.children) # self.canvas.add(self.drips_instruction) # self.canvas.add(self.model_instruction) def on_size(self, *largs): bounds_y = (self.height * 0.7) - self.resin_height bounds_x = self.width - (self.padding * 2) printer_x = self.printer_actual_dimensions[0] printer_y = self.printer_actual_dimensions[2] self.laser_pos = self.width / 2 self.scale = min(bounds_y / printer_y, bounds_x / printer_x) Logger.info("Scale: {}".format(self.scale)) self.print_area_width = printer_x * self.scale self.print_area_height = printer_y * self.scale def redraw(self, key): self._draw_drips() self._draw_laser() self._draw_model() if not self.is_on_canvas: self.canvas.insert(4, self.drips_instruction) self.canvas.insert(4, self.model_instruction) self.is_on_canvas = True Clock.unschedule(self.redraw) Clock.schedule_once(self.redraw, self.refresh_rate) def animation_start(self, *args): Clock.unschedule(self.redraw) self.axis_history = [] self.line_x = [] self.line_y = [] self.last_height = 0 self.min_height = 0.0 self.laser_points = [] self.drip_history = [] Clock.schedule_once(self.redraw, self.refresh_rate) def animation_stop(self): Clock.unschedule(self.redraw) self.axis_history = [] self.line_x = [] self.line_y = [] self.last_height = 0 self.min_height = 0.0 self.laser_points = [] self.drip_history = [] def _draw_drips(self): self.drips_instruction.clear() self.drips_instruction.add(Color(1, 1, 1, 1)) top = time.time() bottom = top - self.drip_time_range drips_in_history = len(self.drip_history) for (index, drip_time) in zip(range(drips_in_history, 0, -1), self.drip_history): if drip_time > bottom: time_ago = top - drip_time y_pos_percent = (self.drip_time_range - time_ago) / self.drip_time_range drip_pos_y = (self.height * y_pos_percent) + self.padding xoff = 10 + math.sin((len(self.drip_history) - index) / (2 * math.pi)) * 20 self.drips_instruction.add(Rectangle(size=[12, 16], pos=[self.print_area_left + xoff, drip_pos_y], texture=self.drip_texture)) def _draw_laser(self): if self.waiting_for_drips: self.laser_points = [] else: x_min = self.print_area_left + (self.last_x_min * self.print_area_width) x_max = self.print_area_left + (self.last_x_max * self.print_area_width) if (self.laser_pos >= x_max): self.laser_pos = x_max self.laser_speed = abs(self.laser_speed) * -1 if (self.laser_pos <= x_min): self.laser_pos = x_min self.laser_speed = abs(self.laser_speed) self.laser_pos += self.laser_speed laser_x = self.laser_pos self.laser_points = [self.middle_x, self.height - self.padding, laser_x, self.water_height + self.print_area_bottom + self.resin_height] def _draw_model(self): if self.axis_history: model_height = self.axis_history[-1][2] if model_height > (self.last_height + self.min_height) or not self.line_x: x1, y1, x2, y2 = self._get_pixels(self.axis_history[-1]) self.last_x_min = x1 self.last_x_max = x2 self.line_x.insert(0, x1) self.line_x.append(x2) self.line_y.insert(0, y1) self.line_y.append(y2) self.last_height = model_height points = [] for idx in range(0, len(self.line_x)): x = int(self.print_area_left + (self.line_x[idx] * self.print_area_width)) y = int(self.print_area_bottom + self.resin_height + (self.line_y[idx] * self.print_area_height)) - 2 points.append(x) points.append(y) self.model_instruction.clear() self.model_instruction.add(Color(rgba=(1.0, 0.0, 0.0, 1.0))) self.model_instruction.add(Line(points=points, width=2, close=True)) def _get_pixels(self, data): pixel_height = data[2] / self.printer_actual_dimensions[2] pixel_pos_min = (data[0][0] + (self.printer_actual_dimensions[1] / 2.0)) / self.printer_actual_dimensions[1] pixel_pos_max = (data[0][1] + (self.printer_actual_dimensions[1] / 2.0)) / self.printer_actual_dimensions[1] return [pixel_pos_min, pixel_height, pixel_pos_max, pixel_height]
class MapViewer(ScatterPlane): def __init__(self, **kwargs): kwargs.setdefault('do_rotation', False) kwargs.setdefault('show_border', False) kwargs.setdefault('close_on_idle', False) kwargs.setdefault('scale_min', 1) super(MapViewer, self).__init__(**kwargs) # init ScatterPlane with above parameters self.map = None self.tilesize = (0, 0) self.tileCache = TileCache() self._zoom = 0 # intern var managed by a property self.reticule = None self.show_arrow = False self.arrow = type('DefaultArrow', (), {'azimuth': 0}) # creation a a default object, so azimth can still be set self.idle = True # used by self.update # variable contenant la dernière position gps self.last_pos = (0, 0) self.last_pos_wgs84 = (0, 0) self.locked_on_pos = False # The path self.path = None self.path_width = 5.0 self.tracking_path = False self._path_zoom = 0 # intern var to track a zoom change # Layers self.map_layer = None self.path_layer = None # Variables relative to layers self.map_cleanup_scheduled = False # Reposition tasks self.reposition_executor = None # Finally, as every user action implies a change of x and/or y value, # we bind those properties change on the update self.bind(x=self.update, y=self.update) def view_map(self, _map): # Prepare the map self._prepare_map(_map) # Set the last pos in map coord if possible self._init_last_pos(_map) # Adding the map layer self.map_layer = InstructionGroup() self.canvas.add(self.map_layer) # Adding the path layer self.path_layer = InstructionGroup() self.path_layer.add(Color(1, 0, 0, 0.5)) self.path = Line(width=self.path_width) self.path_layer.add(self.path) self.canvas.add(self.path_layer) # Creation of the reposition task executor # Update the reticule and if needed the arrow position self.reposition_executor = RepositionExecutor(0.1) if self.reticule is not None: self.reposition_executor.add_reposition_task(lambda: self.set_reticule_pos(*self.last_pos)) self.reposition_executor.add_reposition_task(lambda: self.set_arrow_pos()) # The first time we view a map we have to trigger the first tile drawing and reposition the reticule Clock.schedule_once(self.update_map, 0) self.reposition_executor.execute() def view_map_for_calibration(self, _map): # Prepare the map self._prepare_map(_map) # Adding the map layer self.map_layer = InstructionGroup() self.canvas.add(self.map_layer) # Creation of the reposition task executor self.reposition_executor = RepositionExecutor(0.1) # The first time we view a map we have to trigger the first tile drawing and reposition the reticule Clock.schedule_once(self.update_map, 0) def _prepare_map(self, _map): # Cleanup the canvas, reinitialize variables self.canvas.clear() self.idle = True self.scale = 1 self.pos = 0, 0 self.tileCache.__init__() self.map = _map self.tilesize = _map.get_tile_size() # Here we set the maximum scale of the Scatter, information retrieved from max_zoom in calibration.ini self.scale_max = pow(2, _map.get_max_zoom()) # Save the path of the map's calibration file in application settings config.save_last_map_path(_map.calibration_file_path) def update(self, obj, value, delay=UPDATE_DELAY): """ To prevent overuse of update_map, each change of position triggers an update of the map after UPDATE_DELAY sec. For instance, the position of the reticule has to be updated. """ if self.map is None: return # Pause the Loader, to prevent ui blocking Loader.pause() # Reposition widgets that need to be repositioned self.reposition_executor.execute() # Trigger the map update if self.idle: self.idle = False else: Clock.unschedule(self.update_map) Clock.schedule_once(self.update_map, delay) # Resume the Loader after a short time Clock.schedule_once(MapViewer.resume_loading, 0.1) def update_map(self, dt): # First, we get the current area covered on the ScatterPlane area = self.get_covered_area() map_area = (0, 0, self.tilesize[0], self.tilesize[1]) # Then, we make the list of tiles corresponding to that area tile_list = self.generate_tile_list(self.zoom, map_area, area) # Before drawing the tiles, we may have to perform a cleanup of the map layer if self.map_cleanup_scheduled: self.map_layer.clear() self.tileCache.__init__() self.map_cleanup_scheduled = False # Tiles drawing self.draw_tiles(tile_list) # We schedule a cleanup, which will be done if the app is inactive at the time of the callback execution Clock.schedule_once(partial(self.cleanup, tile_list), CLEANUP_DELAY) self.idle = True # logger.debug("container : %s" % self.tileCache.container.values()) # If we are showing the path, we update its view (adjust the thickness) if self.tracking_path: self.format_path() @staticmethod def resume_loading(dt): Loader.resume() @property def zoom(self): """Get zoom from current scale""" # At each zoom step forward, we cover an area twice as small as the former area self._zoom = log(self.scale, 2) return self._zoom def get_covered_area(self): parent = self.parent xmin = parent.x xmax = parent.x + parent.width ymin = parent.y ymax = parent.y + parent.height # Coordinates in ScatterPlane # Here, local and window coordinates are the same because ScatterPlane's origin # corresponds to the windows's origin xmin, ymin = self.to_local(xmin, ymin) # (x,y) coord of the bottom left corner xmax, ymax = self.to_local(xmax, ymax) # (x,y) coord of the top right corner return xmin, ymin, xmax, ymax def out_of_scope(self): logger.debug("OUT OF SCOPE") return [] @staticmethod def extend_tile_view(tab, maxindex): if len(tab): if tab[0] > 0: tab.insert(0, tab[0] - 1) if tab[-1] < maxindex - 1: tab.append(tab[-1] + 1) def generate_tile_list(self, zoom, map_area, area): """ Generates the list of tiles that should be visible for the given zoom level and the area visible on the scatterPlane """ xmin0, ymin0, xmax0, ymax0 = map_area # area for zoom=0, one unique tile xmin, ymin, xmax, ymax = area # Overlap test if xmax <= xmin0 or xmin >= xmax0 or ymin >= ymax0 or ymax <= ymin0: return self.out_of_scope() # coordinates of the intersection between map_area and area xmin_inter = max(xmin0, xmin) xmax_inter = min(xmax0, xmax) ymin_inter = max(ymin0, ymin) ymax_inter = min(ymax0, ymax) # If the current zoom is already an integer, we take its value. # Otherwise, we take its superior int value because we want to # identify which tiles of the next zoom level have to be displayed zoom_int = int(zoom if zoom == int(zoom) else int(zoom) + 1) targeted_scale = pow(2, zoom_int) tile_width = (xmax0 - xmin0) / float(targeted_scale) startx_index = int(xmin_inter / tile_width) endx_index = int(xmax_inter / tile_width) # Calculation of the indexes on x axis x_indexes = [] append = x_indexes.append for x in range(startx_index, endx_index + 1): append(x) MapViewer.extend_tile_view(x_indexes, targeted_scale) starty_index = int(ymin_inter / tile_width) endy_index = int(ymax_inter / tile_width) # Calculation of the indexes on y axis y_indexes = [] append = y_indexes.append for y in range(starty_index, endy_index + 1): append(y) MapViewer.extend_tile_view(y_indexes, targeted_scale) tile_list = [] append = tile_list.append for x in x_indexes: for y in y_indexes: tile = Tile() # tile.canvas = self.canvas tile.pos = Vector(x, y) * tile_width tile.size = (tile_width, tile_width) tile.x = x tile.y = y tile.zoom = zoom_int append(tile) return tile_list # The good damn right way to do it def draw_tile(self, proxy): if proxy.image.texture: self.map_layer.add( Rectangle(pos=proxy.pos, size=proxy.size, texture=proxy.image.texture, group=proxy.zoom)) def draw_tiles(self, tile_list): for tile in tile_list: image_id = tile.get_id() if self.tileCache.add_tile(tile.zoom, image_id): image = self.map.get_tile(tile.zoom, tile.x, tile.y) if image is None: continue image.create_property("pos", tile.pos) image.create_property("size", tile.size) image.create_property("zoom", str(int(tile.zoom))) image.bind(on_load=self.draw_tile) # if image.loaded: # only useful when Loader actually caches images # image.dispatch("on_load") def cleanup(self, tile_list, *largs): """ Cleanup is achieved when the app is considered inactive, ie when self.idle = True. """ if not self.idle: return zoom = self.zoom zoom_int = int(zoom if zoom == int(zoom) else int(zoom) + 1) print "debut cleanup, conserve zoom", zoom_int for _zoom in TileCache.get_unnecessary_zooms(self.tileCache.container.keys(), zoom_int): try: print "suppr zoom", _zoom self.map_layer.remove_group(str(_zoom)) self.tileCache.remove_tiles_for_zoom(_zoom) except: logger.debug("the canvas doesn't contains the zoom %s" % _zoom) if self.tileCache.is_tile_overfull(zoom_int): self.map_cleanup_scheduled = True # logger.debug("cleanup done, container : %s" % self.tileCache.container.values()) def set_reticule(self, reticule): self.reticule = reticule def set_reticule_pos(self, x, y): """ :param x: x position on the map in the range [0,tile_width] :param y: y position on the map in the range [0,tile_height] :return: """ # TODO : remplacer cette méthode par set_movable_widget_pos ? (cf plus bas) self.reticule.set_center_x(self.x + x * self.scale) self.reticule.set_center_y(self.y + y * self.scale) def set_movable_widget_pos(self, widget, x, y): """ Position a widget given the local coordinates of its center. :param x: x position on the map in the range [0,tile_width] :param y: y position on the map in the range [0,tile_height] """ if widget: widget.set_center_x(self.x + x * self.scale) widget.set_center_y(self.y + y * self.scale) def add_movable_widget(self, widget): """Actions done when a movable widget is added to the MapViewer.""" # Add a reposition task self.reposition_executor.add_reposition_task(lambda: self.set_movable_widget_pos( widget, *widget.pos_local)) def set_arrow_pos(self): """Must be called after set_reticule_pos""" if self.show_arrow: self.arrow.set_center_x(self.reticule.get_center_x()) self.arrow.set_center_y(self.reticule.get_center_y()) def set_orientation_arrow(self, arrow): self.show_arrow = True self.arrow = arrow def update_azimuth(self, instance, angle): self.arrow.azimuth = -angle def _init_last_pos(self, _map): """Initialize the last position in the _map projection coordinates, given the last known wgs84 position. This is used in view_map method. """ if self.last_pos_wgs84 is not None: self.last_pos = _map.get_map_coord(*self.last_pos_wgs84) else: self.last_pos = (0, 0) def update_pos(self, instance, value): # Remember the position, even if there is no map self.last_pos_wgs84 = value # If there is no map, no need to go further if self.map is None: return # Conversion from wgs84 coord to map coord x, y = self.map.get_map_coord(*value) # Remember this position too self.last_pos = x, y # Update the reticule pos self.set_reticule_pos(x, y) # Update the orientation arrow pos if necessary if self.show_arrow: self.set_arrow_pos() # Update the path if self.tracking_path: self.update_path(x, y) # If we are locked on pos, we center the view on the last known position if self.locked_on_pos: self.center_on_last_pos() def update_path(self, x, y): self.path.points += x, y def toggle_tracking_path(self, obj): if self.tracking_path: self.tracking_path = False # Creation of a new path self.path = Line(width=self.path_width) else: self.tracking_path = True def format_path(self, *args): """This updates the width of the path to be consistent with the scale. """ if self._path_zoom != self.zoom: self.path.width = self.path_width / self.scale self._path_zoom = self.zoom def center_on_last_pos(self, *args): scale = self.scale new_x = Window.size[0]/2 - self.last_pos[0]*scale new_y = Window.size[1]/2 - self.last_pos[1]*scale Animation.cancel_all(self) anim = Animation(x=new_x, y=new_y, t='in_out_quad', duration=0.5) anim.start(self) def get_dist_to_center(self): """ :return: The distance in meters between the last known position and the position represented by the middle of the screen. If no distance can be calculated, it returns -1. """ if self.map is None: return -1 try: merc_x, merc_y = self.map.map_coord_to_map_projection(*self.to_local(Window.size[0]/2, Window.size[1]/2)) except ZeroDivisionError: return -1 lat, lon = Map.to_geographic(merc_x, merc_y) lat_last, lon_last = self.last_pos_wgs84 dist = Map.distance(lat_last, lon_last, lat, lon) return dist def transform_with_touch(self, touch): if self.locked_on_pos: if len(self._touches) == 1: return False changed = False # We have more than one touch... list of last known pos points = [Vector(self._last_touch_pos[t]) for t in self._touches if t is not touch] # Add current touch last points.append(Vector(touch.pos)) # We only want to transform if the touch is part of the two touches # farthest apart! So first we find anchor, the point to transform # around as another touch farthest away from current touch's pos anchor_ = max(points[:-1], key=lambda p: p.distance(touch.pos)) # Now we find the touch farthest away from anchor, if its not the # same as touch. Touch is not one of the two touches used to transform farthest = max(points, key=anchor_.distance) if farthest is not points[-1]: return changed # Ok, so we have touch, and anchor, so we can actually compute the # transformation old_line = Vector(*touch.ppos) - anchor_ new_line = Vector(*touch.pos) - anchor_ if not old_line.length(): # div by zero return changed # pol : we don't want rotation here # angle = radians(new_line.angle(old_line)) * self.do_rotation # self.apply_transform(Matrix().rotate(angle, 0, 0, 1), anchor=anchor) # pol : trick -> change the origin!! anchor = Vector(self.to_parent(*self.last_pos)) if self.do_scale: scale = new_line.length() / old_line.length() new_scale = scale * self.scale if new_scale < self.scale_min: scale = self.scale_min / self.scale elif new_scale > self.scale_max: scale = self.scale_max / self.scale self.apply_transform(Matrix().scale(scale, scale, scale), anchor=anchor) changed = True return changed super(MapViewer, self).transform_with_touch(touch)
class PrinterAnimation(RelativeLayout): padding = NumericProperty(40) printer_actual_dimensions = ListProperty([10, 10, 80]) printer_current_actual_height = NumericProperty(0.0) print_area_height = NumericProperty(1) print_area_width = NumericProperty(1) print_area_left = NumericProperty(0) print_area_bottom = NumericProperty(40) container_padding = NumericProperty(0) container_left = NumericProperty(0) container_width = NumericProperty(0) container_bottom = NumericProperty(0) container_height = NumericProperty(0) laser_size = ListProperty([40, 40]) resin_height = NumericProperty(20) water_height = NumericProperty(20) scale = NumericProperty(1.0) resin_color = ListProperty([0.0, 0.8, 0.0, 0.6]) water_color = ListProperty([0.2, 0.2, 1.0, 0.6]) container_color = ListProperty([1.0, 1.0, 1.0, 1.0]) laser_color_edge2 = ListProperty([0.0, 0.0, 0.5, 1.0]) laser_color_edge = ListProperty([0.0, 0.0, 1.0, 1.0]) laser_color = ListProperty([0.7, 1.0, 1.0, 1.0]) drip_history = ListProperty() laser_points = ListProperty() middle_x = NumericProperty(52) laser_pos = NumericProperty(60) laser_speed = NumericProperty(1) refresh_rate = NumericProperty(1.0) def __init__(self, **kwargs): super(PrinterAnimation, self).__init__(**kwargs) self.drip_time_range = 5 self.waiting_for_drips = True self.refresh_rate = App.get_running_app().refresh_rate self._gl_setup() self.axis_history = [] self.drips = 0 self.line_x = [] self.line_y = [] self.last_height = 0.0 self.min_height = 0.0 self.last_x_min = 0.0 self.last_x_max = 1.0 self.is_on_canvas = False def on_printer_actual_dimensions(self, instance, value): self.min_height = self.printer_actual_dimensions[2] / 400.0 self.on_size(None) def _gl_setup(self): self.drip_texture = CoreImage("resources/images/drop.png", mipmap=True).texture self.drips_instruction = InstructionGroup() self.model_instruction = InstructionGroup() print(self.canvas.children) # self.canvas.add(self.drips_instruction) # self.canvas.add(self.model_instruction) def on_size(self, *largs): bounds_y = (self.height * 0.7) - self.resin_height bounds_x = self.width - (self.padding * 2) printer_x = self.printer_actual_dimensions[0] printer_y = self.printer_actual_dimensions[2] self.laser_pos = self.width / 2 self.scale = min(bounds_y / printer_y, bounds_x / printer_x) Logger.info("Scale: {}".format(self.scale)) self.print_area_width = printer_x * self.scale self.print_area_height = printer_y * self.scale def redraw(self, key): self._draw_drips() self._draw_laser() self._draw_model() if not self.is_on_canvas: self.canvas.insert(4, self.drips_instruction) self.canvas.insert(4, self.model_instruction) self.is_on_canvas = True Clock.unschedule(self.redraw) Clock.schedule_once(self.redraw, self.refresh_rate) def reset(self): self.axis_history = [] self.line_x = [] self.line_y = [] self.last_height = 0 self.min_height = 0.0 self.laser_points = [] self.drip_history = [] self.waiting_for_drips = True self.printer_current_actual_height = 0 self.redraw('') Clock.unschedule(self.redraw) def animation_start(self, *args): Clock.unschedule(self.redraw) self.axis_history = [] self.line_x = [] self.line_y = [] self.last_height = 0 self.min_height = 0.0 self.laser_points = [] self.drip_history = [] Clock.schedule_once(self.redraw, self.refresh_rate) def animation_stop(self): Clock.unschedule(self.redraw) self.axis_history = [] self.line_x = [] self.line_y = [] self.last_height = 0 self.min_height = 0.0 self.laser_points = [] self.drip_history = [] def _draw_drips(self): self.drips_instruction.clear() self.drips_instruction.add(Color(1, 1, 1, 1)) top = time.time() bottom = top - self.drip_time_range drips_in_history = len(self.drip_history) for (index, drip_time) in zip(range(drips_in_history, 0, -1), self.drip_history): if drip_time > bottom: time_ago = top - drip_time y_pos_percent = (self.drip_time_range - time_ago) / self.drip_time_range drip_pos_y = (self.height * y_pos_percent) + self.padding xoff = 10 + math.sin((len(self.drip_history) - index) / (2 * math.pi)) * 20 self.drips_instruction.add(Rectangle(size=[12, 16], pos=[self.print_area_left + xoff, drip_pos_y], texture=self.drip_texture)) def _draw_laser(self): if self.waiting_for_drips: self.laser_points = [] else: x_min = self.print_area_left + (self.last_x_min * self.print_area_width) x_max = self.print_area_left + (self.last_x_max * self.print_area_width) if (self.laser_pos >= x_max): self.laser_pos = x_max self.laser_speed = abs(self.laser_speed) * -1 if (self.laser_pos <= x_min): self.laser_pos = x_min self.laser_speed = abs(self.laser_speed) self.laser_pos += self.laser_speed laser_x = self.laser_pos self.laser_points = [self.middle_x, self.height - self.padding, laser_x, self.water_height + self.print_area_bottom + self.resin_height] def _draw_model(self): if self.axis_history: model_height = self.axis_history[-1][2] if model_height > (self.last_height + self.min_height) or not self.line_x: x1, y1, x2, y2 = self._get_pixels(self.axis_history[-1]) self.last_x_min = x1 self.last_x_max = x2 self.line_x.insert(0, x1) self.line_x.append(x2) self.line_y.insert(0, y1) self.line_y.append(y2) self.last_height = model_height points = [] for idx in range(0, len(self.line_x)): x = int(self.print_area_left + (self.line_x[idx] * self.print_area_width)) y = int(self.print_area_bottom + self.resin_height + (self.line_y[idx] * self.print_area_height)) - 2 points.append(x) points.append(y) self.model_instruction.clear() self.model_instruction.add(Color(rgba=(1.0, 0.0, 0.0, 1.0))) self.model_instruction.add(Line(points=points, width=2, close=True)) else: self.model_instruction.clear() def _get_pixels(self, data): pixel_height = data[2] / self.printer_actual_dimensions[2] pixel_pos_min = (data[0][0] + (self.printer_actual_dimensions[1] / 2.0)) / self.printer_actual_dimensions[1] pixel_pos_max = (data[0][1] + (self.printer_actual_dimensions[1] / 2.0)) / self.printer_actual_dimensions[1] return [pixel_pos_min, pixel_height, pixel_pos_max, pixel_height]
class TextureStack(Widget): """Several textures superimposed on one another, and possibly offset by some amount. In 2D games where characters can wear different clothes or hold different equipment, their graphics are often composed of several graphics layered on one another. This widget simplifies the management of such compositions. """ texs = ListProperty() """Texture objects""" offxs = ListProperty() """x-offsets. The texture at the same index will be moved to the right by the number of pixels in this list. """ offys = ListProperty() """y-offsets. The texture at the same index will be moved upward by the number of pixels in this list. """ def _get_offsets(self): return zip(self.offxs, self.offys) def _set_offsets(self, offs): offxs = [] offys = [] for x, y in offs: offxs.append(x) offys.append(y) self.offxs, self.offys = offxs, offys offsets = AliasProperty(_get_offsets, _set_offsets, bind=('offxs', 'offys')) """List of (x, y) tuples by which to offset the corresponding texture.""" _texture_rectangles = DictProperty({}) """Private. Rectangle instructions for each of the textures, keyed by the texture. """ def __init__(self, **kwargs): """Make triggers and bind.""" kwargs['size_hint'] = (None, None) self.translate = Translate(0, 0) self.group = InstructionGroup() super().__init__(**kwargs) self.bind(offxs=self.on_pos, offys=self.on_pos) def on_texs(self, *args): """Make rectangles for each of the textures and add them to the canvas, taking their stacking heights into account. """ if not self.canvas or not self.texs: Clock.schedule_once(self.on_texs, 0) return texlen = len(self.texs) # Ensure each property is the same length as my texs, padding # with 0 as needed for prop in ('offxs', 'offys'): proplen = len(getattr(self, prop)) if proplen > texlen: setattr(self, prop, getattr(self, prop)[:proplen - texlen]) if texlen > proplen: propval = list(getattr(self, prop)) propval += [0] * (texlen - proplen) setattr(self, prop, propval) self.group.clear() self._texture_rectangles = {} w = h = 0 (x, y) = self.pos self.translate.x = x self.translate.y = y self.group.add(PushMatrix()) self.group.add(self.translate) for tex, offx, offy in zip(self.texs, self.offxs, self.offys): rect = Rectangle(pos=(offx, offy), size=tex.size, texture=tex) self._texture_rectangles[tex] = rect self.group.add(rect) tw = tex.width + offx th = tex.height + offy if tw > w: w = tw if th > h: h = th self.size = (w, h) self.group.add(PopMatrix()) if self.group not in self.canvas.children: self.canvas.add(self.group) def on_pos(self, *args): """Translate all the rectangles within this widget to reflect the widget's position. """ (x, y) = self.pos Logger.debug("TextureStack: repositioning to {}".format((x, y))) self.translate.x = x self.translate.y = y def clear(self): """Clear my rectangles, ``texs``, and ``stackhs``.""" self.group.clear() self._texture_rectangles = {} self.texs = [] self.offxs = [] self.offys = [] self.size = [1, 1] def insert(self, i, tex): """Insert the texture into my ``texs``, waiting for the creation of the canvas if necessary. """ if not self.canvas: Clock.schedule_once(lambda dt: TextureStack.insert(self, i, tex), 0) return self.texs.insert(i, tex) self.offxs.insert(i, 0) self.offys.insert(i, 0) def append(self, tex): """``self.insert(len(self.texs), tex)``""" self.insert(len(self.texs), tex) def __delitem__(self, i): """Remove a texture, its rectangle, and its stacking height""" tex = self.texs[i] try: rect = self._texture_rectangles[tex] self.canvas.remove(rect) del self._texture_rectangles[tex] except KeyError: pass del self.offxs[i] del self.offys[i] del self.texs[i] def __setitem__(self, i, v): """First delete at ``i``, then insert there""" if len(self.texs) > 0: self._no_upd_texs = True self.__delitem__(i) self._no_upd_texs = False self.insert(i, v) def pop(self, i=-1): """Delete the offsets and texture at ``i``, returning the texture. """ del self.offxs[i] del self.offys[i] return self.texs.pop(i)
class String(FloatLayout): open_note_val = NumericProperty(0) num_frets = NumericProperty(12) fret_positions = ListProperty() note_vals = ListProperty() mode_filter = NumericProperty(0b101011010101) root_note_idx = NumericProperty(0) scale_text = StringProperty("") notes_to_highlight = StringProperty("") notes_or_octaves = StringProperty("") animation_prop = NumericProperty(0) hit_prop = NumericProperty(0) def __init__(self, **kwargs): super().__init__(**kwargs) self.string_shadow = Rectangle() self.string_graphic = Rectangle() self.note_markers = InstructionGroup() self.octave_markers = InstructionGroup() self.canvas.add(Color(rgba=[0 / 255, 0 / 255, 0 / 255, 0.25])) self.canvas.add(self.string_shadow) self.canvas.add(Color(rgba=[169 / 255, 169 / 255, 169 / 255, 1])) self.canvas.add(self.string_graphic) self._add_markers() self.canvas.add(self.note_markers) self.canvas.add(self.octave_markers) self.bind(size=self.update_canvas, pos=self.update_canvas) self.anim = Animation() self.hit_anim = Animation() self.play_instrs = [] def _add_markers(self): for i in range(25): marker = Marker() self.note_markers.add(marker) def animate_marker(self, index, *args): markers = self.note_markers.children anim = Animation(animation_prop=1, duration=0.5, t="in_circ") anim.bind(on_start=markers[index].initiate_animation) anim.bind(on_progress=markers[index].update_animation) anim.bind(on_complete=markers[index].end_animation) anim.start(self) def on_touch_down(self, touch): if self.collide_point(*touch.pos): for i in range(len(self.note_markers.children)): self.animate_marker(i) def update_canvas(self, *args): if self.fret_positions: # self.fret_positions is empty during instantiation. # self.update_octave_markers() self.update_string_graphics() self.update_note_markers() def update_string_graphics(self): w, h = self.width, self.height * 0.1 x, y = self.pos cy = y + (self.height / 2) string_y = cy - (h / 2) shadow_height = 3 * h shadow_y = string_y - shadow_height # Shadow effect. self.string_shadow.size = [w, shadow_height] self.string_shadow.pos = [x, cy - shadow_height] # String. self.string_graphic.size = [w, h] self.string_graphic.pos = [x, string_y] def update_note_markers(self, *args): x, y = self.pos r1 = self.height / 2 r2 = r1 * 0.9 rdiff = r1 - r2 for i, (note_val, marker) in enumerate( zip(self.note_vals, self.note_markers.children)): # Make right edge of circle touch left edge of fret bar (where your finger should go!) fret_left = self.fret_positions[i] - ( self.fretboard.fret_bar_width / 2) # Draw 2 concentric circles, c1 and c2. # Circles are defined by a square's lower left corner. c1x, c1y = (fret_left - 2 * r1) + x, y c2x, c2y = c1x + rdiff, c1y + rdiff octave, note_idx = divmod(note_val, 12) included = int( bin(self.mode_filter)[2:][note_idx - self.root_note_idx]) highlighted = int( bin(scale_highlights[self.notes_to_highlight])[2:][ note_idx - self.root_note_idx]) if self.notes_or_octaves == "Notes": color_idx = note_idx - self.root_note_idx color = rainbow[color_idx] else: color_idx = octave - 1 color = octave_colors[color_idx] if self.scale_text == "Scale Degrees": note_idx -= self.root_note_idx note_text = scale_texts[self.scale_text][note_idx] marker.update(i, note_text, c1x, c1y, r1, c2x, c2y, r2, included, highlighted, color) def update_octave_markers(self): self.octave_markers.clear() for i, note_val in enumerate(self.note_vals): self.update_octave_marker(i, note_val) def update_octave_marker(self, i, note_val): if self.fret_ranges: octave = (note_val - self.fretboard.root_note_idx) // 12 left, right = self.fret_ranges[i] width = right - left self.octave_markers.add(octave_colors[octave]) self.octave_markers.add( Rectangle(pos=[left, 0], size=[width, self.height])) def on_open_note_val(self, instance, value): self.note_vals = [ val for val in range(self.open_note_val, self.open_note_val + 25) ] self.update_canvas(instance, value) def on_num_frets(self, instance, value): self.note_vals = [ val for val in range(self.open_note_val, self.open_note_val + 25) ] self.update_canvas(instance, value) def on_root_note_idx(self, instance, value): self.update_canvas(instance, value) def on_mode_filter(self, instance, value): self.update_canvas(instance, value) def on_scale_text(self, instance, value): self.update_note_markers() def on_notes_to_highlight(self, instance, value): self.update_note_markers() def on_notes_or_octaves(self, *args): self.update_note_markers() ### SONG PLAYING METHODS def play_thread(self, lead_in): # The GuitarPro songs' tempo are of form BPM where the B(eat) is always a quarter note. thread = Thread(target=partial(self._play_thread_animation, lead_in), daemon=True) thread.start() def _play_thread_animation(self, lead_in): self.stopped = False markers = self.note_markers.children idx = 0 time.sleep(lead_in) start = time.time() goal = start while not self.stopped: if idx == len(self.play_instrs): return fret_num, seconds = self.play_instrs[idx] if fret_num != -1: # self._play_fret(fret_num, seconds) self.animation_prop = 0 anim = Animation(animation_prop=1, duration=seconds) anim.bind(on_start=markers[fret_num].initiate_animation) anim.bind(on_progress=markers[fret_num].update_animation) anim.bind(on_complete=markers[fret_num].end_animation) self.anim = anim self.hit_prop = 0 hit_anim = Animation(hit_prop=1, duration=min(seconds, 0.1)) hit_anim.bind( on_start=markers[fret_num].initiate_hit_animation) hit_anim.bind( on_progress=markers[fret_num].update_hit_animation) hit_anim.bind(on_complete=markers[fret_num].end_hit_animation) self.hit_anim = hit_anim anim.start(self) hit_anim.start(self) goal += seconds idx += 1 time.sleep(max(goal - time.time(), 0)) # def _play_fret(self, fret_num, seconds): # self.anim.stop(self) # self.animation_prop = 0 # markers = self.note_markers.children # anim = Animation(animation_prop=1, duration=seconds) # anim.bind(on_start=markers[fret_num].initiate_animation) # anim.bind(on_progress=markers[fret_num].update_animation) # anim.bind(on_complete=markers[fret_num].end_animation) # self.anim = anim # anim.start(self) def stop(self): self.stopped = True