Esempio n. 1
0
class UIWidget(QWidget):
    """ Class that holds all of the widgets for viewing """

    def __init__(self):
        """ Initializes class and variables """
        super(UIWidget, self).__init__()
        self.axiom = QLabel("Axiom")
        self.angle = QLabel("Angles(degrees)")
        self.iters = QLabel("Iterations")
        self.axiom_edit = CustomLineEdit()
        self.angle_edit = CustomLineEdit()
        self.iters_edit = CustomLineEdit()
        self.text_boxes = [self.axiom_edit, self.angle_edit, self.iters_edit]
        self.prod_plus = QPushButton("+", self)
        self.lsys_button = QPushButton("Generate L System", self)
        self.boxcount_button = QPushButton("Fractal Dim", self)

        self.widget = QWidget()
        self.scroll_area = QtWidgets.QScrollArea()
        self.layout_examples = QVBoxLayout(self.widget)
        self.layout_examples.setAlignment(Qt.AlignTop)


        self.prods = 1
        self.prod_rules_edit = []
        self.examples = []
        self.minuses = None
        self.made_angle = False
        self.prod_percent = []
        self.amount = 0
        self.index = 0
        self.frac_points = []
        self.made_line = False
        self.prod_rules = []
        self.verts = []  # This will store the vertices from generate_lsystem
        self.saved_lsystems = load_saved_lsystems()
        self.two_d = LSystem2DWidget()
        self.three_d = LSystem3DWidget()

        self.fractal_menu = FractalDimension(self)
        self.dims = QStackedWidget()
        self.dims.addWidget(self.two_d)
        self.dims.addWidget(self.three_d)
        self.dims.setCurrentWidget(self.two_d)
        self.graphix = self.two_d

        self.init_UI()
        self.alphabet = [
            "F",
            "f",
            "G",
            "g",
            "H",
            "h",
            "^",
            "&",
            "-",
            "+",
            "[",
            "]",
            "|",
            "(",
            ")",
            ">",
            "<",
            " ",

        ]
        self.ctrl_char = [
            "A",
            "B",
            "C",
            "D",
            "E",
            "I",
            "J",
            "K",
            "L",
            "M",
            "N",
            "O",
            "P",
            "Q",
            "R",
            "S",
            "T",
            "U",
            "V",
            "W",
            "X",
            "Y",
            "Z",
        ]

    def init_UI(self):
        """ Creates and adds all widgets in the viewport and sets the layout  """
        # renames the window
        self.setWindowTitle("L-Systems Generator")
        self.layout = QGridLayout()
        self.init_buttons()
        self.init_text_boxes()
        self.add_widgets()
        self.setLayout(self.layout)
        self.setGeometry(500, 500, 500, 500)

    def init_text_boxes(self):
        """Creates textboxes for the UI """
        self.prod_rules.append(QLabel("Production Rule " + str(self.prods)))

        # creates the text box for each label
        self.axiom_edit.returnPressed.connect(self.lsys_button.click)
        self.axiom_edit.clicked.connect(lambda: self.axiom_edit.reset_color())

        self.prod_rules_edit.append(CustomLineEdit())
        self.prod_rules_edit[0].clicked.connect(
            lambda: self.prod_rules_edit[0].reset_color()
        )
        self.text_boxes.append(self.prod_rules_edit[-1])
        self.prod_rules_edit[0].returnPressed.connect(self.lsys_button.click)
        self.prod_rules_edit[0].textChanged.connect(lambda: self.show_popup())

        self.prod_percent.append(CustomLineEdit())
        self.prod_percent[0].setFixedWidth(50)
        self.prod_percent[0].setText("100%")

        self.text_boxes.append(self.prod_percent[-1])
        self.angle_edit.returnPressed.connect(self.lsys_button.click)
        self.angle_edit.clicked.connect(lambda: self.angle_edit.reset_color())

        self.iters_edit.returnPressed.connect(self.lsys_button.click)
        self.iters_edit.clicked.connect(lambda: self.iters_edit.reset_color())

        self.prod_plus.clicked.connect(self.more_prods)

    def init_buttons(self):
        """Creates buttons for the UI"""
        # makes the lsys generator button
        self.lsys_button.clicked.connect(self.on_lsys_button_clicked)
        self.lsys_button.setAutoDefault(True)

        self.boxcount_button.clicked.connect(self.on_boxcount_button_clicked)
        self.boxcount_button.setAutoDefault(True)

        self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.scroll_area.setWidgetResizable(True)
        self.scroll_area.setFixedWidth(150)
        self.scroll_area.setWidget(self.widget)

        self.precons = ['SierpinksiTriangle', 'KochCurve', 'KochSnowflake',
            'KochIsland', 'PeanoCurve', 'DragonCurve', 'HilbertCurve',
            'TreeExample', 'IslandsandLakes']

        for i, key in enumerate(self.saved_lsystems["two-d"]):
            self.examples.append(QPushButton())
            if i < len(self.precons):
              self.examples[i].setIcon(QIcon('{}/lsystem/assets/images/{}.png'.format(os.getcwd(), self.precons[i])))
              self.examples[i].setIconSize(QtCore.QSize(120, 100))
            else:
              self.examples[i].setText(key)
              self.examples[i].setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
              self.examples[i].customContextMenuRequested.connect(lambda state, x=key: self.del_example(str(x), "two-d"))
            self.examples[i].clicked.connect(
                lambda state, x=key: self.gen_example(str(x))
            )
            self.layout_examples.addWidget(self.examples[i])

    def reload_presets(self):
        """pulls saved lsystems from file"""
        self.saved_lsystems = load_saved_lsystems()
        self.set_presets()
    def set_presets(self):
        """Shows preset L-Systems for the appropriate dimention"""
        if self.is_2d():
            for widget in self.examples:
                self.layout_examples.removeWidget(widget)
                widget.deleteLater()
                widget=None
            self.examples = []
            for i, key in enumerate(self.saved_lsystems["two-d"]):
                self.examples.append(QPushButton())
                if i < len(self.precons):
                  self.examples[i].setIcon(QIcon('{}/lsystem/assets/images/{}.png'.format(os.getcwd(), self.precons[i])))
                  self.examples[i].setIconSize(QtCore.QSize(120, 100))
                else:
                  self.examples[i].setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
                  self.examples[i].customContextMenuRequested.connect(lambda state, x=key: self.del_example(str(x),"two-d"))
                  self.examples[i].setText(key)
                self.examples[i].clicked.connect(
                    lambda state, x=key: self.gen_example(str(x))
                )
                self.layout_examples.addWidget(self.examples[i])
        elif not self.is_2d():
            for widget in self.examples:
                self.layout_examples.removeWidget(widget)
                widget.deleteLater()
                widget=None
            self.examples = []
            for i, key in enumerate(self.saved_lsystems["three-d"]):
                self.examples.append(QPushButton())
                self.examples[i].setText(key)
                self.examples[i].clicked.connect(
                    lambda state, x=key: self.gen_example(str(x))
                )
                self.layout_examples.addWidget(self.examples[i])
                if(i>1): # TODO - This is temporary until we have a manner of separting 3d precons from 2d precons
                  self.examples[i].setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
                  self.examples[i].customContextMenuRequested.connect(lambda state, x=key: self.del_example(str(x),"three-d"))


    @QtCore.pyqtSlot()
    def on_lsys_button_clicked(self):
        """Generates the L-System"""
        self.gen_sys()

    def boxcount_2d(self):
        """Calculates the dimensionality of a 2D L-System"""
        self.fractal_menu.show()
        start_size = 8
        num_sizes = 7
        self.x_arr = []
        self.y_arr = []
        fract_avg = []
        end_size = start_size * (2 ** num_sizes)
        temp_verts = copy.deepcopy(self.verts)
        fractal_dim = fractal_dim_calc(temp_verts, end_size, num_sizes)
        for i in range(num_sizes):
            self.x_arr.append(np.log2((start_size)))
            self.y_arr.append(fractal_dim[i])

            fract_avg.append(np.polyfit(self.x_arr, self.y_arr, 1)[0])
            print("(box width = 1/", start_size, ") FRACTAL AVG: ", fract_avg[-1])
            start_size = start_size * 2
        # y_arr = np.asarray(y_arr)
        # x_arr = np.asarray(x_arr)
        print("Made it this far")
        figi, ax = plt.subplots()
        line, = ax.plot(self.x_arr, self.y_arr, "bo", picker=5)
        ax.set_title(
            "Fractal dimension = {}".format(np.polyfit(self.x_arr, self.y_arr, 1)[0])
        )  # np.average(fract_avg)))
        figi.canvas.mpl_connect('pick_event', self.onpick1)
        #figi.show()
        plt.show()
        print("AVERAGE: ", np.average(fract_avg))

    def boxcount_3d(self):
        """Calculates the dimensionality of a 3D L-System"""
        mesh = self.graphix.mesh
        if(mesh is not None):
            calc_fractal_dim3D(mesh)


    def is_2d(self):
        """Returns true if 2D is loaded, false if 3D is loaded."""
        if (self.dims.currentWidget().__class__.__name__ == 'LSystem3DWidget'):
            return False
        return True

    def on_boxcount_button_clicked(self):
        """Determines which type of dimension checking is done"""
        if(self.is_2d()):
            self.boxcount_2d()
        else:
            self.boxcount_3d()

    def onpick1(self, event):
        print("I am an EVENT1")
        #plt.close(fig=None)
        ind = event.ind[0]+1
        for i in range(ind):
            if(len(self.x_arr)==2):
                break
            # always delete index 0 because array shifts left after delete
            self.x_arr = np.delete(self.x_arr, 0, axis=None)
            self.y_arr = np.delete(self.y_arr, 0, axis=None)
        print("The x array is: ", self.x_arr)
        fig1, ax = plt.subplots()
        ax.plot(self.x_arr, self.y_arr, "bo", picker=5)
        ax.set_title(
            "Fractal dimension = {}".format(np.polyfit(self.x_arr, self.y_arr, 1)[0])
        )  # np.average(fract_avg)))
        fig1.canvas.mpl_connect('pick_event', self.onpick2)
        fig1.show()
        return True
    def onpick2(self, event):
        print("I am an EVENT2")
        #plt.close(fig=None)
        ind = event.ind[0]
        num_to_remove = len(self.x_arr)-ind
        for i in range(num_to_remove):
            if(len(self.x_arr)==2):
                break
            # always delete index -1
            self.x_arr = np.delete(self.x_arr, -1, axis=None)
            self.y_arr = np.delete(self.y_arr, -1, axis=None)
        fig2, ax = plt.subplots()
        ax.plot(self.x_arr, self.y_arr, "bo", picker=5)
        ax.set_title(
            "Fractal dimension = {}".format(np.polyfit(self.x_arr, self.y_arr, 1)[0])
        )  # np.average(fract_avg)))
        fig2.show()
        return True



    def add_widgets(self):
        """Adds widgets to window"""
        self.layout.addWidget(self.axiom, 1, 0)
        self.layout.addWidget(self.axiom_edit, 1, 1, 1, 10)
        self.layout.addWidget(self.prod_rules[0], 2, 0, 1, 1)
        self.layout.addWidget(self.prod_rules_edit[0], 2, 1, 1, 9)
        self.layout.addWidget(self.prod_percent[0], 2, 9)
        self.layout.addWidget(self.prod_plus, 2, 10, 1, 1)
        self.layout.addWidget(self.angle, 10, 0)
        self.layout.addWidget(self.angle_edit, 10, 1, 1, 10)
        self.layout.addWidget(self.iters, 13, 0)
        self.layout.addWidget(self.iters_edit, 13, 1, 1, 10)
        self.layout.addWidget(self.scroll_area, 14, 0, 1, 1)
        self.layout.addWidget(self.boxcount_button, 16, 0, 1, 1)
        self.layout.addWidget(self.dims, 14, 1, 5, -1)
        self.layout.addWidget(self.lsys_button, 20, 0, 1, -1)

    def show_popup(self):
      """Adds and removes extra textboxes as needed"""
      if self.is_2d():
        self.reset_text_box_color()
        prod_rule = ""
        rules = ""
        self.amount = 0
        for prod in self.prod_rules_edit:
            prod_rule += prod.text()
            temp = prod.text()
            temp = temp.replace(" ", "")
            temp = temp[:].split(":")[0]
            rules += temp
            rules += " "

        all_prod_rule = prod_rule + self.axiom_edit.text()

        if (")" in all_prod_rule or "(" in all_prod_rule) and self.made_angle is False:
            self.turn_angle = QLabel("Turning Angle")
            self.turn_angle_edit = CustomLineEdit()
            self.text_boxes.append(self.turn_angle_edit)
            self.turn_angle_edit.returnPressed.connect(self.lsys_button.click)
            self.turn_angle_edit.clicked.connect(
                lambda: self.turn_angle_edit.reset_color()
            )
            self.layout.addWidget(self.turn_angle, 11, 0)
            self.layout.addWidget(self.turn_angle_edit, 11, 1, 1, 10)
            self.made_angle = True

        if (
            self.made_angle is True
            and not "(" in all_prod_rule
            and not ")" in all_prod_rule
            and self.made_angle is True
        ):
            self.text_boxes.remove(self.turn_angle_edit)
            self.layout.removeWidget(self.turn_angle_edit)
            self.layout.removeWidget(self.turn_angle)
            self.turn_angle.deleteLater()
            self.turn_angle_edit.deleteLater()
            self.turn_angle_edit = None
            self.turn_angle = None
            self.made_angle = False

        if (">" in all_prod_rule or "<" in all_prod_rule) and self.made_line is False:
            self.line_scale = QLabel("Line Scale")
            self.line_scale_edit = CustomLineEdit()
            self.text_boxes.append(self.line_scale_edit)
            self.line_scale_edit.returnPressed.connect(self.lsys_button.click)
            self.line_scale_edit.clicked.connect(
                lambda: self.line_scale_edit.reset_color()
            )
            self.layout.addWidget(self.line_scale, 12, 0)
            self.layout.addWidget(self.line_scale_edit, 12, 1, 1, 10)
            self.made_line = True

        if (
            self.made_line is True
            and not "<" in all_prod_rule
            and not ">" in all_prod_rule
            and self.made_line is True
        ):
            self.text_boxes.remove(self.line_scale_edit)
            self.layout.removeWidget(self.line_scale_edit)
            self.layout.removeWidget(self.line_scale)
            self.line_scale.deleteLater()
            self.line_scale_edit.deleteLater()
            self.line_scale_edit = None
            self.line_scale = None
            self.made_line = False

    # Probably doesn't need self as a param, can just be static.
    def gen_rule_dict(self, prod_rules):
        """
        Generates a rule dictionary from an array of production rules taken from the UI.
        
        formats production rules as
        {Symbol1: [[probability,replacement],...], Symbol2: [[probability,replacement]... ], ...}
        """
        rules = {}
        for rule in prod_rules:
            rule = rule.text()
            rule = rule.replace(" ", "")
            prod = rule.split(":")
            rules[prod[0]] = []
        for i, rule in enumerate(prod_rules):
            rule = rule.text()
            rule = rule.replace(" ", "")
            prod = rule.split(":")
            rules[prod[0]].append([float(self.prod_percent[i].text().split("%")[0])/100, prod[1]])
        return rules

    def close_event(self):
        print("[ INFO ] Exiting...")
        exit()

    def more_prods(self):
        """ Adds textboxes for additional production rules, maxiumum 8."""
        if self.prods < 8:
            self.prods = self.prods + 1
            self.prod_rules.append(QLabel("Production Rule " + str(self.prods)))
            self.prod_rules_edit.append(CustomLineEdit())
            self.prod_percent.append(CustomLineEdit())
            self.text_boxes.append(self.prod_rules_edit[-1])
            self.text_boxes.append(self.prod_percent[-1])
            self.prod_percent[-1].setFixedWidth(50)
            self.prod_rules_edit[self.prods - 1].textChanged.connect(
                lambda: self.show_popup()
            )
            self.prod_rules_edit[-1].returnPressed.connect(self.lsys_button.click)
            self.prod_rules_edit[-1].clicked.connect(
                lambda: self.prod_rules_edit[-1].reset_color()
            )
            self.layout.addWidget(self.prod_rules[self.prods - 1], self.prods + 1, 0)
            self.layout.addWidget(
                self.prod_rules_edit[self.prods - 1], self.prods + 1, 1, 1, 9
            )
            self.layout.addWidget(self.prod_percent[self.prods - 1], self.prods + 1, 9)

            if self.minuses is not None:
                # remove last minueses
                self.layout.removeWidget(self.minuses)
                self.minuses.deleteLater()
                self.minuses = None

            self.minuses = QPushButton("-", self)
            self.minuses.clicked.connect(self.less_prods)
            self.layout.addWidget(self.minuses, self.prods + 1, 10, 1, 1)
            self.prod_percent[-1].setText("100%")

    def less_prods(self):
        """ Removes textboxes for production rules when less are needed, minimum 1."""
        if self.prods > 1:
            self.text_boxes.remove(self.prod_rules_edit[-1])
            self.text_boxes.remove(self.prod_percent[-1])

            # remove last widget prodrules
            self.layout.removeWidget(self.prod_rules[-1])
            self.prod_rules[-1].deleteLater()
            self.prod_rules.pop()
            # remove last widget prodrulesEdit
            self.layout.removeWidget(self.prod_rules_edit[-1])
            self.prod_rules_edit[-1].deleteLater()
            self.prod_rules_edit.pop()
            # remove last percentage
            self.layout.removeWidget(self.prod_percent[-1])
            self.prod_percent[-1].deleteLater()
            self.prod_percent.pop()
            # remove last percentage
            # for i in self.index:
            #    for j in i:
            #        if j == self.prods-1:
            #             print("WE NEED TO DELETE")
            #             print(i)
            #      print("HELLO")
            #      self.layout.removeWidget(self.prodPercent[-1])
            #      self.prodPercent[-1].deleteLater()
            #      self.prodPercent.pop()
            #      self.amount = self.amount - 1
            #      print(len(self.prodPercent))
            # remove last minuses
            self.layout.removeWidget(self.minuses)
            self.minuses.deleteLater()
            self.minuses = None
            self.prods = self.prods - 1


        if self.prods > 1:
            self.minuses = QPushButton("-", self)
            self.minuses.clicked.connect(self.less_prods)
            self.layout.addWidget(self.minuses, self.prods + 1, 10, 1, 1)

    def reset_input_boxes(self):
        """Resets textboxes to initial configuration, does not clear the fractal from the widget."""
        while self.prods >1:
            self.less_prods()
        self.prod_rules_edit[-1].setText("")
        self.axiom_edit.setText("")
        self.prod_percent[0].setText("100%")
        self.angle_edit.setText("")
        self.iters_edit.setText("")

    def gen_sys(self):
        """Generates the L-System described by the production rules"""
        if input_check(self):
            axiom_input = self.axiom_edit.text()
            # prodInput = [self.prodrlesEdit.text()] #changed to array
            angle_input = self.angle_edit.text()
            if self.made_angle:
                turn_angle_input = self.turn_angle_edit.text()
            else:
                turn_angle_input = 0
            if self.made_line:
                line_scale_input = self.line_scale_edit.text()
            else:
                line_scale_input = 1
            iters_input = self.iters_edit.text()
            # Format input for use
            rules = self.gen_rule_dict(self.prod_rules_edit)
            # Generate rule grammar dictionary.
            grammar = {
                "rules": rules,
                "axiom": axiom_input,
                "iterations": int(iters_input),
                "angle": float(angle_input),
                "turnAngle": float(turn_angle_input),
                "lineScale": float(line_scale_input),
            }
            if (self.dims.currentWidget().__class__.__name__ == 'LSystem3DWidget'):
              self.mesh = generate_lsystem_3d(grammar)
              if self.mesh ==-1:
                  print("[ ERROR ] Invalid input no vertices generated.")
              else:
                  self.graphix.clear()
                  self.graphix.add_mesh(self.mesh)
            else:
              self.verts = generate_lsystem_2d(grammar)
              if self.verts ==-1:
                  print("[ ERROR ] Invalid input no vertices generated.")
              else:
                  # Sets verts on graphics widget and draws
                  self.graphix.clear()
                  self.graphix.set_graph(self.verts)
            # for i in range(1,len(self.verts)):
            #  self.graphix.set_graph(self.verts[i],1) #split = true
        self.graphix.update()
        self.graphix.reset_camera()

    def gen_example(self, example):
        """Loads preset L-Systems"""
        self.axiom_edit.reset_box()
        for prod in self.prod_rules_edit:
            prod.reset_box()
        self.angle_edit.reset_box()
        self.iters_edit.reset_box()
        if(self.is_2d()):
            grammar = get_saved_lsystem(example, self.saved_lsystems["two-d"])
        else:
            grammar = get_saved_lsystem(example, self.saved_lsystems["three-d"])
        self.axiom_edit.setText(grammar["axiom"])

        num_rules = 0
        for key in grammar["rules"]:
            if isinstance(grammar["rules"][key], list):
                num_rules += len(grammar["rules"][key])
            else:
                num_rules += 1

        while self.prods < num_rules:
            self.more_prods()

        while self.prods > num_rules:
            self.less_prods()

        which_rule = 0
        for i, key in enumerate(grammar["rules"]):
            value = grammar["rules"][key]
            if isinstance(value, str):
                self.prod_rules_edit[which_rule].setText(key + ": " + value)
                which_rule+=1
            else:
                for val in value:
                    print(val)
                    self.prod_rules_edit[which_rule].setText(key + ": " + val[0])
                    self.prod_percent[which_rule].setText(val[1])
                    which_rule+=1

        self.angle_edit.setText(str(grammar["angle"]))
        if self.made_angle:
            self.turn_angle_edit.setText(str(grammar["turn_angle"]))
        if self.made_line:
            self.line_scale_edit.setText(str(grammar["line_scale"]))
        self.iters_edit.setText(str(grammar["iterations"]))
        self.gen_sys()

    def del_example(self, example, dim):
        remove_saved_lsystem(example, dim)
        print(example, " was deleted from disk")
        self.reload_presets()

        

    def reset_text_box_color(self):
        """resets the color of all textboxes"""
        for box in self.text_boxes:
            box.reset_color()
            
    def reset_zoom(self):
        """resets the zoom level in the display widget"""
        self.two_d.reset_zoom()
        self.three_d.reset_zoom() #built in function don't change to snake script
        
    def screenshot(self, parent_pos):
        """Takes a screenshot of the display window"""
        #rel pos is the upper left pointof the widget relative to window
        rel_pos = self.dims.pos()
        #shift the y down by the total height - height of the widget (height of text boxes)
        #1.1 factor added arbitrarily
        rel_pos.setY((self.height()-self.dims.height())*1.1)
        pos =  rel_pos+parent_pos
        qfd = QFileDialog()
        filter = "Images (*.png *.xpm *.jpg)"
        filename, type = QFileDialog.getSaveFileName(self, "", "", filter)
        if filename:
            self.two_d.screenshot(filename,pos)