def _magnitude(arr):
def _normalize(arry):
def scale_transform(x, y, z=1):
def translation_matrix(x, y, z=0):
def rotation_matrix(axis, angle):
def euler_rotation_matrix(heading, attitude, bank):
def triple_axis_rotation_matrix(x, y, z):
def look_at(eye, at, up):
def perspective_matrix(field_of_view, aspect_ratio, near_plane, far_plane): Z_EPSILON = 2 class Geometry:
    """
    Geometry class for all 3D shapes

:param detail_x: number of triangle subdivisions in x-dimension
:type detail_x: integer

:param detail_y: number of triangle subdivisions in y-dimension
:type detail_y: integer

    """

    def __init__(self, detail_x=1, detail_y=1):
        self.vertices = []

        self.line_vertices = []

        self.line_normals = []

        self.vertex_normals = []

        self.faces = []

        self.uvs = []
        # a 2D array containing edge connectivity pattern for create line vertices
        # based on faces for most objects
        self.edges = []
        self.detail_x = detail_x
        self.detail_y = detail_y

        self.stroke_indices = []

        self.matrix = np.identity(4)
        self.material = p5.renderer.style.material

    def reset(self):
        """
        Reset geometry parameters
        """
        self.vertices = []
        self.line_vertices = []
        self.line_normals = []
        self.vertex_normals = []
        self.faces = []
        self.uvs = []
        self.edges = []

    def compute_faces(self):
        """
        Adds the faces for the geometry for predefined order of vertices
        """
        self.faces = []
        sliceCount = self.detail_x + 1
        for i in range(self.detail_y):
            for j in range(self.detail_x):
                a = i * sliceCount + j
                b = i * sliceCount + j + 1
                c = (i + 1) * sliceCount + j + 1
                d = (i + 1) * sliceCount + j
                self.faces.append([a, b, d])
                self.faces.append([d, b, c])

    def make_triangle_edges(self):
        """
        Adds the edges to the geometry based on the faces
        """
        self.edges = []
        for j in range(len(self.faces)):
            self.edges.append([self.faces[j][0], self.faces[j][1]])
            self.edges.append([self.faces[j][1], self.faces[j][2]])
            self.edges.append([self.faces[j][2], self.faces[j][0]])

    def get_face_normal(self, faceId):
        """
        Returns the normal for a given face
        """
        face = self.faces[faceId]
        vA = np.array(self.vertices[face[0]])
        vB = np.array(self.vertices[face[1]])
        vC = np.array(self.vertices[face[2]])
        ab = vB - vA
        ac = vC - vA
        n = np.cross(ab, ac)
        ln = np.linalg.norm(n)

        sinAlpha = ln / (np.linalg.norm(ab) * np.linalg.norm(ac))
        if sinAlpha > 1:
            sinAlpha = 1

        return n * math.sin(sinAlpha) / ln

    def compute_normals(self):
        """
        Compute normals for every vertex
        """
        self.vertex_normals = []
        for iv in range(len(self.vertices)):
            self.vertex_normals.append([0, 0, 0])

        for f in range(len(self.faces)):
            face = self.faces[f]
            face_normal = self.get_face_normal(f)

            for fv in range(3):
                vertex_index = face[fv]
                self.vertex_normals[vertex_index] += face_normal

        for iv in range(len(self.vertices)):
            self.vertex_normals[iv] = self.vertex_normals[iv] / \
                np.linalg.norm(self.vertex_normals[iv])

    def edges_to_vertices(self):
        self.line_vertices = []
        self.line_normals = []

        for i in range(len(self.edges)):
            begin = self.vertices[self.edges[i][0]]
            end = self.vertices[self.edges[i][1]]

            direction = np.array(end) - np.array(begin)
            direction = direction / np.linalg.norm(direction)
            direction = direction.tolist()

            a = begin
            b = begin
            c = end
            d = end
            dirAdd = direction
            dirSub = direction
            dirAdd.append(1)
            dirSub.append(-1)

            self.line_normals.extend(
                [dirAdd, dirSub, dirAdd, dirAdd, dirSub, dirSub])
            self.line_vertices.extend([a, b, c, c, b, d]) class PShape:
    """Custom shape class for p5.

    :param vertices: List of (polygonal) vertices for the shape.
    :type vertices: list | np.ndarray

    :param fill_color: Fill color of the shape (default: 'auto' i.e.,
        the current renderer fill)
    :type fill_color: 'auto' | None | tuple | p5.Color

    :param stroke_color: Stroke color of the shape (default: 'auto'
        i.e., the current renderer stroke color)
    :type stroke_color: 'auto' | None | tuple | p5.color

    :param visible: toggles shape visibility (default: False)
    :type visible: bool

    :param children: List of sub-shapes for the current shape
        (default: [])
    :type children: list

    """

    def __init__(self, fill_color='auto',
                 stroke_color='auto', stroke_weight="auto",
                 stroke_join="auto", stroke_cap="auto",
                 visible=False,
                 children=None, contours=tuple(), vertices=tuple(), shape_type=SType.TESS):
        # basic properties of the shape
        self._fill = None
        self._stroke = None
        self._stroke_weight = None
        self._stroke_cap = None
        self._stroke_join = None

        self._matrix = np.identity(4)
        self._transform_matrix = np.identity(4)
        self._transformed_draw_vertices = None

        # a flag to check if the shape is being edited right now.
        self._in_edit_mode = False

        self.fill = fill_color
        self.stroke = stroke_color
        self.stroke_weight = stroke_weight
        self.stroke_cap = stroke_cap
        self.stroke_join = stroke_join

        self.children = children or []
        self.visible = visible

        self.vertices = list(vertices)
        self.shape_type = shape_type
        self.contours = [list(c) for c in contours]  # List of all contours

    def _set_color(self, name, value=None):
        color = None

        if isinstance(value, Color):  # Is Color, no need to parse
            color = value
        else:  # Not Color, attempt to parse it
            if name == 'stroke' and p5.renderer.style.stroke_enabled:
                color = Color(*p5.renderer.style.stroke_color,
                              color_mode='RGBA', normed=True)
            if name == 'fill' and p5.renderer.style.fill_enabled:
                color = Color(*p5.renderer.style.fill_color,
                              color_mode='RGBA', normed=True)

        if name == 'stroke':
            self._stroke = color
        elif name == 'fill':
            self._fill = color

    @property
    def fill(self):
        return self._fill

    @fill.setter
    def fill(self, new_color):
        self._set_color('fill', new_color)

    @property
    def stroke(self):
        return self._stroke

    @stroke.setter
    def stroke(self, new_color):
        self._set_color('stroke', new_color)

    @property
    def stroke_weight(self):
        return self._stroke_weight

    @stroke_weight.setter
    def stroke_weight(self, stroke):
        if stroke == "auto":
            self._stroke_weight = p5.renderer.stroke_weight
        else:
            self._stroke_weight = stroke

    @property
    def stroke_join(self):
        return self._stroke_join

    @stroke_join.setter
    def stroke_join(self, stroke):
        if stroke == "auto":
            self._stroke_join = p5.renderer.stroke_join
        else:
            self._stroke_join = stroke

    @property
    def stroke_cap(self):
        return self._stroke_cap

    @stroke_cap.setter
    def stroke_cap(self, stroke):
        if stroke == "auto":
            self._stroke_cap = p5.renderer.stroke_cap
        else:
            self._stroke_cap = stroke

    @contextlib.contextmanager
    def edit(self, reset=True):
        """Put the shape in edit mode.

        :param reset: Toggles whether the shape should be "reset"
            during editing. When set to `True` all existing shape
            vertices are cleared. When set to `False` the new vertices
            are appended at the end of the existing vertex list.
            (default: True)
        :type reset: bool

        :raises ValueError: if the shape is already being edited.

        """
        if self._in_edit_mode:
            raise ValueError("Shape is being edited already")
        if reset:
            self.vertices = []
            self.contours = []
        self._in_edit_mode = True
        yield
        self._in_edit_mode = False

    @_ensure_editable
    def add_vertex(self, vertex):
        """Add a vertex to the current shape

        :param vertex: The (next) vertex to add to the current shape.
        :type vertex: tuple | list | p5.Vector | np.ndarray
        """
        self.vertices.append(Point(*vertex))

    @_ensure_editable
    def update_vertex(self, idx, vertex):
        """Edit an individual vertex.

        :param idx: index of the vertex to be edited
        :type idx: int

        :param vertex: The (next) vertex to add to the current shape.
        :type vertex: tuple | list | p5.Vector | np.ndarray
        """
        self.vertices[idx] = Point(*vertex)

    def add_child(self, child):
        """Add a child shape to the current shape

        :param child: Child to be added
        :type child: PShape
        """
        self.children.append(child)

    def transform_matrix(self, mat):
        self._transform_matrix = mat

    @property
    def child_count(self):
        """Number of children.

        :returns: The current number of children.
        :rtype: int
        """
        return len(self.children)

    def apply_matrix(self, mat):
        """Apply the given transformation matrix to the shape.

        :param mat: the 4x4 matrix to be applied to the current shape.
        :type mat: (4, 4) np.ndarray

        """
        self._matrix = self._matrix.dot(mat)

    @_call_on_children
    def apply_transform_matrix(self, mat):
        self._matrix = self._matrix.dot(mat)

    @_call_on_children
    def reset_matrix(self):
        """Reset the transformation matrix associated with the shape.

        """
        self._matrix = np.identity(4)

    @_call_on_children
    @_apply_transform
    def translate(self, x, y, z=0):
        """Translate the shape origin to the given location.

        :param x: The displacement amount in the x-direction (controls
            the left/right displacement)
        :type x: int

        :param y: The displacement amount in the y-direction (controls
            the up/down displacement)
        :type y: int

        :param z: The displacement amount in the z-direction (0 by
            default). This controls the displacement away-from/towards
            the screen.
        :type z: int

        :returns: The translation matrix applied to the transform
            matrix.
        :rtype: np.ndarray

        """
        tmat = matrix.translation_matrix(x, y, z)
        return tmat

    @_call_on_children
    @_apply_transform
    def rotate(self, theta, axis=(0, 0, 1)):
        """Rotate the shape by the given angle along the given axis.

        :param theta: The angle by which to rotate (in radians)
        :type theta: float

        :param axis: The axis along which to rotate (defaults to the
            z-axis)
        :type axis: np.ndarray | list

        :returns: The rotation matrix used to apply the
            transformation.
        :rtype: np.ndarray

        """
        axis = np.array(axis[:])
        tmat = matrix.rotation_matrix(axis, theta)
        return tmat

    def rotate_x(self, theta):
        """Rotate the shape along the x axis.

        :param theta: angle by which to rotate (in radians)
        :type theta: float

        :returns: The rotation matrix used to apply the
            transformation.
        :rtype: np.ndarray

        """
        return self.rotate(theta, axis=[1, 0, 0])

    def rotate_y(self, theta):
        """Rotate the shape along the y axis.

        :param theta: angle by which to rotate (in radians)
        :type theta: float

        :returns: The rotation matrix used to apply the
             transformation.
        :rtype: np.ndarray

        """
        return self.rotate(theta, axis=[0, 1, 0])

    def rotate_z(self, theta):
        """Rotate the shape along the z axis.

        :param theta: angle by which to rotate (in radians)
        :type theta: float

        :returns: The rotation matrix used to apply the
            transformation.
        :rtype: np.ndarray

        """
        return self.rotate(theta)

    @_call_on_children
    @_apply_transform
    def scale(self, sx, sy=None, sz=None):
        """Scale the shape by the given factor.

        :param sx: scale factor along the x-axis.
        :type sx: float

        :param sy: scale factor along the y-axis (defaults to None)
        :type sy: float

        :param sz: scale factor along the z-axis (defaults to None)
        :type sz: float

        :returns: The transformation matrix used to appy the
            transformation.
        :rtype: np.ndarray

        """
        if sy is None and sz is None:
            sy = sx
            sz = sx
        elif sz is None:
            sz = 1
        tmat = matrix.scale_transform(sx, sy, sz)
        return tmat

    @_call_on_children
    @_apply_transform
    def shear_x(self, theta):
        """Shear shape along the x-axis.

        :param theta: angle to shear by (in radians)
        :type theta: float

        :returns: The shear matrix used to apply the tranformation.
        :rtype: np.ndarray

        """
        shear_mat = np.identity(4)
        shear_mat[0, 1] = np.tan(theta)
        return shear_mat

    @_call_on_children
    @_apply_transform
    def shear_y(self, theta):
        """Shear shape along the y-axis.

        :param theta: angle to shear by (in radians)
        :type theta: float

        :returns: The shear matrix used to apply the transformation.
        :rtype: np.ndarray

        """
        shear_mat = np.identity(4)
        shear_mat[1, 0] = np.tan(theta)
        return shear_mat def translation_matrix(x, y, z=0):
    """Return a new translation matrix.

    :param x: translation in the x-direction.
    :type x: numeric

    :param y: translation in the y-direction.
    :type y: numeric

    :param z: translation in the z-direction.
    :type z: numeric (defaults to 0)

    :returns: A transform matrix with the given translation applied to
        it
    :rtype: np.ndarray

    """
    translate_matrix = np.identity(4)
    translate_matrix[0, -1] = x
    translate_matrix[1, -1] = y
    translate_matrix[2, -1] = z
    return translate_matrix class OpenGLRenderer(ABC):
    def __init__(self, src_fbuffer, src_default):
        self.default_prog = None
        self.fbuffer_prog = None

        self.fbuffer = None
        self.fbuffer_tex_front = None
        self.fbuffer_tex_back = None

        self.vertex_buffer = None
        self.index_buffer = None

        # Renderer Globals: STYLE/MATERIAL PROPERTIES
        #
        self.style = Style()

        # Renderer Globals: Curves
        self.stroke_weight = 1
        self.stroke_cap = ROUND
        self.stroke_join = MITER

        # Renderer Globals
        # VIEW MATRICES, ETC
        #
        self.viewport = None
        self.texture_viewport = None
        self.transform_matrix = np.identity(4)
        self.projection_matrix = np.identity(4)

        # Renderer Globals: RENDERING
        self.draw_queue = []

        # Shaders
        self.fbuffer_prog = Program(src_fbuffer.vert, src_fbuffer.frag)
        self.default_prog = Program(src_default.vert, src_default.frag)

    def initialize_renderer(self):
        self.fbuffer = FrameBuffer()

        vertices = np.array([[-1.0, -1.0],
                             [+1.0, -1.0],
                             [-1.0, +1.0],
                             [+1.0, +1.0]],
                            np.float32)
        texcoords = np.array([[0.0, 0.0],
                              [1.0, 0.0],
                              [0.0, 1.0],
                              [1.0, 1.0]],
                             dtype=np.float32)

        self.fbuf_vertices = VertexBuffer(data=vertices)
        self.fbuf_texcoords = VertexBuffer(data=texcoords)

        self.fbuffer_prog['texcoord'] = self.fbuf_texcoords
        self.fbuffer_prog['position'] = self.fbuf_vertices

        self.vertex_buffer = VertexBuffer()
        self.index_buffer = IndexBuffer()

    def render_default(self, draw_type, draw_queue):
        # 1. Get the maximum number of vertices persent in the shapes
        # in the draw queue.
        #
        if len(draw_queue) == 0:
            return

        num_vertices = 0
        for vertices, _, _ in draw_queue:
            num_vertices = num_vertices + len(vertices)

        # 2. Create empty buffers based on the number of vertices.
        #
        data = np.zeros(num_vertices,
                        dtype=[('position', np.float32, 3),
                               ('color', np.float32, 4)])

        # 3. Loop through all the shapes in the geometry queue adding
        # it's information to the buffer.
        #
        sidx = 0
        draw_indices = []
        for vertices, idx, color in draw_queue:
            num_shape_verts = len(vertices)

            data['position'][sidx:(sidx + num_shape_verts),
                             ] = np.array(vertices)

            color_array = np.array([color] * num_shape_verts)
            data['color'][sidx:sidx + num_shape_verts, :] = color_array

            draw_indices.append(sidx + idx)

            sidx += num_shape_verts

        self.vertex_buffer.set_data(data)
        self.index_buffer.set_data(np.hstack(draw_indices))

        # 4. Bind the buffer to the shader.
        #
        self.default_prog.bind(self.vertex_buffer)

        # 5. Draw the shape using the proper shape type and get rid of
        # the buffers.
        #
        self.default_prog.draw(draw_type, indices=self.index_buffer)

    def cleanup(self):
        """Run the clean-up routine for the renderer.

        This method is called when all drawing has been completed and the
        program is about to exit.

        """
        self.default_prog.delete()
        self.fbuffer_prog.delete()
        self.fbuffer.delete()

    def _transform_vertices(self, vertices, local_matrix, global_matrix):
        """Applies `local_matrix` then `global_matrix` to `vertices`
        """
        product = np.dot(np.dot(vertices, local_matrix.T), global_matrix.T)
        # dehomogenize coordinates
        # need np.newaxis to broadcast the vector because each row represents a vertex
        dehomogenized = product / product[:,3][:,np.newaxis]
        return dehomogenized[:,:3] # Return the first three rows of the result def get_render_primitives(shape):
    """Given a shape, return a list of render primitives in the form of [type, vertices, indices]
    """
    _check_shape(shape)
    render_primitives = []
    if isinstance(shape, Arc):
        # Render meshes
        if p5.renderer.style.fill_enabled:
            render_primitives.extend(_get_meshes(shape))
        # Render borders
        if p5.renderer.style.stroke_enabled:
            if shape.arc_mode in [
                    'CHORD', 'OPEN']:  # Implies shape.shape_type == TESS
                render_primitives.extend(_get_borders(shape))
            elif shape.arc_mode is None:  # Implies shape.shape_type == TRIANGLE_FAN
                render_primitives.append(
                    _get_line_from_verts(shape.vertices[1:]))
            elif shape.arc_mode == 'PIE':  # Implies shape.shape_type == TRIANGLE_FAN
                render_primitives.append(_get_line_from_verts(shape.vertices))
    else:
        # Render points
        if shape.shape_type == SType.POINTS:
            render_primitives.append(
                _vertices_to_render_primitive(
                    render_primitives, 'points'))
        # Render meshes
        if p5.renderer.style.fill_enabled:
            render_primitives.extend(_get_meshes(shape))
        # Render borders
        if p5.renderer.style.stroke_enabled:
            render_primitives.extend(_get_borders(shape))
    return render_primitives def to_3x3(mat):
    """Returns the upper left 3x3 corner of an np.array
    """
    return mat[:3, :3] class Style:
    background_color = (0.8, 0.8, 0.8, 1.0)
    fill_color = COLOR_WHITE
    fill_enabled = True
    stroke_color = COLOR_BLACK
    stroke_enabled = True
    tint_color = COLOR_BLACK
    tint_enabled = False COLOR_WHITE = (1, 1, 1, 1) class BasicMaterial:
    def __init__(self, color):
        self.color = color class NormalMaterial:
    pass class BlinnPhongMaterial:
    def __init__(self, ambient, diffuse, specular, shininess):
        self.ambient = ambient
        self.diffuse = diffuse
        self.specular = specular
        self.shininess = shininess
from numpy.linalg import inv
from dataclasses import dataclass
from sys import stderr
from p5.pmath import matrix
from vispy import gloo
from vispy.gloo import Texture2D, Program
from contextlib import contextmanager
from p5.core.constants import Z_EPSILON
from p5.core.geometry import Geometry
from ..Vispy2DRenderer.shape import PShape
from p5.pmath.matrix import translation_matrix
from ..Vispy2DRenderer.openglrenderer import OpenGLRenderer, get_render_primitives, to_3x3, Style, COLOR_WHITE
from .shaders3d import src_default, src_fbuffer, src_normal, src_phong
from p5.core.material import BasicMaterial, NormalMaterial, BlinnPhongMaterial
import numpy as np
import math
import builtins
            int(builtins.height * builtins.pixel_y_density),
        )
        self.texture_viewport = (
            0,
            0,
            builtins.width,
            builtins.height,
        )

        gloo.set_viewport(*self.viewport)  # pylint: disable=no-member

        cz = (builtins.height / 2) / math.tan(math.radians(30))
        self.projection_matrix = matrix.perspective_matrix(
            math.radians(60),
            builtins.width / builtins.height,
            0.1 * cz,
            10 * cz
        )

        self.transform_matrix = np.identity(4)
        self._update_shader_transforms()

        self.fbuffer_tex_front = Texture2D(
            (builtins.height, builtins.width, 3))
        self.fbuffer_tex_back = Texture2D((builtins.height, builtins.width, 3))
        self.fbuffer.depth_buffer = gloo.RenderBuffer(
            (builtins.height, builtins.width))

        for buf in [self.fbuffer_tex_front, self.fbuffer_tex_back]:
            self.fbuffer.color_buffer = buf
            with self.fbuffer:
                self.clear()

    def clear(self, color=True, depth=True):
        """Clear the renderer background."""
        gloo.set_state(clear_color=self.style.background_color)  # pylint: disable=no-member
        gloo.clear(color=color, depth=depth)  # pylint: disable=no-member

    def clear_lights(self):
        self.ambient_light_color.clear()
        self.directional_light_color.clear()
        self.directional_light_dir.clear()
        self.directional_light_specular.clear()
        self.point_light_color.clear()
        self.point_light_pos.clear()
        self.point_light_specular.clear()
        self.const_falloff.clear()
        self.linear_falloff.clear()
        self.quadratic_falloff.clear()

    def _comm_toggles(self, state=True):
        gloo.set_state(blend=state)  # pylint: disable=no-member
        gloo.set_state(depth_test=state)  # pylint: disable=no-member

        if state:
            gloo.set_state(blend_func=('src_alpha', 'one_minus_src_alpha'))  # pylint: disable=no-member
            gloo.set_state(depth_func='lequal')  # pylint: disable=no-member

    def _update_shader_transforms(self):
        # Default shader
        self.default_prog['projection'] = self.projection_matrix.T.flatten()
        self.default_prog['perspective_matrix'] = self.lookat_matrix.T.flatten()

        # Normal shader
        self.normal_prog['projection'] = self.projection_matrix.T.flatten()
        self.normal_prog['perspective'] = self.lookat_matrix.T.flatten()
        # This is a no-op, meaning that the normals stay in world space, which
        # matches the behavior in p5.js
        normal_transform = np.identity(3)
        # I think the transformation below takes the vertices to camera space, but
        # the results are funky, so it's probably incorrect? - ziyaointl, 2020/07/20
        # normal_transform = np.linalg.inv(self.projection_matrix[:3, :3] @ self.lookat_matrix[:3, :3])
        self.normal_prog['normal_transform'] = normal_transform.flatten()

        # Blinn-Phong Shader
        self.phong_prog['projection'] = self.projection_matrix.T.flatten()
        self.phong_prog['perspective'] = self.lookat_matrix.T.flatten()

    @contextmanager
    def draw_loop(self):
        """The main draw loop context manager.
        """

        self.transform_matrix = np.identity(4)
        self._update_shader_transforms()
        self.fbuffer.color_buffer = self.fbuffer_tex_back

        with self.fbuffer:
            gloo.set_viewport(*self.texture_viewport)  # pylint: disable=no-member
            self._comm_toggles()
            self.fbuffer_prog['texture'] = self.fbuffer_tex_front
            self.fbuffer_prog.draw('triangle_strip')
            self.clear(color=False, depth=True)
            self.clear_lights()

            yield

            self.flush_geometry()
            self.transform_matrix = np.identity(4)

        gloo.set_viewport(*self.viewport)  # pylint: disable=no-member
        self._comm_toggles(False)
        self.clear()
        self.fbuffer_prog['texture'] = self.fbuffer_tex_back
        self.fbuffer_prog.draw('triangle_strip')

        self.fbuffer_tex_front, self.fbuffer_tex_back = self.fbuffer_tex_back, self.fbuffer_tex_front

    def _add_to_draw_queue_simple(self, stype, vertices, idx, color):
        """Adds shape of stype to draw queue
        """
        self.draw_queue.append((stype, (vertices, idx, color, None, None)))

    def tnormals(self, shape):
        """Obtain a list of vertex normals in world coordinates
        """
        if isinstance(shape.material,
                      BasicMaterial):  # Basic shader doesn't need this
            return None
        return shape.vertex_normals @ np.linalg.inv(

            to_3x3(self.transform_matrix) @ to_3x3(shape.matrix))