class HairlineRenderer: def __init__(self, view: 'BoardViewWidget') -> None: self.__view = view def initializeGL(self) -> None: self.__dtype = numpy.dtype([('vertex', numpy.float32, 2)]) self.__shader = self.__view.gls.shader_cache.get( "basic_fill_vert", "basic_fill_frag") self._va_vao = VAO() self._va_batch_vbo = VBO(numpy.array([], dtype=self.__dtype), GL.GL_STREAM_DRAW) GL.glObjectLabel(GL.GL_BUFFER, int(self._va_batch_vbo), -1, "Hairline VA batch VBO") with self._va_vao, self._va_batch_vbo: VBOBind(self.__shader.program, self.__dtype, "vertex").assign() def render_va(self, mat: 'npt.NDArray[numpy.float64]', va: 'VA_xy', col: int) -> None: self._va_batch_vbo.set_array(va.buffer()[:]) with self.__shader.program, self._va_vao, self._va_batch_vbo: GL.glUniformMatrix3fv(self.__shader.uniforms.mat, 1, True, mat.astype(numpy.float32)) GL.glUniform4ui(self.__shader.uniforms.layer_info, 255, col, 0, 0) GL.glDrawArrays(GL.GL_LINES, 0, va.count())
class RenderBatch(object): def __init__(self, draw_type=GL_QUADS): self.count = 0 self.color_data = [] self.position_data = [] self.color_buffer = VBO(np.array([])) self.position_buffer = VBO(np.array([])) self.draw_type = draw_type def draw2d(self, points, color=(0, 0, 0, 1), rotation=0, center=(0, 0)): n = len(points) self.count += n if not isinstance(color[0], (tuple, list)): color = [color]*n if rotation: transform = psi.calc.rotation_matrix(rotation) temp = np.array(points) - center temp = transform.dot(temp.T).T + center points = temp.tolist() self.color_data.extend(color) self.position_data.extend(points) def clear(self): self.position_data = [] self.color_data = [] self.count = 0 def render(self): self.color_buffer.set_array(np.array(self.color_data, dtype='float32')) self.position_buffer.set_array(np.array(self.position_data, dtype='float32')) self.color_buffer.bind() glColorPointer(4, GL_FLOAT, 0, self.color_buffer) self.position_buffer.bind() glVertexPointer(2, GL_FLOAT, 0, self.position_buffer) glEnableClientState(GL_VERTEX_ARRAY) glEnableClientState(GL_COLOR_ARRAY) glDrawArrays(self.draw_type, 0, self.count) glDisableClientState(GL_COLOR_ARRAY) glDisableClientState(GL_VERTEX_ARRAY)
class RenderBatch(object): def __init__(self, draw_type=GL_QUADS): self.count = 0 self.color_data = [] self.position_data = [] self.color_buffer = VBO(np.array([])) self.position_buffer = VBO(np.array([])) self.draw_type = draw_type def draw2d(self, points, color=(0, 0, 0, 1), rotation=0, center=(0, 0)): n = len(points) self.count += n if not isinstance(color[0], (tuple, list)): color = [color] * n if rotation: transform = psi.calc.rotation_matrix(rotation) temp = np.array(points) - center temp = transform.dot(temp.T).T + center points = temp.tolist() self.color_data.extend(color) self.position_data.extend(points) def clear(self): self.position_data = [] self.color_data = [] self.count = 0 def render(self): self.color_buffer.set_array(np.array(self.color_data, dtype='float32')) self.position_buffer.set_array( np.array(self.position_data, dtype='float32')) self.color_buffer.bind() glColorPointer(4, GL_FLOAT, 0, self.color_buffer) self.position_buffer.bind() glVertexPointer(2, GL_FLOAT, 0, self.position_buffer) glEnableClientState(GL_VERTEX_ARRAY) glEnableClientState(GL_COLOR_ARRAY) glDrawArrays(self.draw_type, 0, self.count) glDisableClientState(GL_COLOR_ARRAY) glDisableClientState(GL_VERTEX_ARRAY)
class BackgroundGridRender: def __init__(self): self.__width = 0 self.__height = 0 self.__vbo_dtype = numpy.dtype([("vertex", numpy.float32, 2)]) def initializeGL(self, gls, width, height): # Get shader self.__shader = gls.shader_cache.get("basic_fill_vert", "basic_fill_frag") self.__vbo = VBO(numpy.ndarray(0, dtype=self.__vbo_dtype), GL.GL_STATIC_DRAW) self.__vao = VAO() # Create array of lines sufficient to fill screen self.resize(width, height) def __get_vbo_data(self): va = VA_xy(1024) # Unit steps from -n/2..n/2 along X axis. Y lines from -1 to +1 for i in range(0, self.n_steps * 2, 2): i_ = i - int(self.n_steps / 2) va.add_line(i_, -1, i_, 1) return va def resize(self, width, height): self.n_steps = max(self.__width, self.__height) // MIN_PIXEL_SPACE self.__vbo.set_array(self.__get_vbo_data()) def set_grid(self, step): pass def draw(self, transform_matrix): pass
class RenderBatchOpt(object): def __init__(self, draw_type=GL_QUADS): self.count = 0 self.color_buffer = VBO(np.array([])) self.vertex_buffer = VBO(np.array([])) self.draw_type = draw_type def draw2d(self, points, color=(0, 0, 0, 1), rotation=0, center=(0, 0)): n = points.shape[0] self.count += n if rotation: transform = psi.calc.rotation_matrix(rotation) temp = points - center temp = transform.dot(temp.T).T + center points = temp.tolist() self.color_buffer.set_array(color) self.vertex_buffer.set_array(points) def clear(self): self.color_buffer.set_array(np.array([])) self.vertex_buffer.set_array(np.array([])) self.count = 0 def render(self): self.color_buffer.bind() glColorPointer(4, GL_FLOAT, 0, self.color_buffer) self.vertex_buffer.bind() glVertexPointer(2, GL_FLOAT, 0, self.vertex_buffer) glEnableClientState(GL_VERTEX_ARRAY) glEnableClientState(GL_COLOR_ARRAY) glDrawArrays(self.draw_type, 0, self.count) glDisableClientState(GL_COLOR_ARRAY) glDisableClientState(GL_VERTEX_ARRAY)
class RectAlignmentControllerView(BaseToolController): changed = QtCore.Signal() def __init__(self, parent, model: 'pcbre.ui.dialogs.layeralignmentdialog.dialog.AlignmentViewModel'): super(RectAlignmentControllerView, self).__init__() self._parent = parent self.model_overall = model self.model = model.ra self.__idx_handle_sel: Optional[int] = None self.__idx_handle_hover: Optional[int] = None self.__behave_mode = DM.NONE self.__ghost_handle = None self.__init_interaction() self.gls: Optional[Any] = None self.active = False def change(self) -> None: self.changed.emit() @property def tool_actions(self) -> 'Sequence[ToolActionDescription]': return g_ACTIONS def __init_interaction(self) -> None: # Selection / drag handling self.__idx_handle_sel = None self.__behave_mode = DM.NONE # ghost handle for showing placement self.__ghost_handle = None def initialize(self) -> None: self.__init_interaction() self.active = True def finalize(self) -> None: self.active = False def initializeGL(self, gls: 'GLShared') -> None: self.gls = gls assert self.gls is not None # Basic solid-color program self.prog = self.gls.shader_cache.get("vert2", "frag1") self.mat_loc = GL.glGetUniformLocation(self.prog.program, "mat") self.col_loc = GL.glGetUniformLocation(self.prog.program, "color") # Build a VBO for rendering square "drag-handles" self.vbo_handles_ar = numpy.ndarray((4, ), dtype=[("vertex", numpy.float32, 2)]) self.vbo_handles_ar["vertex"] = numpy.array(corners) * HANDLE_HALF_SIZE self.vbo_handles = VBO(self.vbo_handles_ar, GL.GL_STATIC_DRAW, GL.GL_ARRAY_BUFFER) self.vao_handles = VAO() with self.vbo_handles, self.vao_handles: VBOBind(self.prog.program, self.vbo_handles_ar.dtype, "vertex").assign() # Build a VBO/VAO for the perimeter # We don't initialize it here because it is updated every render # 4 verticies for outside perimeter # 6 verticies for each dim self.vbo_per_dim_ar = numpy.zeros(16, dtype=[("vertex", numpy.float32, 2)]) self.vbo_per_dim = VBO(self.vbo_per_dim_ar, GL.GL_DYNAMIC_DRAW, GL.GL_ARRAY_BUFFER) self.vao_per_dim = VAO() with self.vao_per_dim, self.vbo_per_dim: VBOBind(self.prog.program, self.vbo_per_dim_ar.dtype, "vertex").assign() def im2V(self, pt: Vec2) -> Vec2: """Translate Image coordinates to viewport coordinates""" if self.model_overall.view_mode.is_aligned(): ph = project_point(self.model.image_matrix, pt) return self.viewPort.tfW2V(ph) else: return self.viewPort.tfW2V(pt) def V2im(self, pt: Vec2) -> Vec2: """ Translate viewport coordinates to image coordinates :param pt: :return: """ world = self.viewPort.tfV2W(pt) if self.model_overall.view_mode.is_aligned(): inv = numpy.linalg.inv(self.model.image_matrix) return project_point(inv, world) else: return world # inv = numpy.linalg.inv(self.model.image_matrix) # return Vec2(inv.dot(pt)[:2]) def gen_dim(self, idx: int, always_above: bool = True) -> 'npt.NDArray[numpy.float64]': """ Generate rendering data for the dimension-lines :param idx: :return: """ a = self.im2V(self.model.dim_handles[0 + idx]) b = self.im2V(self.model.dim_handles[1 + idx]) d = b - a delta = (b - a).norm() normal = Vec2.from_mat(rotate(math.pi / 2)[:2, :2].dot(delta)) if always_above: if numpy.cross(Vec2(1, 0), normal) > 0: normal = -normal res = numpy.array([ a + normal * 8, a + normal * 20, a + normal * 15, b + normal * 15, b + normal * 8, b + normal * 20, ], dtype=numpy.float64) return res @property def disabled(self) -> bool: # When the we're showing the aligned view; or if we are using keypoint align # don't render any handles return not self.active or self.model_overall.view_mode.is_aligned() def render(self, viewPort: 'ViewPort') -> None: self.viewPort = viewPort # Perimeter is defined by the first 4 handles self.vbo_per_dim_ar["vertex"][:4] = [self.im2V(pt) for pt in self.model.align_handles[:4]] # Generate the dimension lines. For ease of use, we always draw the dim-lines above when dims are manual # or below when dims are unlocked self.vbo_per_dim_ar["vertex"][4:10] = self.gen_dim(0, not self.model.dims_locked) self.vbo_per_dim_ar["vertex"][10:16] = self.gen_dim(2, not self.model.dims_locked) self.vbo_per_dim.set_array(self.vbo_per_dim_ar) # Ugh..... PyOpenGL isn't smart enough to bind the data when it needs to be copied with self.vbo_per_dim: self.vbo_per_dim.copy_data() GL.glDisable(GL.GL_BLEND) # ... and draw the perimeter with self.vao_per_dim, self.prog.program: GL.glUniformMatrix3fv(self.mat_loc, 1, True, self.viewPort.glWMatrix.astype(numpy.float32)) # Draw the outer perimeter if self.disabled: GL.glUniform4f(self.col_loc, 0.8, 0.8, 0.8, 1) else: GL.glUniform4f(self.col_loc, 0.8, 0.8, 0, 1) GL.glDrawArrays(GL.GL_LINE_LOOP, 0, 4) # Draw the dimensions GL.glUniform4f(self.col_loc, 0.8, 0.0, 0.0, 1) GL.glDrawArrays(GL.GL_LINES, 4, 6) GL.glUniform4f(self.col_loc, 0.0, 0.0, 0.8, 1) GL.glDrawArrays(GL.GL_LINES, 10, 6) if self.disabled: return # Now draw a handle at each corner with self.vao_handles, self.prog.program: for n, i in enumerate(self.model.align_handles): # skip nonexistent handles if i is None: continue is_anchor = IDX_IS_LINE(n) corner_pos = self.im2V(i) if self.disabled: color = [0.8, 0.8, 0.8, 1] elif self.__idx_handle_sel == n: color = [1, 1, 1, 1] elif self.__idx_handle_hover == n: color = [1, 1, 0, 1] else: color = [0.8, 0.8, 0, 0.5] self.render_handle(corner_pos, color, is_anchor, True) if self.__idx_handle_sel == n: self.render_handle(corner_pos, [0, 0, 0, 1], is_anchor, False) if self.__ghost_handle is not None: self.render_handle(self.__ghost_handle, [0.8, 0.8, 0, 0.5], True) if not self.model.dims_locked: for n, i in enumerate(self.model.dim_handles): handle_pos = self.im2V(i) if n == self.__idx_handle_sel: color = [1, 1, 1, 1] if n < 2: color = [0.8, 0.0, 0.0, 1] else: color = [0.0, 0.0, 0.8, 1] self.render_handle(handle_pos, color, False, True) if self.__idx_handle_sel == n: self.render_handle(corner_pos, [0, 0, 0, 1], is_anchor, False) def render_handle(self, position: Vec2, color: Sequence[float], diagonal: bool = False, filled: bool = False) -> None: if diagonal: r = rotate(math.pi / 4) else: r = numpy.identity(3) m = self.viewPort.glWMatrix.dot(translate(*position).dot(r)) GL.glUniformMatrix3fv(self.mat_loc, 1, True, m.astype(numpy.float32)) GL.glUniform4f(self.col_loc, *color) GL.glDrawArrays(GL.GL_TRIANGLE_FAN if filled else GL.GL_LINE_LOOP, 0, 4) def get_handle_index_for_mouse(self, pos: Vec2) -> Optional[int]: """ Returns the index of a handle (or None if one isn't present) given a MoveEvent""" for n, handle in enumerate(self.model.all_handles()): if handle is None: continue # get the pix-wise BBOX of the handle p = self.im2V(handle) # Rect encompassing the handle r = Rect.from_center_size(p, HANDLE_HALF_SIZE * 2, HANDLE_HALF_SIZE * 2) # If event inside the bbox if r.point_test(pos) != 0: return n return None def get_line_query_for_mouse(self, pos: Vec2) -> Tuple[Optional[int], Optional[Vec2]]: for n, (p1, p2) in enumerate(self.model.lines()): p1_v = self.im2V(p1) p2_v = self.im2V(p2) p, d = project_point_line(pos, p1_v, p2_v) if d is not None and d < HANDLE_HALF_SIZE: return n, p return None, None def event_add_constraint(self, event: 'ToolActionEvent') -> None: idx, p = self.get_line_query_for_mouse(event.cursor_pos) if idx is not None: anchors = self.model.get_anchors(idx) if len(anchors) < 2: p = self.V2im(p) idx = new_anchor_index(self.model, idx) cmd = cmd_set_handle_position(self.model, idx, p) self._parent.undoStack.push(cmd) self.__idx_handle_sel = idx self.__idx_handle_hover = None def event_remove_constraint(self, event: 'ToolActionEvent') -> None: handle = self.get_handle_index_for_mouse(event.cursor_pos) if handle is not None and handle >= 4: cmd = cmd_set_handle_position(self.model, handle, None) self._parent.undoStack.push(cmd) self.__idx_handle_sel = None self.__idx_handle_hover = None def event_select(self, event: 'ToolActionEvent') -> None: handle = self.get_handle_index_for_mouse(event.cursor_pos) self.__idx_handle_sel = handle self.__idx_handle_hover = None if handle is not None: self.__behave_mode = DM.DRAGGING def event_release(self, event: 'ToolActionEvent') -> None: if self.__behave_mode == DM.DRAGGING: self.__behave_mode = DM.NONE def tool_event(self, event: 'ToolActionEvent') -> None: if self.disabled: return if event.code == EventCode.Select: self.event_select(event) elif event.code == EventCode.Release: self.event_release(event) elif event.code == EventCode.AddToLine: self.event_add_constraint(event) elif event.code == EventCode.RemoveFromLine: self.event_remove_constraint(event) def mouseMoveEvent(self, event: 'MoveEvent') -> None: if self.disabled: return needs_update = False idx = self.get_handle_index_for_mouse(event.cursor_pos) if self.__ghost_handle is not None: self.__ghost_handle = None if self.__behave_mode == DM.NONE: if idx is not None: self.__idx_handle_hover = idx else: self.__idx_handle_hover = None # if event.modifiers() & ADD_MODIFIER: # line_idx, pos = self.get_line_query_for_mouse(event.cursor_pos) # if line_idx is not None: # self.__ghost_handle = pos elif self.__behave_mode == DM.DRAGGING: w_pos = self.V2im(event.cursor_pos) # print(w_pos, event.world_pos) cmd = cmd_set_handle_position(self.model, self.__idx_handle_sel, w_pos, merge=True) self._parent.undoStack.push(cmd) def focusOutEvent(self, evt: QtCore.QEvent) -> None: self.__idx_handle_sel = None def keyPressEvent(self, evt: QtGui.QKeyEvent) -> bool: if self.disabled: return False if evt.key() == QtCore.Qt.Key_Escape: self.__idx_handle_sel = None elif self.__idx_handle_sel is not None: if evt.key() in (QtCore.Qt.Key_Delete, QtCore.Qt.Key_Backspace) and IDX_IS_LINE(self.__idx_handle_sel): cmd = cmd_set_handle_position(self.model, self.__idx_handle_sel, None) self._parent.undoStack.push(cmd) # self.model.set_handle(self.__idx_handle_sel, None) self.__idx_handle_sel = None # Basic 1-px nudging elif evt.key() in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Right, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down): _nudgetab: Dict[int,Tuple[int, int]] = { QtCore.Qt.Key_Left: (-1, 0), QtCore.Qt.Key_Right: (1, 0), QtCore.Qt.Key_Up: (0, -1), QtCore.Qt.Key_Down: (0, 1), } nudge = _nudgetab[evt.key()] current = self.im2V(self.model.align_handles[self.__idx_handle_sel]) viewspace = self.V2im(current + Vec2.from_mat(nudge)) cmd = cmd_set_handle_position(self.model, self.__idx_handle_sel, viewspace) self._parent.undoStack.push(cmd) # self.model.set_handle(self.__idx_handle_sel, viewspace) self.__ghost_handle = None def next_prev_child(self, next: bool) -> bool: all_handles = self.model.all_handles() step = -1 if next: step = 1 if self.__behave_mode == DM.DRAGGING: return True else: idx = self.__idx_handle_sel if idx is None: return False while 1: idx = (idx + step) % len(all_handles) if all_handles[idx] is not None: self.__idx_handle_sel = idx return True
class RectAlignmentControllerView(BaseToolController, GenModel): changed = QtCore.Signal() def __init__(self, parent, model): super(RectAlignmentControllerView, self).__init__() self._parent = parent self.model_overall = model self.model = model.ra self.__init_interaction() self.gls = None self.active = False idx_handle_sel = mdlacc(None) idx_handle_hover = mdlacc(None) #sel_mode = mdlacc(SEL_MODE_NONE) behave_mode = mdlacc(MODE_NONE) ghost_handle = mdlacc(None) def change(self): self.changed.emit() def __init_interaction(self): # Selection / drag handling self.idx_handle_sel = None #self.sel_mode = SEL_MODE_NONE self.behave_mode = MODE_NONE # ghost handle for showing placement self.ghost_handle = None def initialize(self): self.__init_interaction() self.active = True def finalize(self): self.active = False def initializeGL(self, gls): self.gls = gls # Basic solid-color program self.prog = self.gls.shader_cache.get("vert2", "frag1") self.mat_loc = GL.glGetUniformLocation(self.prog, "mat") self.col_loc = GL.glGetUniformLocation(self.prog, "color") # Build a VBO for rendering square "drag-handles" self.vbo_handles_ar = numpy.ndarray(4, dtype=[("vertex", numpy.float32, 2)]) self.vbo_handles_ar["vertex"] = numpy.array(corners) * HANDLE_HALF_SIZE self.vbo_handles = VBO(self.vbo_handles_ar, GL.GL_STATIC_DRAW, GL.GL_ARRAY_BUFFER) self.vao_handles = VAO() with self.vbo_handles, self.vao_handles: vbobind(self.prog, self.vbo_handles_ar.dtype, "vertex").assign() # Build a VBO/VAO for the perimeter # We don't initialize it here because it is updated every render # 4 verticies for outside perimeter # 6 verticies for each dim self.vbo_per_dim_ar = numpy.zeros(16, dtype=[("vertex", numpy.float32, 2)]) self.vbo_per_dim = VBO(self.vbo_per_dim_ar, GL.GL_DYNAMIC_DRAW, GL.GL_ARRAY_BUFFER) self.vao_per_dim = VAO() with self.vao_per_dim, self.vbo_per_dim: vbobind(self.prog, self.vbo_per_dim_ar.dtype, "vertex").assign() def im2V(self, pt): """Translate Image coordinates to viewport coordinates""" if self.model_overall.view_mode: ph = projectPoint(self.model.image_matrix, pt) return self.viewState.tfW2V(ph) else: return self.viewState.tfW2V(pt) def V2im(self, pt): """ Translate viewport coordinates to image coordinates :param pt: :return: """ world = self.viewState.tfV2W(pt) if self.model_overall.view_mode: inv = numpy.linalg.inv(self.model.image_matrix) return projectPoint(inv, world) else: return Vec2(world) #inv = numpy.linalg.inv(self.model.image_matrix) #return Vec2(inv.dot(pt)[:2]) def gen_dim(self, idx, always_above = True): """ Generate rendering data for the dimension-lines :param idx: :return: """ a = self.im2V(self.model.dim_handles[0 + idx]) b = self.im2V(self.model.dim_handles[1 + idx]) d = b-a delta = (b-a).norm() normal = Point2(rotate(math.pi / 2)[:2,:2].dot(delta)) if always_above: if numpy.cross(Vec2(1,0), normal) > 0: normal = -normal res = numpy.array([ a + normal * 8, a + normal * 20, a + normal * 15, b + normal * 15, b + normal * 8, b + normal * 20, ]) return res def render(self, vs): self.viewState = vs disabled = not self.active or self.model_overall.view_mode # Perimeter is defined by the first 4 handles self.vbo_per_dim_ar["vertex"][:4] = [self.im2V(pt) for pt in self.model.align_handles[:4]] # Generate the dimension lines. For ease of use, we always draw the dim-lines above when dims are manual # or below when dims are unlocked self.vbo_per_dim_ar["vertex"][4:10] = self.gen_dim(0, not self.model.dims_locked) self.vbo_per_dim_ar["vertex"][10:16] = self.gen_dim(2, not self.model.dims_locked) self.vbo_per_dim.set_array(self.vbo_per_dim_ar) # Ugh..... PyOpenGL isn't smart enough to bind the data when it needs to be copied with self.vbo_per_dim: self.vbo_per_dim.copy_data() GL.glDisable(GL.GL_BLEND) # ... and draw the perimeter with self.vao_per_dim, self.prog: GL.glUniformMatrix3fv(self.mat_loc, 1, True, self.viewState.glWMatrix.astype(numpy.float32)) # Draw the outer perimeter if disabled: GL.glUniform4f(self.col_loc, 0.8, 0.8, 0.8, 1) else: GL.glUniform4f(self.col_loc, 0.8, 0.8, 0, 1) GL.glDrawArrays(GL.GL_LINE_LOOP, 0, 4) # Draw the dimensions GL.glUniform4f(self.col_loc, 0.8, 0.0, 0.0, 1) GL.glDrawArrays(GL.GL_LINES, 4, 6) GL.glUniform4f(self.col_loc, 0.0, 0.0, 0.8, 1) GL.glDrawArrays(GL.GL_LINES, 10, 6) if disabled: return # Now draw a handle at each corner with self.vao_handles, self.prog: for n, i in enumerate(self.model.align_handles): # skip nonexistent handles if i is None: continue is_anchor = IDX_IS_ANCHOR(n) corner_pos = self.im2V(i) if disabled: color = [0.8, 0.8, 0.8, 1] elif self.idx_handle_sel == n: color = [1, 1, 1, 1] elif self.idx_handle_hover == n: color = [1, 1, 0, 1] else: color = [0.8, 0.8, 0, 0.5] self.render_handle(corner_pos, color, is_anchor, True) if self.idx_handle_sel == n: self.render_handle(corner_pos, [0,0,0,1], is_anchor, False) if self.ghost_handle is not None: self.render_handle(self.ghost_handle,[0.8, 0.8, 0, 0.5], True) if not self.model.dims_locked: for n, i in enumerate(self.model.dim_handles): handle_pos = self.im2V(i) if n == self.idx_handle_sel: color = [1, 1, 1, 1] if n < 2: color = [0.8, 0.0, 0.0, 1] else: color = [0.0, 0.0, 0.8, 1] self.render_handle(handle_pos, color, False, True) if self.idx_handle_sel == n: self.render_handle(corner_pos, [0,0,0,1], is_anchor, False) def render_handle(self, position, color, diagonal=False, filled=False): if diagonal: r = rotate(math.pi/4) else: r = numpy.identity(3) m = self.viewState.glWMatrix.dot(translate(*position).dot(r)) GL.glUniformMatrix3fv(self.mat_loc, 1, True, m.astype(numpy.float32)) GL.glUniform4f(self.col_loc, *color) GL.glDrawArrays(GL.GL_TRIANGLE_FAN if filled else GL.GL_LINE_LOOP, 0, 4) def get_handle_for_mouse(self, pos): for n, handle in enumerate(self.model.all_handles()): if handle is None: continue # get the pix-wise BBOX of the handle p = self.im2V(handle) r = QtCore.QRect(p[0], p[1], 0, 0) r.adjust(-HANDLE_HALF_SIZE, -HANDLE_HALF_SIZE, HANDLE_HALF_SIZE, HANDLE_HALF_SIZE) # If event inside the bbox if r.contains(pos): return n return None def get_line_query_for_mouse(self, pos): for n, (p1, p2) in enumerate(self.model.line_iter()): p1_v = self.im2V(p1) p2_v = self.im2V(p2) p, d = project_point_line(pos, p1_v, p2_v) if p is not None and d < HANDLE_HALF_SIZE: return n, p return None, None def mousePressEvent(self, event): disabled = not self.active or self.model_overall.view_mode if disabled: return False handle = self.get_handle_for_mouse(event.pos()) if event.button() == QtCore.Qt.LeftButton and event.modifiers() & ADD_MODIFIER: idx, p = self.get_line_query_for_mouse(event.pos()) if idx is not None: anchors = self.model.get_anchors(idx) if len(anchors) < 2: p = self.V2im(p) idx = new_anchor_index(self.model, idx) cmd = cmd_set_handle_position(self.model, idx, p) self._parent.undoStack.push(cmd) self.idx_handle_sel = idx self.idx_handle_hover = None elif event.button() == QtCore.Qt.LeftButton and event.modifiers() & DEL_MODIFIER and ( handle is not None and handle >= 4): cmd = cmd_set_handle_position(self.model, handle, None) self._parent.undoStack.push(cmd) #self.model.set_handle(handle, None) self.idx_handle_sel = None self.idx_handle_hover = None elif event.button() == QtCore.Qt.LeftButton: self.idx_handle_sel = handle self.idx_handle_hover = None if handle is not None: self.behave_mode = MODE_DRAGGING else: return False return True def mouseReleaseEvent(self, event): disabled = not self.active or self.model_overall.view_mode if disabled: return False if event.button() == QtCore.Qt.LeftButton and self.behave_mode == MODE_DRAGGING: self.behave_mode = MODE_NONE else: return False return True def mouseMoveEvent(self, event): disabled = not self.active or self.model_overall.view_mode if disabled: return False needs_update = False idx = self.get_handle_for_mouse(event.pos()) if self.ghost_handle is not None: self.ghost_handle = None if self.behave_mode == MODE_NONE: if idx is not None: self.idx_handle_hover = idx else: self.idx_handle_hover = None if event.modifiers() & ADD_MODIFIER: line_idx, pos = self.get_line_query_for_mouse(event.pos()) if line_idx is not None: self.ghost_handle = pos elif self.behave_mode == MODE_DRAGGING: w_pos = self.V2im(Vec2(event.pos())) cmd = cmd_set_handle_position(self.model, self.idx_handle_sel, w_pos, merge=True) self._parent.undoStack.push(cmd) #self.model.set_handle(self.idx_handle_sel, w_pos) return False def focusOutEvent(self, evt): self.idx_handle_sel = None def keyPressEvent(self, evt): disabled = not self.active or self.model_overall.view_mode if disabled: return False if evt.key() == QtCore.Qt.Key_Escape: self.idx_handle_sel = None elif self.idx_handle_sel is not None: if evt.key() in (QtCore.Qt.Key_Delete, QtCore.Qt.Key_Backspace) and IDX_IS_ANCHOR(self.idx_handle_sel): cmd = cmd_set_handle_position(self.model, self.idx_handle_sel, None) self._parent.undoStack.push(cmd) #self.model.set_handle(self.idx_handle_sel, None) self.idx_handle_sel = None # Basic 1-px nudging elif evt.key() in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Right, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down): nudge = { QtCore.Qt.Key_Left: (-1, 0), QtCore.Qt.Key_Right: ( 1, 0), QtCore.Qt.Key_Up: ( 0, -1), QtCore.Qt.Key_Down: ( 0, 1), }[evt.key()] current = Vec2(self.im2V(self.model.align_handles[self.idx_handle_sel])) viewspace = self.V2im(current + nudge) cmd = cmd_set_handle_position(self.model, self.idx_handle_sel, viewspace) self._parent.undoStack.push(cmd) #self.model.set_handle(self.idx_handle_sel, viewspace) self.ghost_handle = None def next_prev_child(self, next): all_handles = self.model.all_handles() step = -1 if next: step = 1 if self.behave_mode == MODE_DRAGGING: return True else: idx = self.idx_handle_sel if idx == None: return False while 1: idx = (idx + step) % len(all_handles) if all_handles[idx] is not None: self.idx_handle_sel = idx return True
class TextBatch: __tag = namedtuple("__tag", ['mat', 'inf']) def __init__(self, tr: 'TextRender') -> None: self.__text_render = tr self.__elem_count = 0 self.__color = (255, COL_TEXT, 0, 0) def initializeGL(self) -> None: self.vbo = VBO( numpy.ndarray([], dtype=self.__text_render.buffer_dtype), GL.GL_STATIC_DRAW, GL.GL_ARRAY_BUFFER) self.vao = VAO() self._va = VA_tex(1024) with self.vao, self.vbo: self.__text_render.b1.assign() self.__text_render.b2.assign() def restart(self) -> None: self.__strs: List[TextBatch.__tag] = [] # TEMP def get_string(self, text: str) -> '_StringMetrics': """ :param text: :return: :returns: _StringMetrics """ return self.__text_render.getStringMetrics(text) def add(self, mat: 'npt.NDArray[numpy.float64]', str_info: '_StringMetrics') -> None: """ Queues a text string to be rendered in the batch :param mat: location matrix :param str_info: :type str_info: _StringMetrics :return: """ self.__strs.append(TextBatch.__tag(mat, str_info)) def prepare(self) -> None: self.__text_render.updateTexture() self._va.clear() for mat, str_info in self.__strs: self._va.extend_project(mat, str_info.arr) self.vbo.set_array(self._va.buffer()[:]) self.vbo.bind() self.__elem_count = self._va.count() def render(self, mat: 'npt.NDArray[numpy.float64]') -> None: with self.__text_render.sdf_shader.program, self.__text_render.tex.on( GL.GL_TEXTURE_2D), self.vao: GL.glUniform1i(self.__text_render.sdf_shader.uniforms.tex1, 0) GL.glUniformMatrix3fv(self.__text_render.sdf_shader.uniforms.mat, 1, True, mat.astype(numpy.float32)) GL.glUniform4ui(self.__text_render.sdf_shader.uniforms.layer_info, *self.__color) GL.glDrawArrays(GL.GL_TRIANGLES, 0, self.__elem_count)
class THRenderer: def __init__(self, parent_view: 'BoardViewWidget') -> None: self.parent = parent_view def initializeGL(self, glshared: 'GLShared') -> None: self._filled_shader = glshared.shader_cache.get( "via_filled_vertex_shader", "via_filled_fragment_shader") self._outline_shader = glshared.shader_cache.get( "via_outline_vertex_shader", "basic_fill_frag") # Build geometry for filled rendering using the frag shader for circle borders filled_points = [ ((-1, -1), ), ((1, -1), ), ((-1, 1), ), ((1, 1), ), ] ar = numpy.array(filled_points, dtype=[("vertex", numpy.float32, 2)]) self._sq_vbo = VBO(ar, GL.GL_STATIC_DRAW) GL.glObjectLabel(GL.GL_BUFFER, int(self._sq_vbo), -1, "Via SQ VBO") # Build geometry for outline rendering outline_points = [] for i in numpy.linspace(0, math.pi * 2, N_OUTLINE_SEGMENTS, False): outline_points.append(((math.cos(i), math.sin(i)), )) outline_points_array = numpy.array(outline_points, dtype=[("vertex", numpy.float32, 2) ]) self._outline_vbo = VBO(outline_points_array, GL.GL_STATIC_DRAW) GL.glObjectLabel(GL.GL_BUFFER, int(self._sq_vbo), -1, "Via Outline VBO") self.__dtype = numpy.dtype([ ("pos", numpy.float32, 2), ("r", numpy.float32), ("r_inside_frac_sq", numpy.float32), ]) self.__filled_vao = VAO() self.__outline_vao = VAO() with self.__filled_vao, self._sq_vbo: VBOBind(self._filled_shader.program, self._sq_vbo.data.dtype, "vertex").assign() # Use a fake array to get a zero-length VBO for initial binding filled_instance_array = numpy.ndarray(0, dtype=self.__dtype) self.filled_instance_vbo = VBO(filled_instance_array) GL.glObjectLabel(GL.GL_BUFFER, int(self._sq_vbo), -1, "Via Filled Instance VBO") with self.__filled_vao, self.filled_instance_vbo: VBOBind(self._filled_shader.program, self.__dtype, "pos", div=1).assign() VBOBind(self._filled_shader.program, self.__dtype, "r", div=1).assign() VBOBind(self._filled_shader.program, self.__dtype, "r_inside_frac_sq", div=1).assign() with self.__outline_vao, self._outline_vbo: VBOBind(self._outline_shader.program, self._outline_vbo.data.dtype, "vertex").assign() # Build instance for outline rendering # We don't have an inner 'r' for this because we just do two instances per vertex # Use a fake array to get a zero-length VBO for initial binding outline_instance_array = numpy.ndarray(0, dtype=self.__dtype) self.outline_instance_vbo = VBO(outline_instance_array) GL.glObjectLabel(GL.GL_BUFFER, int(self.outline_instance_vbo), -1, "Via Outline Instance VBO") with self.__outline_vao, self.outline_instance_vbo: VBOBind(self._outline_shader.program, self.__dtype, "pos", div=1).assign() VBOBind(self._outline_shader.program, self.__dtype, "r", div=1).assign() def render_filled(self, mat: 'npt.NDArray[numpy.float64]', va: 'VA_xy', color: Tuple[float, float, float] = COL_VIA) -> None: self.filled_instance_vbo.set_array(va.buffer()[:]) try: with self._filled_shader.program: with self.__filled_vao: with self.filled_instance_vbo: with self._sq_vbo: GL.glUniformMatrix3fv( self._filled_shader.uniforms.mat, 1, True, mat.astype(numpy.float32)) GL.glUniform1ui(self._filled_shader.uniforms.color, color) GL.glDrawArraysInstancedBaseInstance( GL.GL_TRIANGLE_STRIP, 0, 4, va.count(), 0) except OpenGL.error.GLError as e: print("Threw OGL error:", e) def render_outlines(self, mat: 'npt.NDArray[numpy.float64]', va: 'VA_xy') -> None: if not va.count(): return self.outline_instance_vbo.set_array(va.buffer()[:]) self.outline_instance_vbo.bind() with self._outline_shader.program, self.__outline_vao, self.outline_instance_vbo, self._sq_vbo: GL.glUniformMatrix3fv(self._outline_shader.uniforms.mat, 1, True, mat.astype(numpy.float32)) GL.glUniform4ui(self._outline_shader.uniforms.layer_info, 255, COL_SEL, 0, 0) GL.glDrawArraysInstanced(GL.GL_LINE_LOOP, 0, N_OUTLINE_SEGMENTS, va.count())
class TraceRender: def __init__(self, parent_view: 'BoardViewWidget') -> None: self.parent = parent_view self.restart() def initializeGL(self, gls: 'GLShared') -> None: # Build trace vertex VBO and associated vertex data dtype = [("vertex", numpy.float32, 2), ("ptid", numpy.uint32)] self.working_array = numpy.zeros(NUM_ENDCAP_SEGMENTS * 2 + 2, dtype=dtype) self.trace_vbo = VBO(self.working_array, GL.GL_DYNAMIC_DRAW) GL.glObjectLabel(GL.GL_BUFFER, int(self.trace_vbo), -1, "Thickline Trace VBO") # Generate geometry for trace and endcaps # ptid is a variable with value 0 or 1 that indicates which endpoint the geometry is associated with self.__build_trace() self.__attribute_shader_vao = VAO( debug_name="Thickline attribute shader VAO") shader = gls.shader_cache.get("line_vertex_shader", "basic_fill_frag", defines={"INPUT_TYPE": "in"}) assert shader is not None self.__attribute_shader: 'EnhShaderProgram' = shader # Now we build an index buffer that allows us to render filled geometry from the same # VBO. arr = [] for i in range(NUM_ENDCAP_SEGMENTS - 1): arr.append(0) arr.append(i + 2) arr.append(i + 3) for i in range(NUM_ENDCAP_SEGMENTS - 1): arr.append(1) arr.append(i + NUM_ENDCAP_SEGMENTS + 2) arr.append(i + NUM_ENDCAP_SEGMENTS + 3) arr.append(2) arr.append(2 + NUM_ENDCAP_SEGMENTS - 1) arr.append(2 + NUM_ENDCAP_SEGMENTS) arr.append(2 + NUM_ENDCAP_SEGMENTS) arr.append(2 + NUM_ENDCAP_SEGMENTS * 2 - 1) arr.append(2) arr2 = numpy.array(arr, dtype=numpy.uint32) self.index_vbo = VBO(arr2, target=GL.GL_ELEMENT_ARRAY_BUFFER) GL.glObjectLabel(GL.GL_BUFFER, int(self.index_vbo), -1, "Thickline Index VBO") self.instance_dtype = numpy.dtype([ ("pos_a", numpy.float32, 2), ("pos_b", numpy.float32, 2), ("thickness", numpy.float32), # ("color", numpy.float32, 4) ]) # Use a fake array to get a zero-length VBO for initial binding instance_array: 'npt.NDArray[Any]' = numpy.ndarray( 0, dtype=self.instance_dtype) self.instance_vbo = VBO(instance_array) GL.glObjectLabel(GL.GL_BUFFER, int(self.instance_vbo), -1, "Thickline Instance VBO") with self.__attribute_shader_vao, self.trace_vbo: VBOBind(self.__attribute_shader.program, self.trace_vbo.dtype, "vertex").assign() VBOBind(self.__attribute_shader.program, self.trace_vbo.dtype, "ptid").assign() with self.__attribute_shader_vao, self.instance_vbo: self.__bind_pos_a = VBOBind(self.__attribute_shader.program, self.instance_dtype, "pos_a", div=1) self.__bind_pos_b = VBOBind(self.__attribute_shader.program, self.instance_dtype, "pos_b", div=1) self.__bind_thickness = VBOBind(self.__attribute_shader.program, self.instance_dtype, "thickness", div=1) # vbobind(self.__attribute_shader, self.instance_dtype, "color", div=1).assign() self.__base_rebind(0) self.index_vbo.bind() def __base_rebind(self, base: int) -> None: self.__bind_pos_a.assign(base) self.__bind_pos_b.assign(base) self.__bind_thickness.assign(base) def restart(self) -> None: pass def __build_trace(self) -> None: # Update trace VBO self.working_array["vertex"][0] = (0, 0) self.working_array["ptid"][0] = 0 self.working_array["vertex"][1] = (0, 0) self.working_array["ptid"][1] = 1 end = Vec2(1, 0) for i in range(0, NUM_ENDCAP_SEGMENTS): theta = math.pi * i / (NUM_ENDCAP_SEGMENTS - 1) + math.pi / 2 m = rotate(theta).dot(end.homol()) self.working_array["vertex"][2 + i] = m[:2] self.working_array["ptid"][2 + i] = 0 self.working_array["vertex"][2 + i + NUM_ENDCAP_SEGMENTS] = -m[:2] self.working_array["ptid"][2 + i + NUM_ENDCAP_SEGMENTS] = 1 # Force data copy self.trace_vbo.bind() self.trace_vbo.set_array(self.working_array) def __render_va_inner(self, col: int, is_outline: bool, first: int, count: int) -> None: GL.glUniform4ui(self.__attribute_shader.uniforms.layer_info, 255, col, 0, 0) if has_base_instance: # Many instances backport glDrawElementsInstancedBaseInstance # This is faster than continually rebinding, so support if possible if not is_outline: # filled traces come first in the array GL.glDrawElementsInstancedBaseInstance(GL.GL_TRIANGLES, TRIANGLES_SIZE, GL.GL_UNSIGNED_INT, ctypes.c_void_p(0), count, first) else: # Then outline traces. We reuse the vertex data for the outside GL.glDrawArraysInstancedBaseInstance(GL.GL_LINE_LOOP, 2, NUM_ENDCAP_SEGMENTS * 2, count, first) else: with self.instance_vbo: if not is_outline: # filled traces come first in the array self.__base_rebind(first) GL.glDrawElementsInstanced(GL.GL_TRIANGLES, TRIANGLES_SIZE, GL.GL_UNSIGNED_INT, ctypes.c_void_p(0), count) else: self.__base_rebind(first) # Then outline traces. We reuse the vertex data for the outside GL.glDrawArraysInstanced(GL.GL_LINE_LOOP, 2, NUM_ENDCAP_SEGMENTS * 2, count) def render_va(self, va: 'VA_thickline', mat: 'npt.NDArray[numpy.float64]', col: int, is_outline: bool = False, first: int = 0, count: 'Optional[int]' = None) -> None: GL.glPushDebugGroup(GL.GL_DEBUG_SOURCE_APPLICATION, 0, -1, "Thickline Draw") assert self.instance_dtype.itemsize == va.stride self.instance_vbo.set_array(va.buffer()[:]) if count is None: count = va.count() - first with self.__attribute_shader.program, self.__attribute_shader_vao, self.instance_vbo: GL.glUniformMatrix3fv(self.__attribute_shader.uniforms.mat, 1, True, mat.astype(numpy.float32)) self.__render_va_inner(col, is_outline, first, count) GL.glPopDebugGroup()
uniforms['projection'].update( camera.get_projection_matrix(aspect, zNear, zFar)) uniforms['view'].update(camera.get_view_matrix()) uniforms['objectColor'].update(clothColor) rightShift = grid.numCols * grid.initLength / 2.0 downShift = grid.numRows * grid.initLength / 2.0 uniforms['model'].update(translate(-rightShift, downShift, 0.0)) # compute new parameters grid.get_new_state_and_update(deltaTime) # compute normal gridArray = np.concatenate([grid.pos.squeeze(), grid.normal], axis=2) gridArray = gridArray.flatten().astype(np.float32) # update buffer gridVBO.set_array(gridArray) gridVBO.bind() gridVBO.copy_data() gridVBO.unbind() glBindVertexArray(gridVAO) glDrawElements(GL_TRIANGLES, elementArray.size, GL_UNSIGNED_INT, ctypes.c_void_p(0)) glBindVertexArray(0) # respond key press keyboard_respond_func() # tell glfw to poll and process window events glfw.poll_events() # swap frame buffer glfw.swap_buffers(theWindow)
class TextDrawer: def __init__(self): self.face = None self.textures = dict() # compile rendering program self.renderProgram = GLProgram(_textVertexShaderSource, _textFragmentShaderSource) self.renderProgram.compile_and_link() # make projection uniform self.projectionUniform = GLUniform(self.renderProgram.get_program_id(), 'projection', 'mat4f') self.textColorUniform = GLUniform(self.renderProgram.get_program_id(), 'textColor', 'vec3f') self.textureSizeUniform = GLUniform( self.renderProgram.get_program_id(), 'textureSize', 'vec2f') # create rendering buffer self.vbo = VBO(_get_rendering_buffer(0, 0, 0, 0)) self.vbo.create_buffers() self.vboId = glGenBuffers(1) # initialize VAO self.vao = glGenVertexArrays(1) glBindVertexArray(self.vao) glBindBuffer(GL_ARRAY_BUFFER, self.vboId) self.vbo.bind() self.vbo.copy_data() glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * ctypes.sizeof(ctypes.c_float), ctypes.c_void_p(0)) glEnableVertexAttribArray(0) glVertexAttribPointer( 1, 2, GL_FLOAT, GL_FALSE, 5 * ctypes.sizeof(ctypes.c_float), ctypes.c_void_p(3 * ctypes.sizeof(ctypes.c_float))) glEnableVertexAttribArray(1) # self.vbo.unbind() glBindVertexArray(0) self.zNear = -1.0 self.zFar = 1.0 def delete(self): self.textures.clear() self.face = None self.renderProgram.delete() self.projectionUniform = None self.textColorUniform = None self.textureSizeUniform = None self.vbo.delete() glDeleteVertexArrays(1, [self.vao]) def load_font(self, fontFilename, fontSize): assert os.path.exists(fontFilename) self.textures.clear() self.face = ft.Face(fontFilename) self.face.set_char_size(fontSize) # load all ASCII characters for i in range(128): self.load_character(chr(i)) def load_character(self, character): assert self.face is not None assert len(character) == 1 if character not in self.textures: # load glyph in freetype self.face.load_char(character) ftBitmap = self.face.glyph.bitmap height, width = ftBitmap.rows, ftBitmap.width bitmap = np.array(ftBitmap.buffer, dtype=np.uint8).reshape( (height, width)) # create texture texture = _create_text_texture(bitmap) # add texture object to the dictionary characterSlot = CharacterSlot(texture, self.face.glyph) self.textures[character] = characterSlot def get_character(self, ch): if ch not in self.textures: self.load_character(ch) return self.textures[ch] def _draw_text(self, text, textPos, windowSize, scale, linespread, foreColor): if len(text) == 0: return blendEnabled = glIsEnabled(GL_BLEND) glEnable(GL_BLEND) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) self.renderProgram.use() glBindVertexArray(self.vao) glActiveTexture(GL_TEXTURE0) foreColor = np.asarray(foreColor, np.float32) self.textColorUniform.update(foreColor) projectionMat = orthographic_projection(0.0, windowSize[0], 0.0, windowSize[1], self.zNear, self.zFar) self.projectionUniform.update(projectionMat) lineY = textPos[1] nowX = textPos[0] # split text into lines lines = text.split('\n') for line in lines: if len(line) > 0: # analyze this line bearings = [] for ch in line: charSlot = self.get_character(ch) bearings.append(charSlot.bearing[1] * scale[1]) maxBearings = max(bearings) for ch in line: charSlot = self.get_character(ch) xpos = nowX + charSlot.bearing[0] * scale[0] yLowerd = (maxBearings - charSlot.bearing[1] * scale[1]) ypos = lineY - yLowerd w = charSlot.textureSize[0] * scale[0] h = charSlot.textureSize[1] * scale[1] self.textureSizeUniform.update(np.array((w, h), np.float32)) charSlot.texture.bind() self.vbo.bind() self.vbo.set_array( _get_rendering_buffer(xpos, ypos, w, h, 0.999)) self.vbo.copy_data() self.vbo.unbind() glDrawArrays(GL_TRIANGLES, 0, 6) charSlot.texture.unbind() # the advance is number of 1/64 pixels nowX += (charSlot.advance / 64.0) * scale[0] nowX = textPos[0] yOffset = self.get_character( 'X').textureSize[1] * scale[1] * linespread lineY -= yOffset glBindVertexArray(0) if not blendEnabled: glDisable(GL_BLEND) def draw_text(self, text, textPos, windowSize, color=(1.0, 1.0, 1.0), scale=(1.0, 1.0), linespread=1.5): return self._draw_text(text, textPos, windowSize, scale, linespread, color)
class TextDrawer_Outlined: def __init__(self): self.face = None self.stroker = None self.foreTextures = dict() self.backTextures = dict() # compile rendering program self.renderProgram = GLProgram(_textVertexShaderSource, _textFragmentShaderSource) self.renderProgram.compile_and_link() # make projection uniform self.projectionUniform = GLUniform(self.renderProgram.get_program_id(), 'projection', 'mat4f') self.textColorUniform = GLUniform(self.renderProgram.get_program_id(), 'textColor', 'vec3f') self.textureSizeUniform = GLUniform( self.renderProgram.get_program_id(), 'textureSize', 'vec2f') # create rendering buffer self.vbo = VBO(_get_rendering_buffer(0, 0, 0, 0)) self.vbo.create_buffers() self.vboId = glGenBuffers(1) # initialize VAO self.vao = glGenVertexArrays(1) glBindVertexArray(self.vao) glBindBuffer(GL_ARRAY_BUFFER, self.vboId) self.vbo.bind() self.vbo.copy_data() glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * ctypes.sizeof(ctypes.c_float), ctypes.c_void_p(0)) glEnableVertexAttribArray(0) glVertexAttribPointer( 1, 2, GL_FLOAT, GL_FALSE, 5 * ctypes.sizeof(ctypes.c_float), ctypes.c_void_p(3 * ctypes.sizeof(ctypes.c_float))) glEnableVertexAttribArray(1) # self.vbo.unbind() glBindVertexArray(0) self.zNear = -1.0 self.zFar = 1.0 def delete(self): self.foreTextures.clear() self.backTextures.clear() self.face = None self.stroker = None self.renderProgram.delete() self.projectionUniform = None self.textColorUniform = None self.textureSizeUniform = None self.vbo.delete() glDeleteVertexArrays(1, [self.vao]) def load_font(self, fontFilename, fontSize, outlineSize=2 * 64): assert os.path.exists(fontFilename) self.foreTextures.clear() self.backTextures.clear() self.face = ft.Face(fontFilename) self.face.set_char_size(fontSize) self.outlineSize = outlineSize self.stroker = ft.Stroker() self.stroker.set(outlineSize, ft.FT_STROKER_LINECAPS['FT_STROKER_LINECAP_ROUND'], ft.FT_STROKER_LINEJOINS['FT_STROKER_LINEJOIN_ROUND'], 0) # load all ASCII characters for i in range(128): self.load_character(chr(i)) def load_character(self, character): assert self.face is not None assert len(character) == 1 if character not in self.foreTextures: # load background glyph # the render option will lead to an outline glyph (not rendered) self.face.load_char(character, ft.FT_LOAD_FLAGS['FT_LOAD_DEFAULT']) backGlyph = ft.FT_Glyph() ft.FT_Get_Glyph(self.face.glyph._FT_GlyphSlot, ft.byref(backGlyph)) backGlyph = ft.Glyph(backGlyph) # add border to the glyph error = ft.FT_Glyph_StrokeBorder(ft.byref(backGlyph._FT_Glyph), self.stroker._FT_Stroker, False, False) if error: raise ft.FT_Exception(error) # the render option will lead to a rendered glyph backBitmapGlyph = backGlyph.to_bitmap( ft.FT_RENDER_MODES['FT_RENDER_MODE_NORMAL'], 0) backBitmap = backBitmapGlyph.bitmap backHeight, backWidth = backBitmap.rows, backBitmap.width backBitmap = np.array(backBitmap.buffer, dtype=np.uint8).reshape( (backHeight, backWidth)) backTexture = _create_text_texture(backBitmap) backSlot = CharacterSlot(backTexture, backBitmapGlyph) self.backTextures[character] = backSlot # load foreground glyph self.face.load_char(character, ft.FT_LOAD_FLAGS['FT_LOAD_RENDER']) foreBitmap = self.face.glyph.bitmap foreHeight, foreWidth = foreBitmap.rows, foreBitmap.width foreBitmap = np.array(foreBitmap.buffer, dtype=np.uint8).reshape( (foreHeight, foreWidth)) foreTexture = _create_text_texture(foreBitmap) foreSlot = CharacterSlot(foreTexture, self.face.glyph) self.foreTextures[character] = foreSlot def get_character(self, ch): if ch not in self.foreTextures: self.load_character(ch) return (self.foreTextures[ch], self.backTextures[ch]) def _draw_text(self, text, textPos, windowSize, scale, linespread, foreColor, backColor): if len(text) == 0: return blendEnabled = glIsEnabled(GL_BLEND) glEnable(GL_BLEND) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) self.renderProgram.use() glBindVertexArray(self.vao) glActiveTexture(GL_TEXTURE0) foreColor = np.asarray(foreColor, np.float32) backColor = np.asarray(backColor, np.float32) projectionMat = orthographic_projection(0.0, windowSize[0], 0.0, windowSize[1], self.zNear, self.zFar) self.projectionUniform.update(projectionMat) lineY = textPos[1] nowX = textPos[0] # split text into lines lines = text.split('\n') for line in lines: if len(line) > 0: # analyze this line bearings = [] for ch in line: _, backSlot = self.get_character(ch) bearings.append(backSlot.bearing) minBearings_X = min(zip(*bearings), key=operator.itemgetter(0))[0] * scale[0] maxBearings_Y = max(zip(*bearings), key=operator.itemgetter(1))[1] * scale[1] nowX = -minBearings_X for ch in line: foreSlot, backSlot = self.get_character(ch) # draw the background xpos = nowX + backSlot.bearing[0] * scale[0] yLowerd = (maxBearings_Y - backSlot.bearing[1] * scale[1]) ypos = lineY - yLowerd w = backSlot.textureSize[0] * scale[0] h = backSlot.textureSize[1] * scale[1] self.textureSizeUniform.update(np.array((w, h), np.float32)) backSlot.texture.bind() self.textColorUniform.update(backColor) self.vbo.bind() self.vbo.set_array( _get_rendering_buffer(xpos, ypos, w, h, 0.99)) self.vbo.copy_data() self.vbo.unbind() glDrawArrays(GL_TRIANGLES, 0, 6) backSlot.texture.unbind() # draw the foreground xpos = nowX + foreSlot.bearing[0] * scale[0] yLowerd = (maxBearings_Y - foreSlot.bearing[1] * scale[1]) ypos = lineY - yLowerd w = foreSlot.textureSize[0] * scale[0] h = foreSlot.textureSize[1] * scale[1] foreSlot.texture.bind() self.textColorUniform.update(foreColor) self.vbo.bind() # the foreground is set closer to the screen so that it # is rendered above the background self.vbo.set_array( _get_rendering_buffer(xpos, ypos, w, h, 0.999)) self.vbo.copy_data() self.vbo.unbind() glDrawArrays(GL_TRIANGLES, 0, 6) foreSlot.texture.unbind() # the advance is number of 1/64 pixels nowX += ((foreSlot.advance + 2.0 * self.outlineSize) / 64.0) * scale[0] yOffset = self.get_character( 'X')[1].textureSize[1] * scale[1] * linespread lineY -= yOffset glBindVertexArray(0) if not blendEnabled: glDisable(GL_BLEND) def draw_text(self, text, textPos, windowSize, foreColor=(1.0, 1.0, 1.0), backColor=(0.0, 0.0, 0.0), scale=(1.0, 1.0), linespread=1.5): return self._draw_text(text, textPos, windowSize, scale, linespread, foreColor, backColor)
class DebugRender: def __init__(self, boardview: 'BoardViewWidget') -> None: self.debug_draw = False self.debug_draw_bbox = True self.parent = boardview def initializeGL(self, gls: 'GLShared') -> None: self.shader = gls.shader_cache.get("vert2", "frag1") self.vao = VAO() self.vbo = VBO(bytes(), usage=GL.GL_STREAM_DRAW) # bind the shader with self.vao, self.vbo: loc = GL.glGetAttribLocation(self.shader.program, "vertex") assert loc != -1 GL.glEnableVertexAttribArray(loc) # TODO - HACK. AttribPointer is dependent on ABI data layout. # Should be common on all sane archs, but this should be fetched on demand GL.glVertexAttribPointer(loc, 2, GL.GL_FLOAT, False, 8, ctypes.c_void_p(0)) GL.glVertexAttribDivisor(loc, 0) def render(self) -> None: if not self.debug_draw: return # Create two X-Y vertex buffers; one for selected features, and one non-selected buf = VA_xy(1024) buf_sel = VA_xy(1024) t_debug_bbox = Timer() t_debug_bbox_add = Timer() with t_debug_bbox: if self.debug_draw_bbox: # Build a list of all bboxes we're going to draw bboxes = [] for i in self.parent.getVisible(): bboxes.append((i.bbox, i in self.parent.selectionList)) if isinstance(i, Component): for j in i.get_pads(): bboxes.append((j.bbox, j in self.parent.selectionList)) with t_debug_bbox_add: # Add bboxes to buffers for bbox, selected in bboxes: cx = (bbox.left + bbox.right) / 2 cy = (bbox.bottom + bbox.top) / 2 h = bbox.top - bbox.bottom w = bbox.right - bbox.left dest = buf if selected: dest = buf_sel dest.add_aligned_box(cx, cy, w, h) t_debug_draw = Timer() with t_debug_draw: # TODO: HACK # PyOpenGL doesn't know how to deal with a cffi buffer buf_r = buf.buffer()[:] + buf_sel.buffer()[:] self.vbo.set_array(buf_r, None) # Now render the two buffers with self.shader.program, self.vao, self.vbo: GL.glUniformMatrix3fv( self.shader.uniforms.mat, 1, True, self.parent.viewState.glMatrix.astype(numpy.float32)) GL.glUniform4f(self.shader.uniforms.color, 255, 0, 255, 255) GL.glDrawArrays(GL.GL_LINES, 0, buf.count()) GL.glUniform4f(self.shader.uniforms.color, 255, 255, 255, 255) GL.glDrawArrays(GL.GL_LINES, buf.count(), buf_sel.count()) print("debug draw time: %f(%f) %f" % (t_debug_bbox.interval, t_debug_bbox_add.interval, t_debug_draw.interval))