diff --git a/docs/source/todo.md b/docs/source/todo.md index 65d59ea1..ffa957dc 100644 --- a/docs/source/todo.md +++ b/docs/source/todo.md @@ -14,9 +14,9 @@ Planned feature additions - Add variable constraint GUI object positioning (add keyword arguments that are stored in the `.jmea` files) - Add error handling for over-constrained/Jax shape/no solution in GCS - Add an undo/redo framework to the GUI -- Graphical highlighting of constraint parameters and constraints +- Graphical highlighting of constraint parameters and constraints - make hoverEnter detection on constraint canvas items +- Tie parameter hover to associated constraint hover events - Add unit selection ComboBox -- Add renaming to parameters - Make the "Analysis" tab focused by default after an aerodynamics analysis (possibly implement a user option to override this behavior) - Write the XFOIL/MSES analysis code using the same `CPUBoundProcess` architecture used by optimization @@ -40,6 +40,8 @@ Bug fixes - Remove wave/viscous drag from XFOIL drag history plots (optimization) - Fix symmetry constraint having switched target/tool points (perhaps automatically create the mirror point?) - Toggle grid affects all the dock widgets +- Apply theme to status bar widgets immediately on theme change +- Correct dimensions having default colors before switching themes Testing ------- diff --git a/pymead/core/gcs2.py b/pymead/core/gcs2.py index a2ad7ae1..12b66995 100644 --- a/pymead/core/gcs2.py +++ b/pymead/core/gcs2.py @@ -248,10 +248,10 @@ def solve(self, source: GeoCon): d_angle = new_angle - old_angle rotation_point = source.p2 - if edge_data_p21 and edge_data_p21["angle"] is source: + if edge_data_p21 and "angle" in edge_data_p21 and edge_data_p21["angle"] is source: start = source.p1 # d_angle *= -1 - elif edge_data_p23 and edge_data_p23["angle"] is source: + elif edge_data_p23 and "angle" in edge_data_p23 and edge_data_p23["angle"] is source: start = source.p3 d_angle *= -1 else: @@ -279,9 +279,9 @@ def solve(self, source: GeoCon): d_angle = new_angle - old_angle rotation_point = source.vertex - if edge_data_p21 and edge_data_p21["angle"] is source: + if edge_data_p21 and "angle" in edge_data_p21 and edge_data_p21["angle"] is source: pass - elif edge_data_p23 and edge_data_p23["angle"] is source: + elif edge_data_p23 and "angle" in edge_data_p23 and edge_data_p23["angle"] is source: pass d_angle *= -1 else: diff --git a/pymead/gui/airfoil_canvas.py b/pymead/gui/airfoil_canvas.py index 298110ed..a572f51f 100644 --- a/pymead/gui/airfoil_canvas.py +++ b/pymead/gui/airfoil_canvas.py @@ -2,6 +2,7 @@ import typing import sys from copy import deepcopy +from functools import partial import numpy as np import pyqtgraph as pg @@ -206,7 +207,7 @@ def addPymeadCanvasItem(self, pymead_obj: PymeadObj): pymead_obj.canvas_item.sigPolyExit.connect(self.airfoil_exited) @staticmethod - def runSelectionEventLoop(drawing_object: str, starting_message: str): + def runSelectionEventLoop(drawing_object: str, starting_message: str, enter_callback: typing.Callable = None): drawing_object = drawing_object starting_message = starting_message @@ -215,7 +216,11 @@ def wrapped(self, *args, **kwargs): self.drawing_object = drawing_object self.sigStatusBarUpdate.emit(starting_message, 0) loop = QEventLoop() - self.sigEnterPressed.connect(loop.quit) + connection = None + if enter_callback: + connection = self.sigEnterPressed.connect(partial(enter_callback, self)) + else: + self.sigEnterPressed.connect(loop.quit) self.sigEscapePressed.connect(loop.quit) loop.exec() # if len(self.geo_col.selected_objects["points"]) > 0: @@ -226,6 +231,8 @@ def wrapped(self, *args, **kwargs): # self.clearSelectedObjects() self.drawing_object = None self.sigStatusBarUpdate.emit("", 0) + if enter_callback: + self.sigEnterPressed.disconnect(connection) return wrapped return decorator @@ -234,6 +241,18 @@ def wrapped(self, *args, **kwargs): def drawPoints(self): pass + def drawBezierNoEvent(self): + if len(self.geo_col.selected_objects["points"]) < 2: + msg = f"Choose at least 2 points to define a curve" + self.sigStatusBarUpdate.emit(msg, 2000) + return + + point_sequence = PointSequence([pt for pt in self.geo_col.selected_objects["points"]]) + self.geo_col.add_bezier(point_sequence=point_sequence) + + self.clearSelectedObjects() + self.sigStatusBarUpdate.emit("Select the first Bezier control point of the next curve", 0) + @runSelectionEventLoop(drawing_object="Bezier", starting_message="Select the first Bezier control point") def drawBezier(self): if len(self.geo_col.selected_objects["points"]) < 2: @@ -244,6 +263,14 @@ def drawBezier(self): point_sequence = PointSequence([pt for pt in self.geo_col.selected_objects["points"]]) self.geo_col.add_bezier(point_sequence=point_sequence) + @runSelectionEventLoop(drawing_object="Beziers", starting_message="Select the first Bezier control point", + enter_callback=drawBezierNoEvent) + def drawBeziers(self): + if len(self.geo_col.selected_objects["points"]) < 2: + msg = f"Choose at least 2 points to define a curve" + self.sigStatusBarUpdate.emit(msg, 2000) + return + @runSelectionEventLoop(drawing_object="LineSegment", starting_message="Select the first line endpoint") def drawLineSegment(self): if len(self.geo_col.selected_objects["points"]) < 2: @@ -254,8 +281,12 @@ def drawLineSegment(self): point_sequence = PointSequence([pt for pt in self.geo_col.selected_objects["points"]]) self.geo_col.add_line(point_sequence=point_sequence) + @runSelectionEventLoop(drawing_object="LineSegments", starting_message="Select the first line endpoint") def drawLines(self): - self.drawLineSegment() + if len(self.geo_col.selected_objects["points"]) < 2: + msg = f"Choose at least 2 points to define a curve" + self.sigStatusBarUpdate.emit(msg, 2000) + return @runSelectionEventLoop(drawing_object="Airfoil", starting_message="Select the leading edge point") def generateAirfoil(self): @@ -263,6 +294,7 @@ def generateAirfoil(self): self.sigStatusBarUpdate.emit( "Choose either 2 points (sharp trailing edge) or 4 points (blunt trailing edge)" " to generate an airfoil", 4000) + return le = self.geo_col.selected_objects["points"][0] te = self.geo_col.selected_objects["points"][1] @@ -291,6 +323,7 @@ def addLengthDimension(self): self.sigStatusBarUpdate.emit("Choose either 2 points (no length parameter) or 3 points " "(specified length parameter)" " to add a length dimension", 4000) + return tool_point = self.geo_col.selected_objects["points"][0] target_point = self.geo_col.selected_objects["points"][1] @@ -302,6 +335,7 @@ def addLengthDimension(self): def addDistanceConstraint(self): if len(self.geo_col.selected_objects["points"]) != 2: self.sigStatusBarUpdate.emit("Choose exactly two points to define a distance constraint", 4000) + return # p1 = self.geo_col.selected_objects["points"][0] # p2 = self.geo_col.selected_objects["points"][1] @@ -321,6 +355,7 @@ def addAngleDimension(self): self.sigStatusBarUpdate.emit("Choose either 2 points (no angle parameter) or 3 points " "(specified angle parameter)" " to add an angle dimension", 4000) + return tool_point = self.geo_col.selected_objects["points"][0] target_point = self.geo_col.selected_objects["points"][1] angle_param = None if len(self.geo_col.selected_objects["points"]) <= 2 else self.geo_col.selected_objects["points"][2] @@ -424,14 +459,35 @@ def pointClicked(self, scatter_item, spot, ev, point_item): self.geo_col.select_object(point_item.point) n_ctrl_pts = len(self.geo_col.selected_objects["points"]) degree = n_ctrl_pts - 1 - msg = (f"Added control point to curve. Number of control points: {len(self.geo_col.selected_objects['points'])} " + msg = (f"Added control point to curve. Number of control points: " + f"{len(self.geo_col.selected_objects['points'])} " + f"(degree: {degree}). Press 'Enter' to generate the curve.") + self.sigStatusBarUpdate.emit(msg, 0) + elif self.drawing_object == "Beziers": + self.geo_col.select_object(point_item.point) + n_ctrl_pts = len(self.geo_col.selected_objects["points"]) + degree = n_ctrl_pts - 1 + msg = (f"Added control point to curve. Number of control points: " + f"{len(self.geo_col.selected_objects['points'])} " f"(degree: {degree}). Press 'Enter' to generate the curve.") self.sigStatusBarUpdate.emit(msg, 0) elif self.drawing_object == "LineSegment": if len(self.geo_col.selected_objects["points"]) < 2: self.geo_col.select_object(point_item.point) + if len(self.geo_col.selected_objects["points"]) == 1: + self.sigStatusBarUpdate.emit("Next, choose the line's endpoint", 0) if len(self.geo_col.selected_objects["points"]) == 2: self.sigEnterPressed.emit() # Complete the line after selecting the second point + elif self.drawing_object == "LineSegments": + if len(self.geo_col.selected_objects["points"]) < 2: + self.geo_col.select_object(point_item.point) + if len(self.geo_col.selected_objects["points"]) == 1: + self.sigStatusBarUpdate.emit("Next, choose the line's endpoint", 0) + if len(self.geo_col.selected_objects["points"]) == 2: + point_sequence = PointSequence([pt for pt in self.geo_col.selected_objects["points"]]) + self.geo_col.add_line(point_sequence=point_sequence) + self.clearSelectedObjects() + self.sigStatusBarUpdate.emit("Choose the next line's start point", 0) elif self.adding_point_to_curve is not None: if len(self.geo_col.selected_objects["points"]) < 2: self.geo_col.select_object(point_item.point) diff --git a/pymead/gui/gui.py b/pymead/gui/gui.py index 6f49bedd..ba07f4e0 100644 --- a/pymead/gui/gui.py +++ b/pymead/gui/gui.py @@ -459,6 +459,14 @@ def set_theme(self, theme_name: str): for cnstr in self.geo_col.container()["geocon"].values(): cnstr.canvas_item.setStyle(theme) + for button_name, button_setting in self.main_icon_toolbar.button_settings.items(): + icon_name = button_setting["icon"] + if "dark" not in icon_name: + continue + + image_path = os.path.join(ICON_DIR, icon_name.replace("dark", theme_name)) + self.main_icon_toolbar.buttons[button_name]["button"].setIcon(QIcon(image_path)) + def set_color_bar_style(self, new_values: dict = None): if self.cbar is None: return diff --git a/pymead/gui/gui_settings/buttons.json b/pymead/gui/gui_settings/buttons.json index 5f4157cf..96f411a8 100644 --- a/pymead/gui/gui_settings/buttons.json +++ b/pymead/gui/gui_settings/buttons.json @@ -13,17 +13,59 @@ "function": "change_background_color_button_toggled" }, "draw-point": { - "icon": "point.svg", + "icon": "point_dark.svg", "status_tip": "Draw points", "checkable": false, "function": "on_draw_points_pressed" }, "draw-line": { - "icon": "line.svg", + "icon": "line_dark.svg", "status_tip": "Draw lines", "checkable": false, "function": "on_draw_lines_pressed" }, + "draw-bezier": { + "icon": "bezier_dark.svg", + "status_tip": "Draw Bezier curves", + "checkable": false, + "function": "on_draw_bezier_pressed" + }, + "add-distance-constraint": { + "icon": "distance_constraint_dark.svg", + "status_tip": "Add a distance constraint", + "checkable": false, + "function": "on_add_distance_constraint_pressed" + }, + "add-rel-angle-constraint": { + "icon": "rel_angle_constraint_dark.svg", + "status_tip": "Add a relative angle constraint", + "checkable": false, + "function": "on_add_rel_angle_constraint_pressed" + }, + "add-perp-constraint": { + "icon": "perp3_constraint_dark.svg", + "status_tip": "Add a perpendicular constraint", + "checkable": false, + "function": "on_add_perp_constraint_pressed" + }, + "add-anti-parallel-constraint": { + "icon": "anti_parallel_constraint_dark.svg", + "status_tip": "Add an anti-parallel constraint", + "checkable": false, + "function": "on_add_anti_parallel_constraint_pressed" + }, + "add-symmetry-constraint": { + "icon": "symmetry_constraint_dark.svg", + "status_tip": "Add a symmetry constraint", + "checkable": false, + "function": "on_add_symmetry_constraint_pressed" + }, + "add-roc-constraint": { + "icon": "roc_constraint_dark.svg", + "status_tip": "Add a radius of curvature constraint", + "checkable": false, + "function": "on_add_roc_constraint_pressed" + }, "stop-optimization": { "icon": "stop.png", "status_tip": "Terminate optimization", diff --git a/pymead/gui/main_icon_toolbar.py b/pymead/gui/main_icon_toolbar.py index 1c5d28bc..a5aebb3d 100644 --- a/pymead/gui/main_icon_toolbar.py +++ b/pymead/gui/main_icon_toolbar.py @@ -81,5 +81,26 @@ def on_draw_points_pressed(self): def on_draw_lines_pressed(self): self.parent.airfoil_canvas.drawLines() + def on_draw_bezier_pressed(self): + self.parent.airfoil_canvas.drawBeziers() + + def on_add_distance_constraint_pressed(self): + self.parent.airfoil_canvas.addDistanceConstraint() + + def on_add_rel_angle_constraint_pressed(self): + self.parent.airfoil_canvas.addRelAngle3Constraint() + + def on_add_perp_constraint_pressed(self): + self.parent.airfoil_canvas.addPerp3Constraint() + + def on_add_anti_parallel_constraint_pressed(self): + self.parent.airfoil_canvas.addAntiParallel3Constraint() + + def on_add_symmetry_constraint_pressed(self): + self.parent.airfoil_canvas.addSymmetryConstraint() + + def on_add_roc_constraint_pressed(self): + self.parent.airfoil_canvas.addROCurvatureConstraint() + def on_help_button_pressed(self): self.parent.show_help() diff --git a/pymead/icons/anti_parallel_constraint_dark.svg b/pymead/icons/anti_parallel_constraint_dark.svg new file mode 100644 index 00000000..38237dff --- /dev/null +++ b/pymead/icons/anti_parallel_constraint_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pymead/icons/anti_parallel_constraint_light.svg b/pymead/icons/anti_parallel_constraint_light.svg new file mode 100644 index 00000000..26852ffc --- /dev/null +++ b/pymead/icons/anti_parallel_constraint_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pymead/icons/bezier_dark.svg b/pymead/icons/bezier_dark.svg new file mode 100644 index 00000000..f9a9057d --- /dev/null +++ b/pymead/icons/bezier_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pymead/icons/bezier_light.svg b/pymead/icons/bezier_light.svg new file mode 100644 index 00000000..140af7e6 --- /dev/null +++ b/pymead/icons/bezier_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pymead/icons/distance_constraint_dark.svg b/pymead/icons/distance_constraint_dark.svg new file mode 100644 index 00000000..6be7a0e9 --- /dev/null +++ b/pymead/icons/distance_constraint_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pymead/icons/distance_constraint_light.svg b/pymead/icons/distance_constraint_light.svg new file mode 100644 index 00000000..95051f51 --- /dev/null +++ b/pymead/icons/distance_constraint_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pymead/icons/line.svg b/pymead/icons/line.svg deleted file mode 100644 index 37a4a6e1..00000000 --- a/pymead/icons/line.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/pymead/icons/line_dark.svg b/pymead/icons/line_dark.svg new file mode 100644 index 00000000..6eb89f6e --- /dev/null +++ b/pymead/icons/line_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pymead/icons/line_light.svg b/pymead/icons/line_light.svg new file mode 100644 index 00000000..3513d62c --- /dev/null +++ b/pymead/icons/line_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pymead/icons/perp3_constraint_dark.svg b/pymead/icons/perp3_constraint_dark.svg new file mode 100644 index 00000000..a2672cff --- /dev/null +++ b/pymead/icons/perp3_constraint_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pymead/icons/perp3_constraint_light.svg b/pymead/icons/perp3_constraint_light.svg new file mode 100644 index 00000000..75995923 --- /dev/null +++ b/pymead/icons/perp3_constraint_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pymead/icons/point.svg b/pymead/icons/point_dark.svg similarity index 92% rename from pymead/icons/point.svg rename to pymead/icons/point_dark.svg index 936f190a..572ec0ed 100644 --- a/pymead/icons/point.svg +++ b/pymead/icons/point_dark.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/pymead/icons/point_light.svg b/pymead/icons/point_light.svg new file mode 100644 index 00000000..48794e23 --- /dev/null +++ b/pymead/icons/point_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pymead/icons/rel_angle_constraint_dark.svg b/pymead/icons/rel_angle_constraint_dark.svg new file mode 100644 index 00000000..f537fed0 --- /dev/null +++ b/pymead/icons/rel_angle_constraint_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pymead/icons/rel_angle_constraint_light.svg b/pymead/icons/rel_angle_constraint_light.svg new file mode 100644 index 00000000..b8315a53 --- /dev/null +++ b/pymead/icons/rel_angle_constraint_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pymead/icons/roc_constraint_dark.svg b/pymead/icons/roc_constraint_dark.svg new file mode 100644 index 00000000..995b1ee5 --- /dev/null +++ b/pymead/icons/roc_constraint_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pymead/icons/roc_constraint_light.svg b/pymead/icons/roc_constraint_light.svg new file mode 100644 index 00000000..a4529e23 --- /dev/null +++ b/pymead/icons/roc_constraint_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pymead/icons/symmetry_constraint.drawio.png b/pymead/icons/symmetry_constraint.drawio.png deleted file mode 100644 index 7fcb5949..00000000 Binary files a/pymead/icons/symmetry_constraint.drawio.png and /dev/null differ diff --git a/pymead/icons/symmetry_constraint_dark.svg b/pymead/icons/symmetry_constraint_dark.svg new file mode 100644 index 00000000..27ade121 --- /dev/null +++ b/pymead/icons/symmetry_constraint_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pymead/icons/symmetry_constraint_light.svg b/pymead/icons/symmetry_constraint_light.svg new file mode 100644 index 00000000..61411675 --- /dev/null +++ b/pymead/icons/symmetry_constraint_light.svg @@ -0,0 +1 @@ + \ No newline at end of file