From bd886dee4c25edc877caebc6bc3bf3edca562cdc Mon Sep 17 00:00:00 2001 From: AU Date: Fri, 31 May 2024 22:10:33 +0200 Subject: [PATCH 01/45] Show more objects by default --- cq_editor/cq_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cq_editor/cq_utils.py b/cq_editor/cq_utils.py index a42836cc..356d395d 100644 --- a/cq_editor/cq_utils.py +++ b/cq_editor/cq_utils.py @@ -17,9 +17,11 @@ DEFAULT_FACE_COLOR = Quantity_Color(GOLD) DEFAULT_MATERIAL = Graphic3d_MaterialAspect(Graphic3d_NOM_JADE) +CQ_OBJECTS = (cq.Workplane, cq.Shape, cq.Assembly, cq.Sketch) + def find_cq_objects(results : dict): - return {k:SimpleNamespace(shape=v,options={}) for k,v in results.items() if isinstance(v,cq.Workplane)} + return {k:SimpleNamespace(shape=v,options={}) for k,v in results.items() if isinstance(v, CQ_OBJECTS)} def to_compound(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Sketch]): From da95450fbe6863589db2d17355af64505e5eb6a1 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 2 Jun 2024 11:16:16 +0200 Subject: [PATCH 02/45] Better instance check --- cq_editor/cq_utils.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/cq_editor/cq_utils.py b/cq_editor/cq_utils.py index 356d395d..61bb1302 100644 --- a/cq_editor/cq_utils.py +++ b/cq_editor/cq_utils.py @@ -3,7 +3,7 @@ from typing import List, Union from imp import reload -from types import SimpleNamespace +from types import SimpleNamespace, ModuleType from OCP.XCAFPrs import XCAFPrs_AISObject from OCP.TopoDS import TopoDS_Shape @@ -18,10 +18,21 @@ DEFAULT_MATERIAL = Graphic3d_MaterialAspect(Graphic3d_NOM_JADE) CQ_OBJECTS = (cq.Workplane, cq.Shape, cq.Assembly, cq.Sketch) +CQ_OBJECT_NAMES = tuple(f'{el.__module__}.{el.__qualname__}' for el in CQ_OBJECTS) + + +def is_cq_instance(obj): + + t = type(obj) + rv = f'{t.__module__}.{t.__qualname__}' in CQ_OBJECT_NAMES + + return rv + def find_cq_objects(results : dict): - return {k:SimpleNamespace(shape=v,options={}) for k,v in results.items() if isinstance(v, CQ_OBJECTS)} + return {k:SimpleNamespace(shape=v,options={}) for k,v in results.items() if is_cq_instance(v)} + def to_compound(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Sketch]): @@ -49,6 +60,7 @@ def to_compound(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq. return cq.Compound.makeCompound(vals) + def to_workplane(obj : cq.Shape): rv = cq.Workplane('XY') @@ -56,6 +68,7 @@ def to_workplane(obj : cq.Shape): return rv + def make_AIS(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Assembly, AIS_InteractiveObject], options={}): @@ -84,6 +97,7 @@ def make_AIS(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Sha return ais,shape + def export(obj : Union[cq.Workplane, List[cq.Workplane]], type : str, file, precision=1e-1): @@ -96,6 +110,7 @@ def export(obj : Union[cq.Workplane, List[cq.Workplane]], type : str, elif type == 'brep': comp.exportBrep(file) + def to_occ_color(color) -> Quantity_Color: if not isinstance(color, QColor): @@ -114,6 +129,7 @@ def to_occ_color(color) -> Quantity_Color: color.blueF(), TOC_RGB) + def get_occ_color(obj : Union[AIS_InteractiveObject, Quantity_Color]) -> QColor: if isinstance(obj, AIS_InteractiveObject): @@ -124,6 +140,7 @@ def get_occ_color(obj : Union[AIS_InteractiveObject, Quantity_Color]) -> QColor: return QColor.fromRgbF(color.Red(), color.Green(), color.Blue()) + def set_color(ais : AIS_Shape, color : Quantity_Color) -> AIS_Shape: drawer = ais.Attributes() @@ -132,6 +149,7 @@ def set_color(ais : AIS_Shape, color : Quantity_Color) -> AIS_Shape: return ais + def set_material(ais : AIS_Shape, material: Graphic3d_MaterialAspect) -> AIS_Shape: drawer = ais.Attributes() @@ -140,6 +158,7 @@ def set_material(ais : AIS_Shape, material: Graphic3d_MaterialAspect) -> AIS_Sha return ais + def set_transparency(ais : AIS_Shape, alpha: float) -> AIS_Shape: drawer = ais.Attributes() @@ -148,6 +167,7 @@ def set_transparency(ais : AIS_Shape, alpha: float) -> AIS_Shape: return ais + def reload_cq(): # NB: order of reloads is important From d651f7e606212ce4992b5af0e88367e45cbd6b4a Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 2 Jun 2024 20:21:16 +0200 Subject: [PATCH 03/45] Istance check workaround CQ reloading is not robust, so for now going to use this workaround --- cq_editor/cq_utils.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/cq_editor/cq_utils.py b/cq_editor/cq_utils.py index 61bb1302..b024f040 100644 --- a/cq_editor/cq_utils.py +++ b/cq_editor/cq_utils.py @@ -17,21 +17,17 @@ DEFAULT_FACE_COLOR = Quantity_Color(GOLD) DEFAULT_MATERIAL = Graphic3d_MaterialAspect(Graphic3d_NOM_JADE) -CQ_OBJECTS = (cq.Workplane, cq.Shape, cq.Assembly, cq.Sketch) -CQ_OBJECT_NAMES = tuple(f'{el.__module__}.{el.__qualname__}' for el in CQ_OBJECTS) +def is_cq_obj(obj): -def is_cq_instance(obj): + from cadquery import Workplane, Shape, Assembly, Sketch - t = type(obj) - rv = f'{t.__module__}.{t.__qualname__}' in CQ_OBJECT_NAMES - - return rv + return isinstance(obj, (Workplane, Shape, Assembly, Sketch)) def find_cq_objects(results : dict): - return {k:SimpleNamespace(shape=v,options={}) for k,v in results.items() if is_cq_instance(v)} + return {k:SimpleNamespace(shape=v,options={}) for k,v in results.items() if is_cq_obj(v)} def to_compound(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Sketch]): From 94fa99fe5eee49c8ec0352d7ed88b32f8deb635f Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 2 Jun 2024 20:21:53 +0200 Subject: [PATCH 04/45] Add/fix tests --- tests/test_app.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index eb7463e0..0621024d 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -84,6 +84,13 @@ r1 = cq.Workplane(solid1).translate((10, 0, 0)) """ +code_show_all = """import cadquery as cq +b = cq.Workplane().box(1,1,1) +sh = b.val() +a = cq.Assembly().add(sh) +sk = cq.Sketch().rect(1,1) +""" + def _modify_file(code, path="test.py"): with open(path, "w", 1) as f: f.write(code) @@ -251,11 +258,11 @@ def test_render(main): debugger._actions['Run'][0].triggered.emit() qtbot.wait(100) - assert(obj_tree_comp.CQ.childCount() == 1) + assert(obj_tree_comp.CQ.childCount() == 3) debugger._actions['Run'][0].triggered.emit() qtbot.wait(100) - assert(obj_tree_comp.CQ.childCount() == 1) + assert(obj_tree_comp.CQ.childCount() == 3) def test_export(main,mocker): @@ -1488,3 +1495,22 @@ def test_modulefinder(tmp_path, main): qtbot.wait(100) assert("Cannot determine imported modules" in log.toPlainText().splitlines()[-1]) +def test_show_all(main): + + qtbot, win = main + + editor = win.components['editor'] + debugger = win.components['debugger'] + object_tree = win.components['object_tree'] + + # remove all objects + object_tree.removeObjects() + assert(object_tree.CQ.childCount() == 0) + + # add code wtih Shape, Workplane, Assy, Sketch + editor.set_text(code_show_all) + + # Run and check if all are shown + debugger._actions['Run'][0].triggered.emit() + + assert(object_tree.CQ.childCount() == 4) From e8a26072cdecd55f984b57feffd48ad137138790 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 20 Sep 2024 07:56:02 +0200 Subject: [PATCH 05/45] Support showing Vector, Location, Plane and List[...] --- cq_editor/cq_utils.py | 171 ++++++++++++++++++++++++++++++------------ 1 file changed, 124 insertions(+), 47 deletions(-) diff --git a/cq_editor/cq_utils.py b/cq_editor/cq_utils.py index 32115a3f..f03172b6 100644 --- a/cq_editor/cq_utils.py +++ b/cq_editor/cq_utils.py @@ -5,46 +5,70 @@ from importlib import reload from types import SimpleNamespace +from typish import instance_of as isinstance + from OCP.XCAFPrs import XCAFPrs_AISObject from OCP.TopoDS import TopoDS_Shape -from OCP.AIS import AIS_InteractiveObject, AIS_Shape -from OCP.Quantity import \ - Quantity_TOC_RGB as TOC_RGB, Quantity_Color, Quantity_NOC_GOLD as GOLD +from OCP.AIS import AIS_InteractiveObject, AIS_Shape, AIS_Trihedron +from OCP.Prs3d import Prs3d_DatumParts, Prs3d_DatumMode +from OCP.TCollection import TCollection_ExtendedString +from OCP.gp import gp_Ax2 +from OCP.Geom import Geom_Axis2Placement +from OCP.Quantity import ( + Quantity_TOC_RGB as TOC_RGB, + Quantity_Color, + Quantity_NOC_GOLD as GOLD, + Quantity_NOC_BLUE2, + Quantity_NOC_GREEN2, + Quantity_NOC_RED2, +) from OCP.Graphic3d import Graphic3d_NOM_JADE, Graphic3d_MaterialAspect from PyQt5.QtGui import QColor DEFAULT_FACE_COLOR = Quantity_Color(GOLD) DEFAULT_MATERIAL = Graphic3d_MaterialAspect(Graphic3d_NOM_JADE) +DEFAULT_TRIHEDRON_SIZE = 0.1 + +CompoundLike = Union[cq.Shape, cq.Workplane, cq.Sketch, cq.Assembly] +AISLike = Union[ CompoundLike, cq.Location, cq.Plane, cq.Vector, AIS_InteractiveObject] +AISLikeLists = Union[tuple(List[T] for T in AISLike.__args__)] def is_cq_obj(obj): from cadquery import Workplane, Shape, Assembly, Sketch - return isinstance(obj, (Workplane, Shape, Assembly, Sketch)) + return isinstance(obj, Workplane, Shape, Assembly, Sketch) -def find_cq_objects(results : dict): +def find_cq_objects(results: dict): - return {k:SimpleNamespace(shape=v,options={}) for k,v in results.items() if is_cq_obj(v)} + return { + k: SimpleNamespace(shape=v, options={}) + for k, v in results.items() + if is_cq_obj(v) + } -def to_compound(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Sketch]): +def to_compound( + obj: Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Sketch] +): vals = [] - if isinstance(obj,cq.Workplane): + if isinstance(obj, cq.Workplane): vals.extend(obj.vals()) - elif isinstance(obj,cq.Shape): + elif isinstance(obj, cq.Shape): vals.append(obj) - elif isinstance(obj,list) and isinstance(obj[0],cq.Workplane): - for o in obj: vals.extend(o.vals()) - elif isinstance(obj,list) and isinstance(obj[0],cq.Shape): + elif isinstance(obj, list) and isinstance(obj[0], cq.Workplane): + for o in obj: + vals.extend(o.vals()) + elif isinstance(obj, list) and isinstance(obj[0], cq.Shape): vals.extend(obj) elif isinstance(obj, TopoDS_Shape): vals.append(cq.Shape.cast(obj)) - elif isinstance(obj,list) and isinstance(obj[0],TopoDS_Shape): + elif isinstance(obj, list) and isinstance(obj[0], TopoDS_Shape): vals.extend(cq.Shape.cast(o) for o in obj) elif isinstance(obj, cq.Sketch): if obj._faces: @@ -52,58 +76,114 @@ def to_compound(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq. else: vals.extend(obj._edges) else: - raise ValueError(f'Invalid type {type(obj)}') + raise ValueError(f"Invalid type {type(obj)}") return cq.Compound.makeCompound(vals) -def to_workplane(obj : cq.Shape): +def to_workplane(obj: cq.Shape): - rv = cq.Workplane('XY') - rv.objects = [obj,] + rv = cq.Workplane("XY") + rv.objects = [ + obj, + ] return rv -def make_AIS(obj : Union[cq.Workplane, List[cq.Workplane], cq.Shape, List[cq.Shape], cq.Assembly, AIS_InteractiveObject], - options={}): +def make_trihedron(ax): + + ais = AIS_Trihedron(Geom_Axis2Placement(ax)) + ais.SetSize(DEFAULT_TRIHEDRON_SIZE) + + # disable labels + for ax in ( + Prs3d_DatumParts.Prs3d_DatumParts_XAxis, + Prs3d_DatumParts.Prs3d_DatumParts_YAxis, + Prs3d_DatumParts.Prs3d_DatumParts_ZAxis, + ): + ais.SetLabel(ax, TCollection_ExtendedString()) + + ais.SetDatumPartColor( + Prs3d_DatumParts.Prs3d_DP_XAxis, Quantity_Color(Quantity_NOC_RED2) + ) + ais.SetDatumPartColor( + Prs3d_DatumParts.Prs3d_DP_YAxis, Quantity_Color(Quantity_NOC_GREEN2) + ) + ais.SetDatumPartColor( + Prs3d_DatumParts.Prs3d_DP_ZAxis, Quantity_Color(Quantity_NOC_BLUE2) + ) + + ais.SetDatumDisplayMode(Prs3d_DatumMode.Prs3d_DM_Shaded) + + return ais + + +def make_AIS( + obj: Union[ + AISLike, AISLikeLists + ], + options={}, +) -> List[AIS_InteractiveObject]: shape = None + ais = None + rv = [] if isinstance(obj, cq.Assembly): label, shape = toCAF(obj) ais = XCAFPrs_AISObject(label) + elif isinstance(obj, AIS_InteractiveObject): ais = obj - else: + + elif isinstance(obj, cq.Location): + ax = gp_Ax2() + ax.Transform(obj.wrapped.Transformation()) + + ais = make_trihedron(ax) + + elif isinstance(obj, cq.Plane): + ais = make_trihedron(obj.lcs.Ax2()) + + elif isinstance(obj, CompoundLike): shape = to_compound(obj) ais = AIS_Shape(shape.wrapped) - set_material(ais, DEFAULT_MATERIAL) - set_color(ais, DEFAULT_FACE_COLOR) + elif isinstance(obj, AISLikeLists): + rv = [make_AIS(el, options)[0][0] for el in obj] + + if ais: + set_material(ais, DEFAULT_MATERIAL) + set_color(ais, DEFAULT_FACE_COLOR) + + if "alpha" in options: + set_transparency(ais, options["alpha"]) + if "color" in options: + set_color(ais, to_occ_color(options["color"])) + if "rgba" in options: + r, g, b, a = options["rgba"] + set_color(ais, to_occ_color((r, g, b))) + set_transparency(ais, a) + if "size" in options: + ais.SetSize(options["size"]) - if 'alpha' in options: - set_transparency(ais, options['alpha']) - if 'color' in options: - set_color(ais, to_occ_color(options['color'])) - if 'rgba' in options: - r,g,b,a = options['rgba'] - set_color(ais, to_occ_color((r,g,b))) - set_transparency(ais, a) + rv = [ais] - return ais,shape + return rv, shape -def export(obj : Union[cq.Workplane, List[cq.Workplane]], type : str, - file, precision=1e-1): +def export( + obj: Union[cq.Workplane, List[cq.Workplane]], type: str, file, precision=1e-1 +): comp = to_compound(obj) - if type == 'stl': + if type == "stl": comp.exportStl(file, tolerance=precision) - elif type == 'step': + elif type == "step": comp.exportStep(file) - elif type == 'brep': + elif type == "brep": comp.exportBrep(file) @@ -116,17 +196,14 @@ def to_occ_color(color) -> Quantity_Color: elif isinstance(color[0], float): color = QColor.fromRgbF(*color) else: - raise ValueError('Unknown color format') + raise ValueError("Unknown color format") else: color = QColor(color) - return Quantity_Color(color.redF(), - color.greenF(), - color.blueF(), - TOC_RGB) + return Quantity_Color(color.redF(), color.greenF(), color.blueF(), TOC_RGB) -def get_occ_color(obj : Union[AIS_InteractiveObject, Quantity_Color]) -> QColor: +def get_occ_color(obj: Union[AIS_InteractiveObject, Quantity_Color]) -> QColor: if isinstance(obj, AIS_InteractiveObject): color = Quantity_Color() @@ -137,7 +214,7 @@ def get_occ_color(obj : Union[AIS_InteractiveObject, Quantity_Color]) -> QColor: return QColor.fromRgbF(color.Red(), color.Green(), color.Blue()) -def set_color(ais : AIS_Shape, color : Quantity_Color) -> AIS_Shape: +def set_color(ais: AIS_Shape, color: Quantity_Color) -> AIS_Shape: drawer = ais.Attributes() drawer.SetupOwnShadingAspect() @@ -146,7 +223,7 @@ def set_color(ais : AIS_Shape, color : Quantity_Color) -> AIS_Shape: return ais -def set_material(ais : AIS_Shape, material: Graphic3d_MaterialAspect) -> AIS_Shape: +def set_material(ais: AIS_Shape, material: Graphic3d_MaterialAspect) -> AIS_Shape: drawer = ais.Attributes() drawer.SetupOwnShadingAspect() @@ -155,7 +232,7 @@ def set_material(ais : AIS_Shape, material: Graphic3d_MaterialAspect) -> AIS_Sha return ais -def set_transparency(ais : AIS_Shape, alpha: float) -> AIS_Shape: +def set_transparency(ais: AIS_Shape, alpha: float) -> AIS_Shape: drawer = ais.Attributes() drawer.SetupOwnShadingAspect() @@ -185,13 +262,13 @@ def reload_cq(): reload(cq.occ_impl.exporters.dxf) reload(cq.occ_impl.exporters.amf) reload(cq.occ_impl.exporters.json) - #reload(cq.occ_impl.exporters.assembly) + # reload(cq.occ_impl.exporters.assembly) reload(cq.occ_impl.exporters) reload(cq.assembly) reload(cq) -def is_obj_empty(obj : Union[cq.Workplane,cq.Shape]) -> bool: +def is_obj_empty(obj: Union[cq.Workplane, cq.Shape]) -> bool: rv = False From 2dc8d92eabf94ceaf1b4d159e268a39c6d900c1a Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 20 Sep 2024 07:57:07 +0200 Subject: [PATCH 06/45] Fixes realted to new object support --- cq_editor/widgets/object_tree.py | 48 +++++++++++++++++--------------- cq_editor/widgets/viewer.py | 17 ++++++++--- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/cq_editor/widgets/object_tree.py b/cq_editor/widgets/object_tree.py index 97e3c66e..1ea6f4f3 100644 --- a/cq_editor/widgets/object_tree.py +++ b/cq_editor/widgets/object_tree.py @@ -49,6 +49,9 @@ def __init__(self, children=self.props) self.properties['Name'] = name + + # use properties only of the first object + ais = ais[0] self.properties['Alpha'] = ais.Transparency() self.properties['Color'] = get_occ_color(ais) if ais and ais.HasColor() else get_occ_color(DEFAULT_FACE_COLOR) self.properties.sigTreeStateChanged.connect(self.propertiesChanged) @@ -57,18 +60,19 @@ def propertiesChanged(self, properties, changed): changed_prop = changed[0][0] - self.setData(0,0,self.properties['Name']) - self.ais.SetTransparency(self.properties['Alpha']) + for ais in self.ais: + self.setData(0,0,self.properties['Name']) + ais.SetTransparency(self.properties['Alpha']) - if changed_prop.name() == 'Color': - set_color(self.ais, to_occ_color(self.properties['Color'])) + if changed_prop.name() == 'Color': + set_color(ais, to_occ_color(self.properties['Color'])) - self.ais.Redisplay() + ais.Redisplay() - if self.properties['Visible']: - self.setCheckState(0,Qt.Checked) - else: - self.setCheckState(0,Qt.Unchecked) + if self.properties['Visible']: + self.setCheckState(0,Qt.Checked) + else: + self.setCheckState(0,Qt.Unchecked) if self.sig: self.sig.emit() @@ -129,7 +133,7 @@ def __init__(self,parent): root = tree.invisibleRootItem() root.addChild(self.CQ) root.addChild(self.Helpers) - + tree.expandToDepth(1) self._export_STL_action = \ @@ -209,11 +213,11 @@ def addLines(self): gp_Dir(*direction))) line = AIS_Line(line_placement) line.SetColor(to_occ_color(color)) - + self.Helpers.addChild(ObjectTreeItem(name, - ais=line)) + ais=[line])) - ais_list.append(line) + ais_list.append([line]) self.sigObjectsAdded.emit(ais_list) @@ -240,7 +244,7 @@ def addObjects(self,objects,clean=False,root=None): request_fit_view = True if root.childCount() == 0 else False preserve_props = self.preferences['Preserve properties on reload'] - + if preserve_props: current_props = self._current_properties() @@ -254,19 +258,19 @@ def addObjects(self,objects,clean=False,root=None): for name,obj in objects_f.items(): ais,shape_display = make_AIS(obj.shape,obj.options) - + child = ObjectTreeItem(name, shape=obj.shape, shape_display=shape_display, ais=ais, sig=self.sigObjectPropertiesChanged) - + if preserve_props and name in current_props: self._restore_properties(child,current_props) - + if child.properties['Visible']: ais_list.append(ais) - + root.addChild(child) if request_fit_view: @@ -294,9 +298,9 @@ def addObject(self,obj,name='',options={}): def removeObjects(self,objects=None): if objects: - removed_items_ais = [self.CQ.takeChild(i).ais for i in objects] + removed_items_ais = [el for i in objects for el in self.CQ.takeChild(i).ais] else: - removed_items_ais = [ch.ais for ch in self.CQ.takeChildren()] + removed_items_ais = [el for ch in self.CQ.takeChildren() for el in ch.ais] self.sigObjectsRemoved.emit(removed_items_ais) @@ -347,7 +351,7 @@ def handleSelection(self): return # emit list of all selected ais objects (might be empty) - ais_objects = [item.ais for item in items if item.parent() is self.CQ] + ais_objects = [el for item in items for el in item.ais if item.parent() is self.CQ] self.sigAISObjectsSelected.emit(ais_objects) # handle context menu and emit last selected CQ object (if present) @@ -379,7 +383,7 @@ def handleGraphicalSelection(self,shapes): for i in range(CQ.childCount()): item = CQ.child(i) for shape in shapes: - if item.ais.Shape().IsEqual(shape): + if any(el.Shape().IsEqual(shape) for el in item.ais): item.setSelected(True) @pyqtSlot(QTreeWidgetItem,int) diff --git a/cq_editor/widgets/viewer.py b/cq_editor/widgets/viewer.py index 9c5d620b..290f8c59 100644 --- a/cq_editor/widgets/viewer.py +++ b/cq_editor/widgets/viewer.py @@ -203,27 +203,36 @@ def display_many(self,ais_list,fit=None): context = self._get_context() for ais in ais_list: - context.Display(ais,True) + for el in ais: + context.Display(el, False) if self.preferences['Fit automatically'] and fit is None: self.fit() elif fit: self.fit() + else: + self.redraw() @pyqtSlot(QTreeWidgetItem,int) def update_item(self,item,col): ctx = self._get_context() if item.checkState(0): - ctx.Display(item.ais,True) + for el in item.ais: + ctx.Display(el, False) else: - ctx.Erase(item.ais,True) + for el in item.ais: + ctx.Erase(el, False) + + self.redraw() @pyqtSlot(list) def remove_items(self,ais_items): ctx = self._get_context() - for ais in ais_items: ctx.Erase(ais,True) + for ais in ais_items: ctx.Erase(ais, False) + + self.redraw() @pyqtSlot() def redraw(self): From e40c4878bebb6256fe65e9f0e16be54a56178f6d Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 20 Sep 2024 08:16:43 +0200 Subject: [PATCH 07/45] Add run cell and show --- cq_editor/main_window.py | 26 +++++++++++---- cq_editor/widgets/debugger.py | 21 +++++++++--- cq_editor/widgets/traceback_viewer.py | 48 +++++++++++++-------------- 3 files changed, 60 insertions(+), 35 deletions(-) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index 4bbee280..23979531 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -48,7 +48,7 @@ def __init__(self,parent=None, filename=None): self.prepare_statusbar() self.prepare_actions() - + self.components['object_tree'].addLines() self.prepare_console() @@ -74,7 +74,7 @@ def closeEvent(self,event): if self.components['editor'].document().isModified(): rv = confirm(self, 'Confirm close', 'Close without saving?') - + if rv: event.accept() super(MainWindow,self).closeEvent(event) @@ -187,7 +187,7 @@ def prepare_menubar(self): QAction(icon('about'), 'About', self,triggered=self.about)) - + menu_help.addAction( \ QAction('Check for CadQuery updates', self,triggered=self.check_for_cq_updates)) @@ -221,6 +221,8 @@ def prepare_actions(self): .connect(self.components['variables_viewer'].update_frame) self.components['debugger'].sigLocals\ .connect(self.components['console'].push_vars) + self.components['debugger'].sigRunCell\ + .connect(self.run_cell) self.components['object_tree'].sigObjectsAdded[list]\ .connect(self.components['viewer'].display_many) @@ -275,7 +277,7 @@ def prepare_console(self): console = self.components['console'] obj_tree = self.components['object_tree'] - + #application related items console.push_vars({'self' : self}) @@ -286,6 +288,16 @@ def prepare_console(self): 'cq' : cq, 'log' : Logger(self.name).info}) + + def run_cell(self): + + cell_code, _ = self.components['editor'].get_cell_as_executable_code() + cell_code = cell_code.strip() + cell_code = f'{cell_code}\n' # add line break - workaround for not executing in some cases + + self.components['console'].execute(cell_code, interactive=True) + + def fill_dummy(self): self.components['editor']\ @@ -323,12 +335,12 @@ def about(self): about_dialog( self, - f'About CQ-editor', + 'About CQ-editor', f'PyQt GUI for CadQuery.\nVersion: {__version__}.\nSource Code: https://github.com/CadQuery/CQ-editor', ) - + def check_for_cq_updates(self): - + check_gtihub_for_updates(self,cq) def documentation(self): diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index 70d5795f..bc7d7364 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -114,6 +114,7 @@ class Debugger(QObject,ComponentMixin): sigRendered = pyqtSignal(dict) sigLocals = pyqtSignal(dict) sigTraceback = pyqtSignal(object,str) + sigRunCell = pyqtSignal() sigFrameChanged = pyqtSignal(object) sigLineChanged = pyqtSignal(int) @@ -137,6 +138,11 @@ def __init__(self,parent): self, shortcut='F5', triggered=self.render), + QAction(icon('cell'), + 'Run cell', + self, + shortcut='ctrl+shift+F5', + triggered=self.sigRunCell.emit), QAction(icon('debug'), 'Debug', self, @@ -166,9 +172,9 @@ def __init__(self,parent): def get_current_script(self): return self.parent().components['editor'].get_text_with_eol() - + def get_current_script_path(self): - + filename = self.parent().components["editor"].filename if filename: return Path(filename).absolute() @@ -228,7 +234,7 @@ def _inject_locals(self,module): cq_objects = {} - def _show_object(obj,name=None, options={}): + def _show_object(obj, name=None, options={}): if name: cq_objects.update({name : SimpleNamespace(shape=obj,options=options)}) @@ -243,13 +249,20 @@ def _show_object(obj,name=None, options={}): #use id if not found name = str(id(obj)) - cq_objects.update({name : SimpleNamespace(shape=obj,options=options)}) + cq_objects.update( + {name : SimpleNamespace( + shape=obj if isinstance(obj, list) else [obj], + options=options + ) + } + ) def _debug(obj,name=None): _show_object(obj,name,options=dict(color='red',alpha=0.2)) module.__dict__['show_object'] = _show_object + module.__dict__['show'] = _show_object module.__dict__['debug'] = _debug module.__dict__['rand_color'] = self._rand_color module.__dict__['log'] = lambda x: info(str(x)) diff --git a/cq_editor/widgets/traceback_viewer.py b/cq_editor/widgets/traceback_viewer.py index 7d1051a0..1b7ed962 100644 --- a/cq_editor/widgets/traceback_viewer.py +++ b/cq_editor/widgets/traceback_viewer.py @@ -11,49 +11,49 @@ class TracebackTree(QTreeWidget): name = 'Traceback Viewer' - + def __init__(self,parent): - - super(TracebackTree,self).__init__(parent) + + super(TracebackTree,self).__init__(parent) self.setHeaderHidden(False) self.setItemsExpandable(False) self.setRootIsDecorated(False) self.setContextMenuPolicy(Qt.ActionsContextMenu) - + self.setColumnCount(3) self.setHeaderLabels(['File','Line','Code']) - - + + self.root = self.invisibleRootItem() class TracebackPane(QWidget,ComponentMixin): - + sigHighlightLine = pyqtSignal(int) - + def __init__(self,parent): - + super(TracebackPane,self).__init__(parent) - + self.tree = TracebackTree(self) self.current_exception = QLabel(self) self.current_exception.setStyleSheet(\ "QLabel {color : red; }"); - + layout(self, (self.current_exception, self.tree), self) - + self.tree.currentItemChanged.connect(self.handleSelection) - + @pyqtSlot(object,str) def addTraceback(self,exc_info,code): - + self.tree.clear() - + if exc_info: t,exc,tb = exc_info - + root = self.tree.root code = code.splitlines() @@ -72,13 +72,13 @@ def addTraceback(self,exc_info,code): exc_name = t.__name__ exc_msg = str(exc) - exc_msg = exc_msg.replace('<', '<').replace('>', '>') #replace <> + exc_msg = exc_msg.replace('<', '<').replace('>', '>') #replace <> self.current_exception.\ setText('{}: {}'.format(exc_name,exc_msg)) - + # handle the special case of a SyntaxError - if t is SyntaxError: + if t is SyntaxError: root.addChild(QTreeWidgetItem( [exc.filename, str(exc.lineno), @@ -87,13 +87,13 @@ def addTraceback(self,exc_info,code): else: self.current_exception.setText('') - @pyqtSlot(QTreeWidgetItem,QTreeWidgetItem) + @pyqtSlot(QTreeWidgetItem,QTreeWidgetItem) def handleSelection(self,item,*args): - + if item: f,line = item.data(0,0),int(item.data(1,0)) - + if '' in f: self.sigHighlightLine.emit(line) - - + + From d9ef9575df78cd8a2ae563cd86f2ea91b4dce493 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 3 Oct 2024 08:52:09 +0200 Subject: [PATCH 08/45] Overwrite cq.vis.show when executing --- cq_editor/widgets/debugger.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index bc7d7364..cad97f55 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -7,6 +7,8 @@ from inspect import currentframe import cadquery as cq +import cadquery.vis as cqvis + from PyQt5 import QtCore from PyQt5.QtCore import Qt, QObject, pyqtSlot, pyqtSignal, QEventLoop, QAbstractTableModel from PyQt5.QtWidgets import QAction, QTableView @@ -177,7 +179,7 @@ def get_current_script_path(self): filename = self.parent().components["editor"].filename if filename: - return Path(filename).absolute() + return Path(filename).abspath() def get_breakpoints(self): @@ -198,7 +200,7 @@ def compile_code(self, cq_script, cq_script_path=None): def _exec(self, code, locals_dict, globals_dict): with ExitStack() as stack: - p = (self.get_current_script_path() or Path("")).absolute().dirname() + p = (self.get_current_script_path() or Path("")).abspath().dirname() if self.preferences['Add script dir to path'] and p.exists(): sys.path.insert(0,p) @@ -268,12 +270,21 @@ def _debug(obj,name=None): module.__dict__['log'] = lambda x: info(str(x)) module.__dict__['cq'] = cq + # overwrite cq.vis.show + self.old_show = cqvis.show + cqvis.show = _show_object + return cq_objects, set(module.__dict__)-{'cq'} def _cleanup_locals(self,module,injected_names): for name in injected_names: module.__dict__.pop(name) + # restore cq.vis.show + self.old_show = cqvis.show + cqvis.show = self.old_show + + @pyqtSlot(bool) def render(self): @@ -307,6 +318,7 @@ def render(self): sys.last_traceback = exc_info[-1] self.sigTraceback.emit(exc_info, cq_script) + @property def breakpoints(self): return [ el[0] for el in self.get_breakpoints()] From 17c399173e9d1f9f5653ed142d129a7c0198ea9d Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 3 Oct 2024 08:52:28 +0200 Subject: [PATCH 09/45] Use abspath() iso absolute() --- cq_editor/widgets/editor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index 20bee81f..cc145052 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -145,10 +145,10 @@ def new(self): self.reset_modified() def open(self): - + if not self.confirm_discard(): return - curr_dir = Path(self.filename).absolute().dirname() + curr_dir = Path(self.filename).abspath().dirname() fname = get_open_filename(self.EXTENSIONS, curr_dir) if fname != '': self.load_from_file(fname) @@ -248,10 +248,10 @@ def autoreload(self, enabled): def reset_modified(self): self.document().setModified(False) - + @property def modified(self): - + return self.document().isModified() def saveComponentState(self,store): From 086e4652bc4207d4db6d792c661da874321bcd5a Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 3 Oct 2024 08:54:41 +0200 Subject: [PATCH 10/45] Apply black --- cq_editor/__main__.py | 10 +- cq_editor/cq_utils.py | 6 +- cq_editor/icons.py | 94 +++-- cq_editor/icons_res.py | 11 +- cq_editor/main_window.py | 416 ++++++++++++----------- cq_editor/mixins.py | 58 ++-- cq_editor/preferences.py | 82 ++--- cq_editor/utils.py | 203 ++++++----- cq_editor/widgets/console.py | 39 ++- cq_editor/widgets/cq_object_inspector.py | 127 +++---- cq_editor/widgets/debugger.py | 317 +++++++++-------- cq_editor/widgets/editor.py | 216 ++++++------ cq_editor/widgets/log.py | 55 +-- cq_editor/widgets/object_tree.py | 318 +++++++++-------- cq_editor/widgets/occt_widget.py | 162 +++++---- cq_editor/widgets/traceback_viewer.py | 77 ++--- cq_editor/widgets/viewer.py | 378 ++++++++++++-------- 17 files changed, 1410 insertions(+), 1159 deletions(-) diff --git a/cq_editor/__main__.py b/cq_editor/__main__.py index 0fc8f700..2298ea56 100644 --- a/cq_editor/__main__.py +++ b/cq_editor/__main__.py @@ -3,18 +3,18 @@ from PyQt5.QtWidgets import QApplication -NAME = 'CQ-editor' +NAME = "CQ-editor" -#need to initialize QApp here, otherewise svg icons do not work on windows -app = QApplication(sys.argv, - applicationName=NAME) +# need to initialize QApp here, otherewise svg icons do not work on windows +app = QApplication(sys.argv, applicationName=NAME) from .main_window import MainWindow + def main(): parser = argparse.ArgumentParser(description=NAME) - parser.add_argument('filename',nargs='?',default=None) + parser.add_argument("filename", nargs="?", default=None) args = parser.parse_args(app.arguments()[1:]) diff --git a/cq_editor/cq_utils.py b/cq_editor/cq_utils.py index f03172b6..8d62038f 100644 --- a/cq_editor/cq_utils.py +++ b/cq_editor/cq_utils.py @@ -31,7 +31,7 @@ DEFAULT_TRIHEDRON_SIZE = 0.1 CompoundLike = Union[cq.Shape, cq.Workplane, cq.Sketch, cq.Assembly] -AISLike = Union[ CompoundLike, cq.Location, cq.Plane, cq.Vector, AIS_InteractiveObject] +AISLike = Union[CompoundLike, cq.Location, cq.Plane, cq.Vector, AIS_InteractiveObject] AISLikeLists = Union[tuple(List[T] for T in AISLike.__args__)] @@ -120,9 +120,7 @@ def make_trihedron(ax): def make_AIS( - obj: Union[ - AISLike, AISLikeLists - ], + obj: Union[AISLike, AISLikeLists], options={}, ) -> List[AIS_InteractiveObject]: diff --git a/cq_editor/icons.py b/cq_editor/icons.py index 6d568baa..06ea06b5 100644 --- a/cq_editor/icons.py +++ b/cq_editor/icons.py @@ -9,51 +9,75 @@ from PyQt5.QtGui import QIcon from . import icons_res -_icons = { - 'app' : QIcon(":/images/icons/cadquery_logo_dark.svg") - } + +_icons = {"app": QIcon(":/images/icons/cadquery_logo_dark.svg")} import qtawesome as qta _icons_specs = { - 'new' : (('fa.file-o',),{}), - 'open' : (('fa.folder-open-o',),{}), + "new": (("fa.file-o",), {}), + "open": (("fa.folder-open-o",), {}), # borrowed from spider-ide - 'autoreload': [('fa.repeat', 'fa.clock-o'), {'options': [{'scale_factor': 0.75, 'offset': (-0.1, -0.1)}, {'scale_factor': 0.5, 'offset': (0.25, 0.25)}]}], - 'save' : (('fa.save',),{}), - 'save_as': (('fa.save','fa.pencil'), - {'options':[{'scale_factor': 1,}, - {'scale_factor': 0.8, - 'offset': (0.2, 0.2)}]}), - 'run' : (('fa.play',),{}), - 'delete' : (('fa.trash',),{}), - 'delete-many' : (('fa.trash','fa.trash',), - {'options' : \ - [{'scale_factor': 0.8, - 'offset': (0.2, 0.2), - 'color': 'gray'}, - {'scale_factor': 0.8}]}), - 'help' : (('fa.life-ring',),{}), - 'about': (('fa.info',),{}), - 'preferences' : (('fa.cogs',),{}), - 'inspect' : (('fa.cubes','fa.search'), - {'options' : \ - [{'scale_factor': 0.8, - 'offset': (0,0), - 'color': 'gray'},{}]}), - 'screenshot' : (('fa.camera',),{}), - 'screenshot-save' : (('fa.save','fa.camera'), - {'options' : \ - [{'scale_factor': 0.8}, - {'scale_factor': 0.8, - 'offset': (.2,.2)}]}) + "autoreload": [ + ("fa.repeat", "fa.clock-o"), + { + "options": [ + {"scale_factor": 0.75, "offset": (-0.1, -0.1)}, + {"scale_factor": 0.5, "offset": (0.25, 0.25)}, + ] + }, + ], + "save": (("fa.save",), {}), + "save_as": ( + ("fa.save", "fa.pencil"), + { + "options": [ + { + "scale_factor": 1, + }, + {"scale_factor": 0.8, "offset": (0.2, 0.2)}, + ] + }, + ), + "run": (("fa.play",), {}), + "delete": (("fa.trash",), {}), + "delete-many": ( + ( + "fa.trash", + "fa.trash", + ), + { + "options": [ + {"scale_factor": 0.8, "offset": (0.2, 0.2), "color": "gray"}, + {"scale_factor": 0.8}, + ] + }, + ), + "help": (("fa.life-ring",), {}), + "about": (("fa.info",), {}), + "preferences": (("fa.cogs",), {}), + "inspect": ( + ("fa.cubes", "fa.search"), + {"options": [{"scale_factor": 0.8, "offset": (0, 0), "color": "gray"}, {}]}, + ), + "screenshot": (("fa.camera",), {}), + "screenshot-save": ( + ("fa.save", "fa.camera"), + { + "options": [ + {"scale_factor": 0.8}, + {"scale_factor": 0.8, "offset": (0.2, 0.2)}, + ] + }, + ), } + def icon(name): if name in _icons: return _icons[name] - args,kwargs = _icons_specs[name] + args, kwargs = _icons_specs[name] - return qta.icon(*args,**kwargs) \ No newline at end of file + return qta.icon(*args, **kwargs) diff --git a/cq_editor/icons_res.py b/cq_editor/icons_res.py index 5f51a6b8..b7f3f893 100644 --- a/cq_editor/icons_res.py +++ b/cq_editor/icons_res.py @@ -9107,10 +9107,17 @@ \x00\x00\x00\xe4\x00\x00\x00\x00\x00\x01\x00\x01\x48\xc1\ " + def qInitResources(): - QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + QtCore.qRegisterResourceData( + 0x01, qt_resource_struct, qt_resource_name, qt_resource_data + ) + def qCleanupResources(): - QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + QtCore.qUnregisterResourceData( + 0x01, qt_resource_struct, qt_resource_name, qt_resource_data + ) + qInitResources() diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index 23979531..9768cce1 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -1,6 +1,6 @@ import sys -from PyQt5.QtWidgets import (QLabel, QMainWindow, QToolBar, QDockWidget, QAction) +from PyQt5.QtWidgets import QLabel, QMainWindow, QToolBar, QDockWidget, QAction from logbook import Logger import cadquery as cq @@ -14,42 +14,50 @@ from .widgets.log import LogViewer from . import __version__ -from .utils import dock, add_actions, open_url, about_dialog, check_gtihub_for_updates, confirm +from .utils import ( + dock, + add_actions, + open_url, + about_dialog, + check_gtihub_for_updates, + confirm, +) from .mixins import MainMixin from .icons import icon from .preferences import PreferencesWidget -class MainWindow(QMainWindow,MainMixin): +class MainWindow(QMainWindow, MainMixin): - name = 'CQ-Editor' - org = 'CadQuery' + name = "CQ-Editor" + org = "CadQuery" - def __init__(self,parent=None, filename=None): + def __init__(self, parent=None, filename=None): - super(MainWindow,self).__init__(parent) + super(MainWindow, self).__init__(parent) MainMixin.__init__(self) - self.setWindowIcon(icon('app')) + self.setWindowIcon(icon("app")) # Windows workaround - makes the correct task bar icon show up. if sys.platform == "win32": import ctypes - myappid = 'cq-editor' # arbitrary string + + myappid = "cq-editor" # arbitrary string ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) self.viewer = OCCViewer(self) self.setCentralWidget(self.viewer.canvas) self.prepare_panes() - self.registerComponent('viewer',self.viewer) + self.registerComponent("viewer", self.viewer) self.prepare_toolbar() self.prepare_menubar() self.prepare_statusbar() self.prepare_actions() - self.components['object_tree'].addLines() + self.components["object_tree"].addLines() self.prepare_console() @@ -61,78 +69,72 @@ def __init__(self,parent=None, filename=None): self.restoreWindow() if filename: - self.components['editor'].load_from_file(filename) + self.components["editor"].load_from_file(filename) self.restoreComponentState() - def closeEvent(self,event): + def closeEvent(self, event): self.saveWindow() self.savePreferences() self.saveComponentState() - if self.components['editor'].document().isModified(): + if self.components["editor"].document().isModified(): - rv = confirm(self, 'Confirm close', 'Close without saving?') + rv = confirm(self, "Confirm close", "Close without saving?") if rv: event.accept() - super(MainWindow,self).closeEvent(event) + super(MainWindow, self).closeEvent(event) else: event.ignore() else: - super(MainWindow,self).closeEvent(event) + super(MainWindow, self).closeEvent(event) def prepare_panes(self): - self.registerComponent('editor', - Editor(self), - lambda c : dock(c, - 'Editor', - self, - defaultArea='left')) - - self.registerComponent('object_tree', - ObjectTree(self), - lambda c: dock(c, - 'Objects', - self, - defaultArea='right')) - - self.registerComponent('console', - ConsoleWidget(self), - lambda c: dock(c, - 'Console', - self, - defaultArea='bottom')) - - self.registerComponent('traceback_viewer', - TracebackPane(self), - lambda c: dock(c, - 'Current traceback', - self, - defaultArea='bottom')) - - self.registerComponent('debugger',Debugger(self)) - - self.registerComponent('variables_viewer',LocalsView(self), - lambda c: dock(c, - 'Variables', - self, - defaultArea='right')) - - self.registerComponent('cq_object_inspector', - CQObjectInspector(self), - lambda c: dock(c, - 'CQ object inspector', - self, - defaultArea='right')) - self.registerComponent('log', - LogViewer(self), - lambda c: dock(c, - 'Log viewer', - self, - defaultArea='bottom')) + self.registerComponent( + "editor", + Editor(self), + lambda c: dock(c, "Editor", self, defaultArea="left"), + ) + + self.registerComponent( + "object_tree", + ObjectTree(self), + lambda c: dock(c, "Objects", self, defaultArea="right"), + ) + + self.registerComponent( + "console", + ConsoleWidget(self), + lambda c: dock(c, "Console", self, defaultArea="bottom"), + ) + + self.registerComponent( + "traceback_viewer", + TracebackPane(self), + lambda c: dock(c, "Current traceback", self, defaultArea="bottom"), + ) + + self.registerComponent("debugger", Debugger(self)) + + self.registerComponent( + "variables_viewer", + LocalsView(self), + lambda c: dock(c, "Variables", self, defaultArea="right"), + ) + + self.registerComponent( + "cq_object_inspector", + CQObjectInspector(self), + lambda c: dock(c, "CQ object inspector", self, defaultArea="right"), + ) + self.registerComponent( + "log", + LogViewer(self), + lambda c: dock(c, "Log viewer", self, defaultArea="bottom"), + ) for d in self.docks.values(): d.show() @@ -141,26 +143,27 @@ def prepare_menubar(self): menu = self.menuBar() - menu_file = menu.addMenu('&File') - menu_edit = menu.addMenu('&Edit') - menu_tools = menu.addMenu('&Tools') - menu_run = menu.addMenu('&Run') - menu_view = menu.addMenu('&View') - menu_help = menu.addMenu('&Help') - - #per component menu elements - menus = {'File' : menu_file, - 'Edit' : menu_edit, - 'Run' : menu_run, - 'Tools': menu_tools, - 'View' : menu_view, - 'Help' : menu_help} + menu_file = menu.addMenu("&File") + menu_edit = menu.addMenu("&Edit") + menu_tools = menu.addMenu("&Tools") + menu_run = menu.addMenu("&Run") + menu_view = menu.addMenu("&View") + menu_help = menu.addMenu("&Help") + + # per component menu elements + menus = { + "File": menu_file, + "Edit": menu_edit, + "Run": menu_run, + "Tools": menu_tools, + "View": menu_view, + "Help": menu_help, + } for comp in self.components.values(): - self.prepare_menubar_component(menus, - comp.menuActions()) + self.prepare_menubar_component(menus, comp.menuActions()) - #global menu elements + # global menu elements menu_view.addSeparator() for d in self.findChildren(QDockWidget): menu_view.addAction(d.toggleViewAction()) @@ -169,139 +172,168 @@ def prepare_menubar(self): for t in self.findChildren(QToolBar): menu_view.addAction(t.toggleViewAction()) - menu_edit.addAction( \ - QAction(icon('preferences'), - 'Preferences', - self,triggered=self.edit_preferences)) + menu_edit.addAction( + QAction( + icon("preferences"), + "Preferences", + self, + triggered=self.edit_preferences, + ) + ) - menu_help.addAction( \ - QAction(icon('help'), - 'Documentation', - self,triggered=self.documentation)) + menu_help.addAction( + QAction(icon("help"), "Documentation", self, triggered=self.documentation) + ) - menu_help.addAction( \ - QAction('CQ documentation', - self,triggered=self.cq_documentation)) + menu_help.addAction( + QAction("CQ documentation", self, triggered=self.cq_documentation) + ) - menu_help.addAction( \ - QAction(icon('about'), - 'About', - self,triggered=self.about)) + menu_help.addAction(QAction(icon("about"), "About", self, triggered=self.about)) - menu_help.addAction( \ - QAction('Check for CadQuery updates', - self,triggered=self.check_for_cq_updates)) + menu_help.addAction( + QAction( + "Check for CadQuery updates", self, triggered=self.check_for_cq_updates + ) + ) - def prepare_menubar_component(self,menus,comp_menu_dict): + def prepare_menubar_component(self, menus, comp_menu_dict): - for name,action in comp_menu_dict.items(): + for name, action in comp_menu_dict.items(): menus[name].addActions(action) def prepare_toolbar(self): - self.toolbar = QToolBar('Main toolbar',self,objectName='Main toolbar') + self.toolbar = QToolBar("Main toolbar", self, objectName="Main toolbar") for c in self.components.values(): - add_actions(self.toolbar,c.toolbarActions()) + add_actions(self.toolbar, c.toolbarActions()) self.addToolBar(self.toolbar) def prepare_statusbar(self): - self.status_label = QLabel('',parent=self) + self.status_label = QLabel("", parent=self) self.statusBar().insertPermanentWidget(0, self.status_label) def prepare_actions(self): - self.components['debugger'].sigRendered\ - .connect(self.components['object_tree'].addObjects) - self.components['debugger'].sigTraceback\ - .connect(self.components['traceback_viewer'].addTraceback) - self.components['debugger'].sigLocals\ - .connect(self.components['variables_viewer'].update_frame) - self.components['debugger'].sigLocals\ - .connect(self.components['console'].push_vars) - self.components['debugger'].sigRunCell\ - .connect(self.run_cell) - - self.components['object_tree'].sigObjectsAdded[list]\ - .connect(self.components['viewer'].display_many) - self.components['object_tree'].sigObjectsAdded[list,bool]\ - .connect(self.components['viewer'].display_many) - self.components['object_tree'].sigItemChanged.\ - connect(self.components['viewer'].update_item) - self.components['object_tree'].sigObjectsRemoved\ - .connect(self.components['viewer'].remove_items) - self.components['object_tree'].sigCQObjectSelected\ - .connect(self.components['cq_object_inspector'].setObject) - self.components['object_tree'].sigObjectPropertiesChanged\ - .connect(self.components['viewer'].redraw) - self.components['object_tree'].sigAISObjectsSelected\ - .connect(self.components['viewer'].set_selected) - - self.components['viewer'].sigObjectSelected\ - .connect(self.components['object_tree'].handleGraphicalSelection) - - self.components['traceback_viewer'].sigHighlightLine\ - .connect(self.components['editor'].go_to_line) - - self.components['cq_object_inspector'].sigDisplayObjects\ - .connect(self.components['viewer'].display_many) - self.components['cq_object_inspector'].sigRemoveObjects\ - .connect(self.components['viewer'].remove_items) - self.components['cq_object_inspector'].sigShowPlane\ - .connect(self.components['viewer'].toggle_grid) - self.components['cq_object_inspector'].sigShowPlane[bool,float]\ - .connect(self.components['viewer'].toggle_grid) - self.components['cq_object_inspector'].sigChangePlane\ - .connect(self.components['viewer'].set_grid_orientation) - - self.components['debugger'].sigLocalsChanged\ - .connect(self.components['variables_viewer'].update_frame) - self.components['debugger'].sigLineChanged\ - .connect(self.components['editor'].go_to_line) - self.components['debugger'].sigDebugging\ - .connect(self.components['object_tree'].stashObjects) - self.components['debugger'].sigCQChanged\ - .connect(self.components['object_tree'].addObjects) - self.components['debugger'].sigTraceback\ - .connect(self.components['traceback_viewer'].addTraceback) + self.components["debugger"].sigRendered.connect( + self.components["object_tree"].addObjects + ) + self.components["debugger"].sigTraceback.connect( + self.components["traceback_viewer"].addTraceback + ) + self.components["debugger"].sigLocals.connect( + self.components["variables_viewer"].update_frame + ) + self.components["debugger"].sigLocals.connect( + self.components["console"].push_vars + ) + self.components["debugger"].sigRunCell.connect(self.run_cell) - # trigger re-render when file is modified externally or saved - self.components['editor'].triggerRerender \ - .connect(self.components['debugger'].render) - self.components['editor'].sigFilenameChanged\ - .connect(self.handle_filename_change) + self.components["object_tree"].sigObjectsAdded[list].connect( + self.components["viewer"].display_many + ) + self.components["object_tree"].sigObjectsAdded[list, bool].connect( + self.components["viewer"].display_many + ) + self.components["object_tree"].sigItemChanged.connect( + self.components["viewer"].update_item + ) + self.components["object_tree"].sigObjectsRemoved.connect( + self.components["viewer"].remove_items + ) + self.components["object_tree"].sigCQObjectSelected.connect( + self.components["cq_object_inspector"].setObject + ) + self.components["object_tree"].sigObjectPropertiesChanged.connect( + self.components["viewer"].redraw + ) + self.components["object_tree"].sigAISObjectsSelected.connect( + self.components["viewer"].set_selected + ) - def prepare_console(self): + self.components["viewer"].sigObjectSelected.connect( + self.components["object_tree"].handleGraphicalSelection + ) - console = self.components['console'] - obj_tree = self.components['object_tree'] + self.components["traceback_viewer"].sigHighlightLine.connect( + self.components["editor"].go_to_line + ) - #application related items - console.push_vars({'self' : self}) + self.components["cq_object_inspector"].sigDisplayObjects.connect( + self.components["viewer"].display_many + ) + self.components["cq_object_inspector"].sigRemoveObjects.connect( + self.components["viewer"].remove_items + ) + self.components["cq_object_inspector"].sigShowPlane.connect( + self.components["viewer"].toggle_grid + ) + self.components["cq_object_inspector"].sigShowPlane[bool, float].connect( + self.components["viewer"].toggle_grid + ) + self.components["cq_object_inspector"].sigChangePlane.connect( + self.components["viewer"].set_grid_orientation + ) - #CQ related items - console.push_vars({'show' : obj_tree.addObject, - 'show_object' : obj_tree.addObject, - 'rand_color' : self.components['debugger']._rand_color, - 'cq' : cq, - 'log' : Logger(self.name).info}) + self.components["debugger"].sigLocalsChanged.connect( + self.components["variables_viewer"].update_frame + ) + self.components["debugger"].sigLineChanged.connect( + self.components["editor"].go_to_line + ) + self.components["debugger"].sigDebugging.connect( + self.components["object_tree"].stashObjects + ) + self.components["debugger"].sigCQChanged.connect( + self.components["object_tree"].addObjects + ) + self.components["debugger"].sigTraceback.connect( + self.components["traceback_viewer"].addTraceback + ) + # trigger re-render when file is modified externally or saved + self.components["editor"].triggerRerender.connect( + self.components["debugger"].render + ) + self.components["editor"].sigFilenameChanged.connect( + self.handle_filename_change + ) + + def prepare_console(self): + + console = self.components["console"] + obj_tree = self.components["object_tree"] + + # application related items + console.push_vars({"self": self}) + + # CQ related items + console.push_vars( + { + "show": obj_tree.addObject, + "show_object": obj_tree.addObject, + "rand_color": self.components["debugger"]._rand_color, + "cq": cq, + "log": Logger(self.name).info, + } + ) def run_cell(self): - cell_code, _ = self.components['editor'].get_cell_as_executable_code() + cell_code, _ = self.components["editor"].get_cell_as_executable_code() cell_code = cell_code.strip() - cell_code = f'{cell_code}\n' # add line break - workaround for not executing in some cases - - self.components['console'].execute(cell_code, interactive=True) + cell_code = f"{cell_code}\n" # add line break - workaround for not executing in some cases + self.components["console"].execute(cell_code, interactive=True) def fill_dummy(self): - self.components['editor']\ - .set_text('import cadquery as cq\nresult = cq.Workplane("XY" ).box(3, 3, 0.5).edges("|Z").fillet(0.125)') + self.components["editor"].set_text( + 'import cadquery as cq\nresult = cq.Workplane("XY" ).box(3, 3, 0.5).edges("|Z").fillet(0.125)' + ) def setup_logging(self): @@ -309,8 +341,8 @@ def setup_logging(self): from logbook import INFO, Logger redirect_logging() - self.components['log'].handler.level = INFO - self.components['log'].handler.push_application() + self.components["log"].handler.level = INFO + self.components["log"].handler.push_application() self._logger = Logger(self.name) @@ -320,42 +352,44 @@ def handle_exception(exc_type, exc_value, exc_traceback): sys.__excepthook__(exc_type, exc_value, exc_traceback) return - self._logger.error("Uncaught exception occurred", - exc_info=(exc_type, exc_value, exc_traceback)) + self._logger.error( + "Uncaught exception occurred", + exc_info=(exc_type, exc_value, exc_traceback), + ) sys.excepthook = handle_exception - def edit_preferences(self): - prefs = PreferencesWidget(self,self.components) + prefs = PreferencesWidget(self, self.components) prefs.exec_() def about(self): about_dialog( self, - 'About CQ-editor', - f'PyQt GUI for CadQuery.\nVersion: {__version__}.\nSource Code: https://github.com/CadQuery/CQ-editor', + "About CQ-editor", + f"PyQt GUI for CadQuery.\nVersion: {__version__}.\nSource Code: https://github.com/CadQuery/CQ-editor", ) def check_for_cq_updates(self): - check_gtihub_for_updates(self,cq) + check_gtihub_for_updates(self, cq) def documentation(self): - open_url('https://github.com/CadQuery') + open_url("https://github.com/CadQuery") def cq_documentation(self): - open_url('https://cadquery.readthedocs.io/en/latest/') + open_url("https://cadquery.readthedocs.io/en/latest/") def handle_filename_change(self, fname): new_title = fname if fname else "*" self.setWindowTitle(f"{self.name}: {new_title}") + if __name__ == "__main__": pass diff --git a/cq_editor/mixins.py b/cq_editor/mixins.py index f48f0d23..f614ffe6 100644 --- a/cq_editor/mixins.py +++ b/cq_editor/mixins.py @@ -12,10 +12,11 @@ from PyQt5.QtCore import pyqtSlot, QSettings + class MainMixin(object): - name = 'Main' - org = 'Unknown' + name = "Main" + org = "Unknown" components = {} docks = {} @@ -23,9 +24,9 @@ class MainMixin(object): def __init__(self): - self.settings = QSettings(self.org,self.name) + self.settings = QSettings(self.org, self.name) - def registerComponent(self,name,component,dock=None): + def registerComponent(self, name, component, dock=None): self.components[name] = component @@ -34,38 +35,40 @@ def registerComponent(self,name,component,dock=None): def saveWindow(self): - self.settings.setValue('geometry',self.saveGeometry()) - self.settings.setValue('windowState',self.saveState()) + self.settings.setValue("geometry", self.saveGeometry()) + self.settings.setValue("windowState", self.saveState()) def restoreWindow(self): - if self.settings.value('geometry'): - self.restoreGeometry(self.settings.value('geometry')) - if self.settings.value('windowState'): - self.restoreState(self.settings.value('windowState')) + if self.settings.value("geometry"): + self.restoreGeometry(self.settings.value("geometry")) + if self.settings.value("windowState"): + self.restoreState(self.settings.value("windowState")) def savePreferences(self): settings = self.settings if self.preferences: - settings.setValue('General',self.preferences.saveState()) + settings.setValue("General", self.preferences.saveState()) for comp in (c for c in self.components.values() if c.preferences): - settings.setValue(comp.name,comp.preferences.saveState()) + settings.setValue(comp.name, comp.preferences.saveState()) def restorePreferences(self): settings = self.settings - if self.preferences and settings.value('General'): - self.preferences.restoreState(settings.value('General'), - removeChildren=False) + if self.preferences and settings.value("General"): + self.preferences.restoreState( + settings.value("General"), removeChildren=False + ) for comp in (c for c in self.components.values() if c.preferences): if settings.value(comp.name): - comp.preferences.restoreState(settings.value(comp.name), - removeChildren=False) + comp.preferences.restoreState( + settings.value(comp.name), removeChildren=False + ) def saveComponentState(self): @@ -84,19 +87,16 @@ def restoreComponentState(self): class ComponentMixin(object): - - name = 'Component' + name = "Component" preferences = None _actions = {} - def __init__(self): if self.preferences: - self.preferences.sigTreeStateChanged.\ - connect(self.updatePreferences) - + self.preferences.sigTreeStateChanged.connect(self.updatePreferences) + self._logger = Logger(self.name) def menuActions(self): @@ -106,19 +106,19 @@ def menuActions(self): def toolbarActions(self): if len(self._actions) > 0: - return reduce(add,[a for a in self._actions.values()]) + return reduce(add, [a for a in self._actions.values()]) else: return [] - @pyqtSlot(object,object) - def updatePreferences(self,*args): + @pyqtSlot(object, object) + def updatePreferences(self, *args): pass - def saveComponentState(self,store): + def saveComponentState(self, store): pass - def restoreComponentState(self,store): + def restoreComponentState(self, store): - pass \ No newline at end of file + pass diff --git a/cq_editor/preferences.py b/cq_editor/preferences.py index 5dc947ca..e37c9ff8 100644 --- a/cq_editor/preferences.py +++ b/cq_editor/preferences.py @@ -1,5 +1,4 @@ -from PyQt5.QtWidgets import (QTreeWidget, QTreeWidgetItem, - QStackedWidget, QDialog) +from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QStackedWidget, QDialog from PyQt5.QtCore import pyqtSlot, Qt from pyqtgraph.parametertree import ParameterTree @@ -8,55 +7,58 @@ class PreferencesTreeItem(QTreeWidgetItem): - - def __init__(self,name,widget,): - - super(PreferencesTreeItem,self).__init__(name) + def __init__( + self, + name, + widget, + ): + + super(PreferencesTreeItem, self).__init__(name) self.widget = widget + class PreferencesWidget(QDialog): - - def __init__(self,parent,components): - - super(PreferencesWidget,self).__init__( - parent, - Qt.Window | Qt.CustomizeWindowHint | Qt.WindowCloseButtonHint, - windowTitle='Preferences') - + def __init__(self, parent, components): + + super(PreferencesWidget, self).__init__( + parent, + Qt.Window | Qt.CustomizeWindowHint | Qt.WindowCloseButtonHint, + windowTitle="Preferences", + ) + self.stacked = QStackedWidget(self) - self.preferences_tree = QTreeWidget(self, - headerHidden=True, - itemsExpandable=False, - rootIsDecorated=False, - columnCount=1) - + self.preferences_tree = QTreeWidget( + self, + headerHidden=True, + itemsExpandable=False, + rootIsDecorated=False, + columnCount=1, + ) + self.root = self.preferences_tree.invisibleRootItem() - - self.add('General', - parent) - + + self.add("General", parent) + for v in parent.components.values(): - self.add(v.name,v) - - self.splitter = splitter((self.preferences_tree,self.stacked),(2,5)) - layout(self,(self.splitter,),self) - + self.add(v.name, v) + + self.splitter = splitter((self.preferences_tree, self.stacked), (2, 5)) + layout(self, (self.splitter,), self) + self.preferences_tree.currentItemChanged.connect(self.handleSelection) - def add(self,name,component): - + def add(self, name, component): + if component.preferences: widget = ParameterTree() widget.setHeaderHidden(True) - widget.setParameters(component.preferences,showTop=False) - self.root.addChild(PreferencesTreeItem((name,), - widget)) - + widget.setParameters(component.preferences, showTop=False) + self.root.addChild(PreferencesTreeItem((name,), widget)) + self.stacked.addWidget(widget) - - @pyqtSlot(QTreeWidgetItem,QTreeWidgetItem) - def handleSelection(self,item,*args): - + + @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem) + def handleSelection(self, item, *args): + if item: self.stacked.setCurrentWidget(item.widget) - diff --git a/cq_editor/utils.py b/cq_editor/utils.py index 6e4cebcd..dde99ff0 100644 --- a/cq_editor/utils.py +++ b/cq_editor/utils.py @@ -7,17 +7,23 @@ from PyQt5.QtCore import QUrl from PyQt5.QtWidgets import QFileDialog, QMessageBox -DOCK_POSITIONS = {'right' : QtCore.Qt.RightDockWidgetArea, - 'left' : QtCore.Qt.LeftDockWidgetArea, - 'top' : QtCore.Qt.TopDockWidgetArea, - 'bottom' : QtCore.Qt.BottomDockWidgetArea} - -def layout(parent,items, - top_widget = None, - layout_type = QtWidgets.QVBoxLayout, - margin = 2, - spacing = 0): - +DOCK_POSITIONS = { + "right": QtCore.Qt.RightDockWidgetArea, + "left": QtCore.Qt.LeftDockWidgetArea, + "top": QtCore.Qt.TopDockWidgetArea, + "bottom": QtCore.Qt.BottomDockWidgetArea, +} + + +def layout( + parent, + items, + top_widget=None, + layout_type=QtWidgets.QVBoxLayout, + margin=2, + spacing=0, +): + if not top_widget: top_widget = QtWidgets.QWidget(parent) top_widget_was_none = True @@ -25,110 +31,133 @@ def layout(parent,items, top_widget_was_none = False layout = layout_type(top_widget) top_widget.setLayout(layout) - - for item in items: layout.addWidget(item) + + for item in items: + layout.addWidget(item) layout.setSpacing(spacing) - layout.setContentsMargins(margin,margin,margin,margin) - + layout.setContentsMargins(margin, margin, margin, margin) + if top_widget_was_none: return top_widget else: return layout - -def splitter(items, - stretch_factors = None, - orientation=QtCore.Qt.Horizontal): - + + +def splitter(items, stretch_factors=None, orientation=QtCore.Qt.Horizontal): + sp = QtWidgets.QSplitter(orientation) - - for item in items: sp.addWidget(item) - + + for item in items: + sp.addWidget(item) + if stretch_factors: - for i,s in enumerate(stretch_factors): - sp.setStretchFactor(i,s) - - + for i, s in enumerate(stretch_factors): + sp.setStretchFactor(i, s) + return sp -def dock(widget, - title, - parent, - allowedAreas = QtCore.Qt.AllDockWidgetAreas, - defaultArea = 'right', - name=None, - icon = None): - - dock = QtWidgets.QDockWidget(title,parent,objectName=title) - - if name: dock.setObjectName(name) - if icon: dock.toggleViewAction().setIcon(icon) - + +def dock( + widget, + title, + parent, + allowedAreas=QtCore.Qt.AllDockWidgetAreas, + defaultArea="right", + name=None, + icon=None, +): + + dock = QtWidgets.QDockWidget(title, parent, objectName=title) + + if name: + dock.setObjectName(name) + if icon: + dock.toggleViewAction().setIcon(icon) + dock.setAllowedAreas(allowedAreas) dock.setWidget(widget) action = dock.toggleViewAction() action.setText(title) - - dock.setFeatures(QtWidgets.QDockWidget.DockWidgetFeatures(\ - QtWidgets.QDockWidget.AllDockWidgetFeatures)) - - parent.addDockWidget(DOCK_POSITIONS[defaultArea], - dock) - + + dock.setFeatures( + QtWidgets.QDockWidget.DockWidgetFeatures( + QtWidgets.QDockWidget.AllDockWidgetFeatures + ) + ) + + parent.addDockWidget(DOCK_POSITIONS[defaultArea], dock) + return dock -def add_actions(menu,actions): - + +def add_actions(menu, actions): + if len(actions) > 0: menu.addActions(actions) menu.addSeparator() - + + def open_url(url): - - QDesktopServices.openUrl(QUrl(url)) - -def about_dialog(parent,title,text): - - QtWidgets.QMessageBox.about(parent,title,text) - + + QDesktopServices.openUrl(QUrl(url)) + + +def about_dialog(parent, title, text): + + QtWidgets.QMessageBox.about(parent, title, text) + + def get_save_filename(suffix): - - rv,_ = QFileDialog.getSaveFileName(filter='*.{}'.format(suffix)) - if rv != '' and not rv.endswith(suffix): rv += '.'+suffix - + + rv, _ = QFileDialog.getSaveFileName(filter="*.{}".format(suffix)) + if rv != "" and not rv.endswith(suffix): + rv += "." + suffix + return rv + def get_open_filename(suffix, curr_dir): - - rv,_ = QFileDialog.getOpenFileName(directory=curr_dir, filter='*.{}'.format(suffix)) - if rv != '' and not rv.endswith(suffix): rv += '.'+suffix - + + rv, _ = QFileDialog.getOpenFileName( + directory=curr_dir, filter="*.{}".format(suffix) + ) + if rv != "" and not rv.endswith(suffix): + rv += "." + suffix + return rv -def check_gtihub_for_updates(parent, - mod, - github_org='cadquery', - github_proj='cadquery'): - - url = f'https://api.github.com/repos/{github_org}/{github_proj}/releases' + +def check_gtihub_for_updates( + parent, mod, github_org="cadquery", github_proj="cadquery" +): + + url = f"https://api.github.com/repos/{github_org}/{github_proj}/releases" resp = requests.get(url).json() - - newer = [el['tag_name'] for el in resp if not el['draft'] and \ - parse_version(el['tag_name']) > parse_version(mod.__version__)] - + + newer = [ + el["tag_name"] + for el in resp + if not el["draft"] + and parse_version(el["tag_name"]) > parse_version(mod.__version__) + ] + if newer: - title='Updates available' - text=f'There are newer versions of {github_proj} ' \ - f'available on github:\n' + '\n'.join(newer) - + title = "Updates available" + text = ( + f"There are newer versions of {github_proj} " + f"available on github:\n" + "\n".join(newer) + ) + else: - title='No updates available' - text=f'You are already using the latest version of {github_proj}' - - QtWidgets.QMessageBox.about(parent,title,text) - -def confirm(parent,title,msg): - + title = "No updates available" + text = f"You are already using the latest version of {github_proj}" + + QtWidgets.QMessageBox.about(parent, title, text) + + +def confirm(parent, title, msg): + rv = QMessageBox.question(parent, title, msg, QMessageBox.Yes, QMessageBox.No) - + return True if rv == QMessageBox.Yes else False diff --git a/cq_editor/widgets/console.py b/cq_editor/widgets/console.py index 77fd1dcc..3ed51a74 100644 --- a/cq_editor/widgets/console.py +++ b/cq_editor/widgets/console.py @@ -6,22 +6,23 @@ from ..mixins import ComponentMixin -class ConsoleWidget(RichJupyterWidget,ComponentMixin): - - name = 'Console' + +class ConsoleWidget(RichJupyterWidget, ComponentMixin): + + name = "Console" def __init__(self, customBanner=None, namespace=dict(), *args, **kwargs): super(ConsoleWidget, self).__init__(*args, **kwargs) -# if not customBanner is None: -# self.banner = customBanner + # if not customBanner is None: + # self.banner = customBanner self.font_size = 6 self.kernel_manager = kernel_manager = QtInProcessKernelManager() kernel_manager.start_kernel(show_banner=False) - kernel_manager.kernel.gui = 'qt' + kernel_manager.kernel.gui = "qt" kernel_manager.kernel.shell.banner1 = "" - + self.kernel_client = kernel_client = self._kernel_manager.client() kernel_client.start_channels() @@ -31,9 +32,9 @@ def stop(): QApplication.instance().exit() self.exit_requested.connect(stop) - + self.clear() - + self.push_vars(namespace) @pyqtSlot(dict) @@ -50,7 +51,6 @@ def clear(self): """ self._control.clear() - def print_text(self, text): """ Prints some plain text to the console @@ -62,20 +62,19 @@ def execute_command(self, command): Execute a command in the frame of the console widget """ self._execute(command, False) - + def _banner_default(self): - - return '' - + return "" + + if __name__ == "__main__": - - + import sys - + app = QApplication(sys.argv) - - console = ConsoleWidget(customBanner='IPython console test') + + console = ConsoleWidget(customBanner="IPython console test") console.show() - + sys.exit(app.exec_()) diff --git a/cq_editor/widgets/cq_object_inspector.py b/cq_editor/widgets/cq_object_inspector.py index c8c3d37c..9fc01eba 100644 --- a/cq_editor/widgets/cq_object_inspector.py +++ b/cq_editor/widgets/cq_object_inspector.py @@ -10,63 +10,68 @@ from ..icons import icon - class CQChildItem(QTreeWidgetItem): - - def __init__(self,cq_item,**kwargs): - - super(CQChildItem,self).\ - __init__([type(cq_item).__name__,str(cq_item)],**kwargs) - + def __init__(self, cq_item, **kwargs): + + super(CQChildItem, self).__init__( + [type(cq_item).__name__, str(cq_item)], **kwargs + ) + self.cq_item = cq_item + class CQStackItem(QTreeWidgetItem): - - def __init__(self,name,workplane=None,**kwargs): - - super(CQStackItem,self).__init__([name,''],**kwargs) - + def __init__(self, name, workplane=None, **kwargs): + + super(CQStackItem, self).__init__([name, ""], **kwargs) + self.workplane = workplane -class CQObjectInspector(QTreeWidget,ComponentMixin): - - name = 'CQ Object Inspector' - +class CQObjectInspector(QTreeWidget, ComponentMixin): + + name = "CQ Object Inspector" + sigRemoveObjects = pyqtSignal(list) - sigDisplayObjects = pyqtSignal(list,bool) - sigShowPlane = pyqtSignal([bool],[bool,float]) + sigDisplayObjects = pyqtSignal(list, bool) + sigShowPlane = pyqtSignal([bool], [bool, float]) sigChangePlane = pyqtSignal(gp_Ax3) - - def __init__(self,parent): - - super(CQObjectInspector,self).__init__(parent) + + def __init__(self, parent): + + super(CQObjectInspector, self).__init__(parent) self.setHeaderHidden(False) self.setRootIsDecorated(True) self.setContextMenuPolicy(Qt.ActionsContextMenu) self.setColumnCount(2) - self.setHeaderLabels(['Type','Value']) - + self.setHeaderLabels(["Type", "Value"]) + self.root = self.invisibleRootItem() self.inspected_items = [] - - self._toolbar_actions = \ - [QAction(icon('inspect'),'Inspect CQ object',self,\ - toggled=self.inspect,checkable=True)] - + + self._toolbar_actions = [ + QAction( + icon("inspect"), + "Inspect CQ object", + self, + toggled=self.inspect, + checkable=True, + ) + ] + self.addActions(self._toolbar_actions) - + def menuActions(self): - - return {'Tools' : self._toolbar_actions} - + + return {"Tools": self._toolbar_actions} + def toolbarActions(self): - + return self._toolbar_actions - + @pyqtSlot(bool) - def inspect(self,value): - + def inspect(self, value): + if value: self.itemSelectionChanged.connect(self.handleSelection) self.itemSelectionChanged.emit() @@ -74,56 +79,54 @@ def inspect(self,value): self.itemSelectionChanged.disconnect(self.handleSelection) self.sigRemoveObjects.emit(self.inspected_items) self.sigShowPlane.emit(False) - - @pyqtSlot() + + @pyqtSlot() def handleSelection(self): - + inspected_items = self.inspected_items self.sigRemoveObjects.emit(inspected_items) inspected_items.clear() - + items = self.selectedItems() if len(items) == 0: return - + item = items[-1] if type(item) is CQStackItem: cq_plane = item.workplane.plane dim = item.workplane.largestDimension() - plane = gp_Ax3(cq_plane.origin.toPnt(), - cq_plane.zDir.toDir(), - cq_plane.xDir.toDir()) + plane = gp_Ax3( + cq_plane.origin.toPnt(), cq_plane.zDir.toDir(), cq_plane.xDir.toDir() + ) self.sigChangePlane.emit(plane) - self.sigShowPlane[bool,float].emit(True,dim) - + self.sigShowPlane[bool, float].emit(True, dim) + for child in (item.child(i) for i in range(item.childCount())): obj = child.cq_item - if hasattr(obj,'wrapped') and type(obj) != Vector: + if hasattr(obj, "wrapped") and type(obj) != Vector: ais = AIS_ColoredShape(obj.wrapped) inspected_items.append(ais) - + else: self.sigShowPlane.emit(False) obj = item.cq_item - if hasattr(obj,'wrapped') and type(obj) != Vector: + if hasattr(obj, "wrapped") and type(obj) != Vector: ais = AIS_ColoredShape(obj.wrapped) inspected_items.append(ais) - - self.sigDisplayObjects.emit(inspected_items,False) - + + self.sigDisplayObjects.emit(inspected_items, False) + @pyqtSlot(object) - def setObject(self,cq_obj): - + def setObject(self, cq_obj): + self.root.takeChildren() - + # iterate through parent objects if they exist - while getattr(cq_obj, 'parent', None): - current_frame = CQStackItem(str(cq_obj.plane.origin),workplane=cq_obj) + while getattr(cq_obj, "parent", None): + current_frame = CQStackItem(str(cq_obj.plane.origin), workplane=cq_obj) self.root.addChild(current_frame) - + for obj in cq_obj.objects: current_frame.addChild(CQChildItem(obj)) - + cq_obj = cq_obj.parent - - \ No newline at end of file diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index cad97f55..248ec2c7 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -10,19 +10,26 @@ import cadquery.vis as cqvis from PyQt5 import QtCore -from PyQt5.QtCore import Qt, QObject, pyqtSlot, pyqtSignal, QEventLoop, QAbstractTableModel +from PyQt5.QtCore import ( + Qt, + QObject, + pyqtSlot, + pyqtSignal, + QEventLoop, + QAbstractTableModel, +) from PyQt5.QtWidgets import QAction, QTableView from logbook import info from path import Path from pyqtgraph.parametertree import Parameter from spyder.utils.icon_manager import icon -from random import randrange as rrr,seed +from random import randrange as rrr, seed from ..cq_utils import find_cq_objects, reload_cq from ..mixins import ComponentMixin -DUMMY_FILE = '' +DUMMY_FILE = "" class DbgState(Enum): @@ -32,35 +39,39 @@ class DbgState(Enum): STEP_IN = auto() RETURN = auto() + class DbgEevent(object): - LINE = 'line' - CALL = 'call' - RETURN = 'return' + LINE = "line" + CALL = "call" + RETURN = "return" + class LocalsModel(QAbstractTableModel): - HEADER = ('Name','Type', 'Value') + HEADER = ("Name", "Type", "Value") - def __init__(self,parent): + def __init__(self, parent): - super(LocalsModel,self).__init__(parent) + super(LocalsModel, self).__init__(parent) self.frame = None - def update_frame(self,frame): - - self.frame = \ - [(k,type(v).__name__, str(v)) for k,v in frame.items() if not k.startswith('_')] + def update_frame(self, frame): + self.frame = [ + (k, type(v).__name__, str(v)) + for k, v in frame.items() + if not k.startswith("_") + ] - def rowCount(self,parent=QtCore.QModelIndex()): + def rowCount(self, parent=QtCore.QModelIndex()): if self.frame: return len(self.frame) else: return 0 - def columnCount(self,parent=QtCore.QModelIndex()): + def columnCount(self, parent=QtCore.QModelIndex()): return 3 @@ -78,13 +89,13 @@ def data(self, index, role): return QtCore.QVariant() -class LocalsView(QTableView,ComponentMixin): +class LocalsView(QTableView, ComponentMixin): - name = 'Variables' + name = "Variables" - def __init__(self,parent): + def __init__(self, parent): - super(LocalsView,self).__init__(parent) + super(LocalsView, self).__init__(parent) ComponentMixin.__init__(self) header = self.horizontalHeader() @@ -94,86 +105,99 @@ def __init__(self,parent): vheader.setVisible(False) @pyqtSlot(dict) - def update_frame(self,frame): + def update_frame(self, frame): model = LocalsModel(self) model.update_frame(frame) self.setModel(model) -class Debugger(QObject,ComponentMixin): - name = 'Debugger' +class Debugger(QObject, ComponentMixin): - preferences = Parameter.create(name='Preferences',children=[ - {'name': 'Reload CQ', 'type': 'bool', 'value': False}, - {'name': 'Add script dir to path','type': 'bool', 'value': True}, - {'name': 'Change working dir to script dir','type': 'bool', 'value': True}, - {'name': 'Reload imported modules', 'type': 'bool', 'value': True}, - ]) + name = "Debugger" + preferences = Parameter.create( + name="Preferences", + children=[ + {"name": "Reload CQ", "type": "bool", "value": False}, + {"name": "Add script dir to path", "type": "bool", "value": True}, + {"name": "Change working dir to script dir", "type": "bool", "value": True}, + {"name": "Reload imported modules", "type": "bool", "value": True}, + ], + ) sigRendered = pyqtSignal(dict) sigLocals = pyqtSignal(dict) - sigTraceback = pyqtSignal(object,str) + sigTraceback = pyqtSignal(object, str) sigRunCell = pyqtSignal() sigFrameChanged = pyqtSignal(object) sigLineChanged = pyqtSignal(int) sigLocalsChanged = pyqtSignal(dict) - sigCQChanged = pyqtSignal(dict,bool) + sigCQChanged = pyqtSignal(dict, bool) sigDebugging = pyqtSignal(bool) - _frames : List[FrameType] - _stop_debugging : bool + _frames: List[FrameType] + _stop_debugging: bool - def __init__(self,parent): + def __init__(self, parent): - super(Debugger,self).__init__(parent) + super(Debugger, self).__init__(parent) ComponentMixin.__init__(self) self.inner_event_loop = QEventLoop(self) - self._actions = \ - {'Run' : [QAction(icon('run'), - 'Render', - self, - shortcut='F5', - triggered=self.render), - QAction(icon('cell'), - 'Run cell', - self, - shortcut='ctrl+shift+F5', - triggered=self.sigRunCell.emit), - QAction(icon('debug'), - 'Debug', - self, - checkable=True, - shortcut='ctrl+F5', - triggered=self.debug), - QAction(icon('arrow-step-over'), - 'Step', - self, - shortcut='ctrl+F10', - triggered=lambda: self.debug_cmd(DbgState.STEP)), - QAction(icon('arrow-step-in'), - 'Step in', - self, - shortcut='ctrl+F11', - triggered=lambda: self.debug_cmd(DbgState.STEP_IN)), - QAction(icon('arrow-continue'), - 'Continue', - self, - shortcut='ctrl+F12', - triggered=lambda: self.debug_cmd(DbgState.CONT)) - ]} + self._actions = { + "Run": [ + QAction( + icon("run"), "Render", self, shortcut="F5", triggered=self.render + ), + QAction( + icon("cell"), + "Run cell", + self, + shortcut="ctrl+shift+F5", + triggered=self.sigRunCell.emit, + ), + QAction( + icon("debug"), + "Debug", + self, + checkable=True, + shortcut="ctrl+F5", + triggered=self.debug, + ), + QAction( + icon("arrow-step-over"), + "Step", + self, + shortcut="ctrl+F10", + triggered=lambda: self.debug_cmd(DbgState.STEP), + ), + QAction( + icon("arrow-step-in"), + "Step in", + self, + shortcut="ctrl+F11", + triggered=lambda: self.debug_cmd(DbgState.STEP_IN), + ), + QAction( + icon("arrow-continue"), + "Continue", + self, + shortcut="ctrl+F12", + triggered=lambda: self.debug_cmd(DbgState.CONT), + ), + ] + } self._frames = [] self._stop_debugging = False def get_current_script(self): - return self.parent().components['editor'].get_text_with_eol() + return self.parent().components["editor"].get_text_with_eol() def get_current_script_path(self): @@ -183,15 +207,15 @@ def get_current_script_path(self): def get_breakpoints(self): - return self.parent().components['editor'].debugger.get_breakpoints() + return self.parent().components["editor"].debugger.get_breakpoints() def compile_code(self, cq_script, cq_script_path=None): try: - module = ModuleType('__cq_main__') + module = ModuleType("__cq_main__") if cq_script_path: module.__dict__["__file__"] = cq_script_path - cq_code = compile(cq_script, DUMMY_FILE, 'exec') + cq_code = compile(cq_script, DUMMY_FILE, "exec") return cq_code, module except Exception: self.sigTraceback.emit(sys.exc_info(), cq_script) @@ -202,129 +226,131 @@ def _exec(self, code, locals_dict, globals_dict): with ExitStack() as stack: p = (self.get_current_script_path() or Path("")).abspath().dirname() - if self.preferences['Add script dir to path'] and p.exists(): - sys.path.insert(0,p) + if self.preferences["Add script dir to path"] and p.exists(): + sys.path.insert(0, p) stack.callback(sys.path.remove, p) - if self.preferences['Change working dir to script dir'] and p.exists(): + if self.preferences["Change working dir to script dir"] and p.exists(): stack.enter_context(p) - if self.preferences['Reload imported modules']: + if self.preferences["Reload imported modules"]: stack.enter_context(module_manager()) exec(code, locals_dict, globals_dict) @staticmethod - def _rand_color(alpha = 0., cfloat=False): - #helper function to generate a random color dict - #for CQ-editor's show_object function + def _rand_color(alpha=0.0, cfloat=False): + # helper function to generate a random color dict + # for CQ-editor's show_object function lower = 10 - upper = 100 #not too high to keep color brightness in check - if cfloat: #for two output types depending on need + upper = 100 # not too high to keep color brightness in check + if cfloat: # for two output types depending on need return ( - (rrr(lower,upper)/255), - (rrr(lower,upper)/255), - (rrr(lower,upper)/255), - alpha, - ) - return {"alpha": alpha, - "color": ( - rrr(lower,upper), - rrr(lower,upper), - rrr(lower,upper), - )} - - def _inject_locals(self,module): + (rrr(lower, upper) / 255), + (rrr(lower, upper) / 255), + (rrr(lower, upper) / 255), + alpha, + ) + return { + "alpha": alpha, + "color": ( + rrr(lower, upper), + rrr(lower, upper), + rrr(lower, upper), + ), + } + + def _inject_locals(self, module): cq_objects = {} def _show_object(obj, name=None, options={}): if name: - cq_objects.update({name : SimpleNamespace(shape=obj,options=options)}) + cq_objects.update({name: SimpleNamespace(shape=obj, options=options)}) else: - #get locals of the enclosing scope + # get locals of the enclosing scope d = currentframe().f_back.f_locals - #try to find the name + # try to find the name try: name = list(d.keys())[list(d.values()).index(obj)] except ValueError: - #use id if not found + # use id if not found name = str(id(obj)) cq_objects.update( - {name : SimpleNamespace( - shape=obj if isinstance(obj, list) else [obj], - options=options + { + name: SimpleNamespace( + shape=obj if isinstance(obj, list) else [obj], + options=options, ) } ) - def _debug(obj,name=None): + def _debug(obj, name=None): - _show_object(obj,name,options=dict(color='red',alpha=0.2)) + _show_object(obj, name, options=dict(color="red", alpha=0.2)) - module.__dict__['show_object'] = _show_object - module.__dict__['show'] = _show_object - module.__dict__['debug'] = _debug - module.__dict__['rand_color'] = self._rand_color - module.__dict__['log'] = lambda x: info(str(x)) - module.__dict__['cq'] = cq + module.__dict__["show_object"] = _show_object + module.__dict__["show"] = _show_object + module.__dict__["debug"] = _debug + module.__dict__["rand_color"] = self._rand_color + module.__dict__["log"] = lambda x: info(str(x)) + module.__dict__["cq"] = cq # overwrite cq.vis.show self.old_show = cqvis.show cqvis.show = _show_object - return cq_objects, set(module.__dict__)-{'cq'} + return cq_objects, set(module.__dict__) - {"cq"} - def _cleanup_locals(self,module,injected_names): + def _cleanup_locals(self, module, injected_names): - for name in injected_names: module.__dict__.pop(name) + for name in injected_names: + module.__dict__.pop(name) # restore cq.vis.show self.old_show = cqvis.show cqvis.show = self.old_show - @pyqtSlot(bool) def render(self): seed(59798267586177) - if self.preferences['Reload CQ']: + if self.preferences["Reload CQ"]: reload_cq() cq_script = self.get_current_script() cq_script_path = self.get_current_script_path() - cq_code,module = self.compile_code(cq_script, cq_script_path) + cq_code, module = self.compile_code(cq_script, cq_script_path) - if cq_code is None: return + if cq_code is None: + return - cq_objects,injected_names = self._inject_locals(module) + cq_objects, injected_names = self._inject_locals(module) try: self._exec(cq_code, module.__dict__, module.__dict__) - #remove the special methods - self._cleanup_locals(module,injected_names) + # remove the special methods + self._cleanup_locals(module, injected_names) - #collect all CQ objects if no explicit show_object was called + # collect all CQ objects if no explicit show_object was called if len(cq_objects) == 0: cq_objects = find_cq_objects(module.__dict__) self.sigRendered.emit(cq_objects) - self.sigTraceback.emit(None, - cq_script) + self.sigTraceback.emit(None, cq_script) self.sigLocals.emit(module.__dict__) except Exception: exc_info = sys.exc_info() sys.last_traceback = exc_info[-1] self.sigTraceback.emit(exc_info, cq_script) - @property def breakpoints(self): - return [ el[0] for el in self.get_breakpoints()] + return [el[0] for el in self.get_breakpoints()] @pyqtSlot(bool) - def debug(self,value): + def debug(self, value): # used to stop the debugging session early self._stop_debugging = False @@ -337,39 +363,37 @@ def debug(self,value): self.script = self.get_current_script() cq_script_path = self.get_current_script_path() - code,module = self.compile_code(self.script, cq_script_path) + code, module = self.compile_code(self.script, cq_script_path) if code is None: self.sigDebugging.emit(False) - self._actions['Run'][1].setChecked(False) + self._actions["Run"][1].setChecked(False) return - cq_objects,injected_names = self._inject_locals(module) + cq_objects, injected_names = self._inject_locals(module) - #clear possible traceback - self.sigTraceback.emit(None, - self.script) + # clear possible traceback + self.sigTraceback.emit(None, self.script) try: sys.settrace(self.trace_callback) - exec(code,module.__dict__,module.__dict__) + exec(code, module.__dict__, module.__dict__) except BdbQuit: pass except Exception: exc_info = sys.exc_info() sys.last_traceback = exc_info[-1] - self.sigTraceback.emit(exc_info, - self.script) + self.sigTraceback.emit(exc_info, self.script) finally: sys.settrace(previous_trace) self.sigDebugging.emit(False) - self._actions['Run'][1].setChecked(False) + self._actions["Run"][1].setChecked(False) if len(cq_objects) == 0: cq_objects = find_cq_objects(module.__dict__) self.sigRendered.emit(cq_objects) - self._cleanup_locals(module,injected_names) + self._cleanup_locals(module, injected_names) self.sigLocals.emit(module.__dict__) self._frames = [] @@ -378,32 +402,33 @@ def debug(self,value): self._stop_debugging = True self.inner_event_loop.exit(0) - def debug_cmd(self,state=DbgState.STEP): + def debug_cmd(self, state=DbgState.STEP): self.state = state self.inner_event_loop.exit(0) - - def trace_callback(self,frame,event,arg): + def trace_callback(self, frame, event, arg): filename = frame.f_code.co_filename - if filename==DUMMY_FILE: + if filename == DUMMY_FILE: if not self._frames: self._frames.append(frame) - self.trace_local(frame,event,arg) + self.trace_local(frame, event, arg) return self.trace_callback else: return None - def trace_local(self,frame,event,arg): + def trace_local(self, frame, event, arg): lineno = frame.f_lineno if event in (DbgEevent.LINE,): - if (self.state in (DbgState.STEP, DbgState.STEP_IN) and frame is self._frames[-1]) \ - or (lineno in self.breakpoints): + if ( + self.state in (DbgState.STEP, DbgState.STEP_IN) + and frame is self._frames[-1] + ) or (lineno in self.breakpoints): if lineno in self.breakpoints: self._frames.append(frame) @@ -411,7 +436,7 @@ def trace_local(self,frame,event,arg): self.sigLineChanged.emit(lineno) self.sigFrameChanged.emit(frame) self.sigLocalsChanged.emit(frame.f_locals) - self.sigCQChanged.emit(find_cq_objects(frame.f_locals),True) + self.sigCQChanged.emit(find_cq_objects(frame.f_locals), True) self.inner_event_loop.exec_() @@ -428,12 +453,12 @@ def trace_local(self,frame,event,arg): self._frames.append(frame) if self._stop_debugging: - raise BdbQuit #stop debugging if requested + raise BdbQuit # stop debugging if requested @contextmanager def module_manager(): - """ unloads any modules loaded while the context manager is active """ + """unloads any modules loaded while the context manager is active""" loaded_modules = set(sys.modules.keys()) try: diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index cc145052..4713be82 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -17,85 +17,101 @@ from ..icons import icon -class Editor(CodeEditor,ComponentMixin): - name = 'Code Editor' +class Editor(CodeEditor, ComponentMixin): + + name = "Code Editor" # This signal is emitted whenever the currently-open file changes and # autoreload is enabled. triggerRerender = pyqtSignal(bool) sigFilenameChanged = pyqtSignal(str) - preferences = Parameter.create(name='Preferences',children=[ - {'name': 'Font size', 'type': 'int', 'value': 12}, - {'name': 'Autoreload', 'type': 'bool', 'value': False}, - {'name': 'Autoreload delay', 'type': 'int', 'value': 50}, - {'name': 'Autoreload: watch imported modules', 'type': 'bool', 'value': False}, - {'name': 'Line wrap', 'type': 'bool', 'value': False}, - {'name': 'Color scheme', 'type': 'list', - 'values': ['Spyder','Monokai','Zenburn'], 'value': 'Spyder'}]) - - EXTENSIONS = 'py' - - def __init__(self,parent=None): + preferences = Parameter.create( + name="Preferences", + children=[ + {"name": "Font size", "type": "int", "value": 12}, + {"name": "Autoreload", "type": "bool", "value": False}, + {"name": "Autoreload delay", "type": "int", "value": 50}, + { + "name": "Autoreload: watch imported modules", + "type": "bool", + "value": False, + }, + {"name": "Line wrap", "type": "bool", "value": False}, + { + "name": "Color scheme", + "type": "list", + "values": ["Spyder", "Monokai", "Zenburn"], + "value": "Spyder", + }, + ], + ) + + EXTENSIONS = "py" + + def __init__(self, parent=None): self._watched_file = None - super(Editor,self).__init__(parent) + super(Editor, self).__init__(parent) ComponentMixin.__init__(self) - self.setup_editor(linenumbers=True, - markers=True, - edge_line=False, - tab_mode=False, - show_blanks=True, - font=QFontDatabase.systemFont(QFontDatabase.FixedFont), - language='Python', - filename='') - - self._actions = \ - {'File' : [QAction(icon('new'), - 'New', - self, - shortcut='ctrl+N', - triggered=self.new), - QAction(icon('open'), - 'Open', - self, - shortcut='ctrl+O', - triggered=self.open), - QAction(icon('save'), - 'Save', - self, - shortcut='ctrl+S', - triggered=self.save), - QAction(icon('save_as'), - 'Save as', - self, - shortcut='ctrl+shift+S', - triggered=self.save_as), - QAction(icon('autoreload'), - 'Automatic reload and preview', - self,triggered=self.autoreload, - checkable=True, - checked=False, - objectName='autoreload'), - ]} + self.setup_editor( + linenumbers=True, + markers=True, + edge_line=False, + tab_mode=False, + show_blanks=True, + font=QFontDatabase.systemFont(QFontDatabase.FixedFont), + language="Python", + filename="", + ) + + self._actions = { + "File": [ + QAction( + icon("new"), "New", self, shortcut="ctrl+N", triggered=self.new + ), + QAction( + icon("open"), "Open", self, shortcut="ctrl+O", triggered=self.open + ), + QAction( + icon("save"), "Save", self, shortcut="ctrl+S", triggered=self.save + ), + QAction( + icon("save_as"), + "Save as", + self, + shortcut="ctrl+shift+S", + triggered=self.save_as, + ), + QAction( + icon("autoreload"), + "Automatic reload and preview", + self, + triggered=self.autoreload, + checkable=True, + checked=False, + objectName="autoreload", + ), + ] + } for a in self._actions.values(): self.addActions(a) - self._fixContextMenu() # autoreload support self._file_watcher = QFileSystemWatcher(self) # we wait for 50ms after a file change for the file to be written completely self._file_watch_timer = QTimer(self) - self._file_watch_timer.setInterval(self.preferences['Autoreload delay']) + self._file_watch_timer.setInterval(self.preferences["Autoreload delay"]) self._file_watch_timer.setSingleShot(True) self._file_watcher.fileChanged.connect( - lambda val: self._file_watch_timer.start()) + lambda val: self._file_watch_timer.start() + ) self._file_watch_timer.timeout.connect(self._file_changed) self.updatePreferences() @@ -109,20 +125,19 @@ def _fixContextMenu(self): menu.removeAction(self.run_selection_action) menu.removeAction(self.re_run_last_cell_action) - def updatePreferences(self,*args): + def updatePreferences(self, *args): - self.set_color_scheme(self.preferences['Color scheme']) + self.set_color_scheme(self.preferences["Color scheme"]) font = self.font() - font.setPointSize(self.preferences['Font size']) + font.setPointSize(self.preferences["Font size"]) self.set_font(font) - self.findChild(QAction, 'autoreload') \ - .setChecked(self.preferences['Autoreload']) + self.findChild(QAction, "autoreload").setChecked(self.preferences["Autoreload"]) - self._file_watch_timer.setInterval(self.preferences['Autoreload delay']) + self._file_watch_timer.setInterval(self.preferences["Autoreload delay"]) - self.toggle_wrap_mode(self.preferences['Line wrap']) + self.toggle_wrap_mode(self.preferences["Line wrap"]) self._clear_watched_paths() self._watch_paths() @@ -130,7 +145,11 @@ def updatePreferences(self,*args): def confirm_discard(self): if self.modified: - rv = confirm(self,'Please confirm','Current document is not saved - do you want to continue?') + rv = confirm( + self, + "Please confirm", + "Current document is not saved - do you want to continue?", + ) else: rv = True @@ -138,22 +157,24 @@ def confirm_discard(self): def new(self): - if not self.confirm_discard(): return + if not self.confirm_discard(): + return - self.set_text('') - self.filename = '' + self.set_text("") + self.filename = "" self.reset_modified() def open(self): - if not self.confirm_discard(): return + if not self.confirm_discard(): + return curr_dir = Path(self.filename).abspath().dirname() fname = get_open_filename(self.EXTENSIONS, curr_dir) - if fname != '': + if fname != "": self.load_from_file(fname) - def load_from_file(self,fname): + def load_from_file(self, fname): self.set_text_from_file(fname) self.filename = fname @@ -164,24 +185,24 @@ def determine_encoding(self, fname): # this function returns the encoding spyder used to read the file _, encoding = spyder.utils.encoding.read(fname) # spyder returns a -guessed suffix in some cases - return encoding.replace('-guessed', '') + return encoding.replace("-guessed", "") else: - return 'utf-8' + return "utf-8" def save(self): - if self._filename != '': + if self._filename != "": - if self.preferences['Autoreload']: + if self.preferences["Autoreload"]: self._file_watcher.blockSignals(True) self._file_watch_timer.stop() encoding = self.determine_encoding(self._filename) encoded = self.toPlainText().encode(encoding) - with open(self._filename, 'wb') as f: + with open(self._filename, "wb") as f: f.write(encoded) - if self.preferences['Autoreload']: + if self.preferences["Autoreload"]: self._file_watcher.blockSignals(False) self.triggerRerender.emit(True) @@ -193,19 +214,25 @@ def save(self): def save_as(self): fname = get_save_filename(self.EXTENSIONS) - if fname != '': - encoded = self.toPlainText().encode('utf-8') - with open(fname, 'wb') as f: + if fname != "": + encoded = self.toPlainText().encode("utf-8") + with open(fname, "wb") as f: f.write(encoded) self.filename = fname self.reset_modified() def _update_filewatcher(self): - if self._watched_file and (self._watched_file != self.filename or not self.preferences['Autoreload']): + if self._watched_file and ( + self._watched_file != self.filename or not self.preferences["Autoreload"] + ): self._clear_watched_paths() self._watched_file = None - if self.preferences['Autoreload'] and self.filename and self.filename != self._watched_file: + if ( + self.preferences["Autoreload"] + and self.filename + and self.filename != self._watched_file + ): self._watched_file = self._filename self._watch_paths() @@ -227,8 +254,8 @@ def _clear_watched_paths(self): def _watch_paths(self): if Path(self._filename).exists(): self._file_watcher.addPath(self._filename) - if self.preferences['Autoreload: watch imported modules']: - module_paths = self.get_imported_module_paths(self._filename) + if self.preferences["Autoreload: watch imported modules"]: + module_paths = self.get_imported_module_paths(self._filename) if module_paths: self._file_watcher.addPaths(module_paths) @@ -242,7 +269,7 @@ def _file_changed(self): # Turn autoreload on/off. def autoreload(self, enabled): - self.preferences['Autoreload'] = enabled + self.preferences["Autoreload"] = enabled self._update_filewatcher() def reset_modified(self): @@ -254,21 +281,20 @@ def modified(self): return self.document().isModified() - def saveComponentState(self,store): + def saveComponentState(self, store): - if self.filename != '': - store.setValue(self.name+'/state',self.filename) + if self.filename != "": + store.setValue(self.name + "/state", self.filename) - def restoreComponentState(self,store): + def restoreComponentState(self, store): - filename = store.value(self.name+'/state') + filename = store.value(self.name + "/state") - if filename and self.filename == '': + if filename and self.filename == "": try: self.load_from_file(filename) except IOError: - self._logger.warning(f'could not open {filename}') - + self._logger.warning(f"could not open {filename}") def get_imported_module_paths(self, module_path): @@ -278,15 +304,15 @@ def get_imported_module_paths(self, module_path): try: finder.run_script(module_path) except SyntaxError as err: - self._logger.warning(f'Syntax error in {module_path}: {err}') + self._logger.warning(f"Syntax error in {module_path}: {err}") except Exception as err: self._logger.warning( - f'Cannot determine imported modules in {module_path}: {type(err).__name__} {err}' + f"Cannot determine imported modules in {module_path}: {type(err).__name__} {err}" ) else: for module_name, module in finder.modules.items(): - if module_name != '__main__': - path = getattr(module, '__file__', None) + if module_name != "__main__": + path = getattr(module, "__file__", None) if path is not None and os.path.isfile(path): imported_modules.append(path) diff --git a/cq_editor/widgets/log.py b/cq_editor/widgets/log.py index 0186f8f1..8b9b3d6e 100644 --- a/cq_editor/widgets/log.py +++ b/cq_editor/widgets/log.py @@ -7,51 +7,54 @@ from ..mixins import ComponentMixin + def strip_escape_sequences(input_string): # Regular expression pattern to match ANSI escape codes - escape_pattern = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]') + escape_pattern = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]") # Use re.sub to replace escape codes with an empty string - clean_string = re.sub(escape_pattern, '', input_string) + clean_string = re.sub(escape_pattern, "", input_string) return clean_string -class QtLogHandler(logging.Handler,logging.StringFormatterHandlerMixin): - - def __init__(self, log_widget,*args,**kwargs): - - super(QtLogHandler,self).__init__(*args,**kwargs) - logging.StringFormatterHandlerMixin.__init__(self,None) - + +class QtLogHandler(logging.Handler, logging.StringFormatterHandlerMixin): + def __init__(self, log_widget, *args, **kwargs): + + super(QtLogHandler, self).__init__(*args, **kwargs) + logging.StringFormatterHandlerMixin.__init__(self, None) + self.log_widget = log_widget def emit(self, record): - + msg = self.format(record) msg = strip_escape_sequences(msg) - QtCore.QMetaObject\ - .invokeMethod(self.log_widget, - 'appendPlainText', - QtCore.Qt.QueuedConnection, - QtCore.Q_ARG(str, msg)) + QtCore.QMetaObject.invokeMethod( + self.log_widget, + "appendPlainText", + QtCore.Qt.QueuedConnection, + QtCore.Q_ARG(str, msg), + ) + class LogViewer(QPlainTextEdit, ComponentMixin): - - name = 'Log viewer' - - def __init__(self,*args,**kwargs): - - super(LogViewer,self).__init__(*args,**kwargs) + + name = "Log viewer" + + def __init__(self, *args, **kwargs): + + super(LogViewer, self).__init__(*args, **kwargs) self._MAX_ROWS = 500 - + self.setReadOnly(True) self.setMaximumBlockCount(self._MAX_ROWS) self.setLineWrapMode(QPlainTextEdit.NoWrap) - + self.handler = QtLogHandler(self) - - def append(self,msg): - + + def append(self, msg): + self.appendPlainText(msg) diff --git a/cq_editor/widgets/object_tree.py b/cq_editor/widgets/object_tree.py index 1ea6f4f3..b883700d 100644 --- a/cq_editor/widgets/object_tree.py +++ b/cq_editor/widgets/object_tree.py @@ -1,4 +1,11 @@ -from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QAction, QMenu, QWidget, QAbstractItemView +from PyQt5.QtWidgets import ( + QTreeWidget, + QTreeWidgetItem, + QAction, + QMenu, + QWidget, + QAbstractItemView, +) from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal from pyqtgraph.parametertree import Parameter, ParameterTree @@ -9,51 +16,66 @@ from ..mixins import ComponentMixin from ..icons import icon -from ..cq_utils import make_AIS, export, to_occ_color, is_obj_empty, get_occ_color, set_color +from ..cq_utils import ( + make_AIS, + export, + to_occ_color, + is_obj_empty, + get_occ_color, + set_color, +) from .viewer import DEFAULT_FACE_COLOR from ..utils import splitter, layout, get_save_filename + class TopTreeItem(QTreeWidgetItem): + def __init__(self, *args, **kwargs): - def __init__(self,*args,**kwargs): + super(TopTreeItem, self).__init__(*args, **kwargs) - super(TopTreeItem,self).__init__(*args,**kwargs) class ObjectTreeItem(QTreeWidgetItem): - props = [{'name': 'Name', 'type': 'str', 'value': ''}, - {'name': 'Color', 'type': 'color', 'value': "#f4a824"}, - {'name': 'Alpha', 'type': 'float', 'value': 0, 'limits': (0,1), 'step': 1e-1}, - {'name': 'Visible', 'type': 'bool','value': True}] - - def __init__(self, - name, - ais=None, - shape=None, - shape_display=None, - sig=None, - alpha=0., - color='#f4a824', - **kwargs): - - super(ObjectTreeItem,self).__init__([name],**kwargs) - self.setFlags( self.flags() | Qt.ItemIsUserCheckable) - self.setCheckState(0,Qt.Checked) + props = [ + {"name": "Name", "type": "str", "value": ""}, + {"name": "Color", "type": "color", "value": "#f4a824"}, + {"name": "Alpha", "type": "float", "value": 0, "limits": (0, 1), "step": 1e-1}, + {"name": "Visible", "type": "bool", "value": True}, + ] + + def __init__( + self, + name, + ais=None, + shape=None, + shape_display=None, + sig=None, + alpha=0.0, + color="#f4a824", + **kwargs + ): + + super(ObjectTreeItem, self).__init__([name], **kwargs) + self.setFlags(self.flags() | Qt.ItemIsUserCheckable) + self.setCheckState(0, Qt.Checked) self.ais = ais self.shape = shape self.shape_display = shape_display self.sig = sig - self.properties = Parameter.create(name='Properties', - children=self.props) + self.properties = Parameter.create(name="Properties", children=self.props) - self.properties['Name'] = name + self.properties["Name"] = name # use properties only of the first object ais = ais[0] - self.properties['Alpha'] = ais.Transparency() - self.properties['Color'] = get_occ_color(ais) if ais and ais.HasColor() else get_occ_color(DEFAULT_FACE_COLOR) + self.properties["Alpha"] = ais.Transparency() + self.properties["Color"] = ( + get_occ_color(ais) + if ais and ais.HasColor() + else get_occ_color(DEFAULT_FACE_COLOR) + ) self.properties.sigTreeStateChanged.connect(self.propertiesChanged) def propertiesChanged(self, properties, changed): @@ -61,59 +83,63 @@ def propertiesChanged(self, properties, changed): changed_prop = changed[0][0] for ais in self.ais: - self.setData(0,0,self.properties['Name']) - ais.SetTransparency(self.properties['Alpha']) + self.setData(0, 0, self.properties["Name"]) + ais.SetTransparency(self.properties["Alpha"]) - if changed_prop.name() == 'Color': - set_color(ais, to_occ_color(self.properties['Color'])) + if changed_prop.name() == "Color": + set_color(ais, to_occ_color(self.properties["Color"])) ais.Redisplay() - if self.properties['Visible']: - self.setCheckState(0,Qt.Checked) + if self.properties["Visible"]: + self.setCheckState(0, Qt.Checked) else: - self.setCheckState(0,Qt.Unchecked) + self.setCheckState(0, Qt.Unchecked) if self.sig: self.sig.emit() -class CQRootItem(TopTreeItem): - def __init__(self,*args,**kwargs): +class CQRootItem(TopTreeItem): + def __init__(self, *args, **kwargs): - super(CQRootItem,self).__init__(['CQ models'],*args,**kwargs) + super(CQRootItem, self).__init__(["CQ models"], *args, **kwargs) class HelpersRootItem(TopTreeItem): + def __init__(self, *args, **kwargs): - def __init__(self,*args,**kwargs): + super(HelpersRootItem, self).__init__(["Helpers"], *args, **kwargs) - super(HelpersRootItem,self).__init__(['Helpers'],*args,**kwargs) +class ObjectTree(QWidget, ComponentMixin): -class ObjectTree(QWidget,ComponentMixin): - - name = 'Object Tree' + name = "Object Tree" _stash = [] - preferences = Parameter.create(name='Preferences',children=[ - {'name': 'Preserve properties on reload', 'type': 'bool', 'value': False}, - {'name': 'Clear all before each run', 'type': 'bool', 'value': True}, - {'name': 'STL precision','type': 'float', 'value': .1}]) + preferences = Parameter.create( + name="Preferences", + children=[ + {"name": "Preserve properties on reload", "type": "bool", "value": False}, + {"name": "Clear all before each run", "type": "bool", "value": True}, + {"name": "STL precision", "type": "float", "value": 0.1}, + ], + ) - sigObjectsAdded = pyqtSignal([list],[list,bool]) + sigObjectsAdded = pyqtSignal([list], [list, bool]) sigObjectsRemoved = pyqtSignal(list) sigCQObjectSelected = pyqtSignal(object) sigAISObjectsSelected = pyqtSignal(list) - sigItemChanged = pyqtSignal(QTreeWidgetItem,int) + sigItemChanged = pyqtSignal(QTreeWidgetItem, int) sigObjectPropertiesChanged = pyqtSignal() - def __init__(self,parent): + def __init__(self, parent): - super(ObjectTree,self).__init__(parent) + super(ObjectTree, self).__init__(parent) - self.tree = tree = QTreeWidget(self, - selectionMode=QAbstractItemView.ExtendedSelection) + self.tree = tree = QTreeWidget( + self, selectionMode=QAbstractItemView.ExtendedSelection + ) self.properties_editor = ParameterTree(self) tree.setHeaderHidden(True) @@ -121,10 +147,9 @@ def __init__(self,parent): tree.setRootIsDecorated(False) tree.setContextMenuPolicy(Qt.ActionsContextMenu) - #forward itemChanged singal - tree.itemChanged.connect(\ - lambda item,col: self.sigItemChanged.emit(item,col)) - #handle visibility changes form tree + # forward itemChanged singal + tree.itemChanged.connect(lambda item, col: self.sigItemChanged.emit(item, col)) + # handle visibility changes form tree tree.itemChanged.connect(self.handleChecked) self.CQ = CQRootItem() @@ -136,30 +161,31 @@ def __init__(self,parent): tree.expandToDepth(1) - self._export_STL_action = \ - QAction('Export as STL', - self, - enabled=False, - triggered=lambda: \ - self.export('stl', - self.preferences['STL precision'])) - - self._export_STEP_action = \ - QAction('Export as STEP', - self, - enabled=False, - triggered=lambda: \ - self.export('step')) - - self._clear_current_action = QAction(icon('delete'), - 'Clear current', - self, - enabled=False, - triggered=self.removeSelected) - - self._toolbar_actions = \ - [QAction(icon('delete-many'),'Clear all',self,triggered=self.removeObjects), - self._clear_current_action,] + self._export_STL_action = QAction( + "Export as STL", + self, + enabled=False, + triggered=lambda: self.export("stl", self.preferences["STL precision"]), + ) + + self._export_STEP_action = QAction( + "Export as STEP", self, enabled=False, triggered=lambda: self.export("step") + ) + + self._clear_current_action = QAction( + icon("delete"), + "Clear current", + self, + enabled=False, + triggered=self.removeSelected, + ) + + self._toolbar_actions = [ + QAction( + icon("delete-many"), "Clear all", self, triggered=self.removeObjects + ), + self._clear_current_action, + ] self.prepareMenu() @@ -168,34 +194,34 @@ def __init__(self,parent): self.prepareLayout() - def prepareMenu(self): self.tree.setContextMenuPolicy(Qt.CustomContextMenu) self._context_menu = QMenu(self) self._context_menu.addActions(self._toolbar_actions) - self._context_menu.addActions((self._export_STL_action, - self._export_STEP_action)) + self._context_menu.addActions( + (self._export_STL_action, self._export_STEP_action) + ) def prepareLayout(self): - self._splitter = splitter((self.tree,self.properties_editor), - stretch_factors = (2,1), - orientation=Qt.Vertical) - layout(self,(self._splitter,),top_widget=self) + self._splitter = splitter( + (self.tree, self.properties_editor), + stretch_factors=(2, 1), + orientation=Qt.Vertical, + ) + layout(self, (self._splitter,), top_widget=self) self._splitter.show() - def showMenu(self,position): + def showMenu(self, position): self._context_menu.exec_(self.tree.viewport().mapToGlobal(position)) - def menuActions(self): - return {'Tools' : [self._export_STL_action, - self._export_STEP_action]} + return {"Tools": [self._export_STL_action, self._export_STEP_action]} def toolbarActions(self): @@ -203,19 +229,19 @@ def toolbarActions(self): def addLines(self): - origin = (0,0,0) + origin = (0, 0, 0) ais_list = [] - for name,color,direction in zip(('X','Y','Z'), - ('red','lawngreen','blue'), - ((1,0,0),(0,1,0),(0,0,1))): - line_placement = Geom_Line(gp_Ax1(gp_Pnt(*origin), - gp_Dir(*direction))) + for name, color, direction in zip( + ("X", "Y", "Z"), + ("red", "lawngreen", "blue"), + ((1, 0, 0), (0, 1, 0), (0, 0, 1)), + ): + line_placement = Geom_Line(gp_Ax1(gp_Pnt(*origin), gp_Dir(*direction))) line = AIS_Line(line_placement) line.SetColor(to_occ_color(color)) - self.Helpers.addChild(ObjectTreeItem(name, - ais=[line])) + self.Helpers.addChild(ObjectTreeItem(name, ais=[line])) ais_list.append([line]) @@ -226,76 +252,82 @@ def _current_properties(self): current_params = {} for i in range(self.CQ.childCount()): child = self.CQ.child(i) - current_params[child.properties['Name']] = child.properties + current_params[child.properties["Name"]] = child.properties return current_params - def _restore_properties(self,obj,properties): + def _restore_properties(self, obj, properties): - for p in properties[obj.properties['Name']]: + for p in properties[obj.properties["Name"]]: obj.properties[p.name()] = p.value() - @pyqtSlot(dict,bool) + @pyqtSlot(dict, bool) @pyqtSlot(dict) - def addObjects(self,objects,clean=False,root=None): + def addObjects(self, objects, clean=False, root=None): if root is None: root = self.CQ request_fit_view = True if root.childCount() == 0 else False - preserve_props = self.preferences['Preserve properties on reload'] + preserve_props = self.preferences["Preserve properties on reload"] if preserve_props: current_props = self._current_properties() - if clean or self.preferences['Clear all before each run']: + if clean or self.preferences["Clear all before each run"]: self.removeObjects() ais_list = [] - #remove empty objects - objects_f = {k:v for k,v in objects.items() if not is_obj_empty(v.shape)} + # remove empty objects + objects_f = {k: v for k, v in objects.items() if not is_obj_empty(v.shape)} - for name,obj in objects_f.items(): - ais,shape_display = make_AIS(obj.shape,obj.options) + for name, obj in objects_f.items(): + ais, shape_display = make_AIS(obj.shape, obj.options) - child = ObjectTreeItem(name, - shape=obj.shape, - shape_display=shape_display, - ais=ais, - sig=self.sigObjectPropertiesChanged) + child = ObjectTreeItem( + name, + shape=obj.shape, + shape_display=shape_display, + ais=ais, + sig=self.sigObjectPropertiesChanged, + ) if preserve_props and name in current_props: - self._restore_properties(child,current_props) + self._restore_properties(child, current_props) - if child.properties['Visible']: + if child.properties["Visible"]: ais_list.append(ais) root.addChild(child) if request_fit_view: - self.sigObjectsAdded[list,bool].emit(ais_list,True) + self.sigObjectsAdded[list, bool].emit(ais_list, True) else: self.sigObjectsAdded[list].emit(ais_list) - @pyqtSlot(object,str,object) - def addObject(self,obj,name='',options={}): + @pyqtSlot(object, str, object) + def addObject(self, obj, name="", options={}): root = self.CQ - ais,shape_display = make_AIS(obj, options) + ais, shape_display = make_AIS(obj, options) - root.addChild(ObjectTreeItem(name, - shape=obj, - shape_display=shape_display, - ais=ais, - sig=self.sigObjectPropertiesChanged)) + root.addChild( + ObjectTreeItem( + name, + shape=obj, + shape_display=shape_display, + ais=ais, + sig=self.sigObjectPropertiesChanged, + ) + ) self.sigObjectsAdded.emit([ais]) @pyqtSlot(list) @pyqtSlot() - def removeObjects(self,objects=None): + def removeObjects(self, objects=None): if objects: removed_items_ais = [el for i in objects for el in self.CQ.takeChild(i).ais] @@ -305,7 +337,7 @@ def removeObjects(self,objects=None): self.sigObjectsRemoved.emit(removed_items_ais) @pyqtSlot(bool) - def stashObjects(self,action : bool): + def stashObjects(self, action: bool): if action: self._stash = self.CQ.takeChildren() @@ -325,7 +357,7 @@ def removeSelected(self): self.removeObjects(rows) - def export(self,export_type,precision=None): + def export(self, export_type, precision=None): items = self.tree.selectedItems() @@ -338,20 +370,22 @@ def export(self,export_type,precision=None): shapes = [item.shape for item in items if item.parent() is self.CQ] fname = get_save_filename(export_type) - if fname != '': - export(shapes,export_type,fname,precision) + if fname != "": + export(shapes, export_type, fname, precision) @pyqtSlot() def handleSelection(self): - items =self.tree.selectedItems() + items = self.tree.selectedItems() if len(items) == 0: self._export_STL_action.setEnabled(False) self._export_STEP_action.setEnabled(False) return # emit list of all selected ais objects (might be empty) - ais_objects = [el for item in items for el in item.ais if item.parent() is self.CQ] + ais_objects = [ + el for item in items for el in item.ais if item.parent() is self.CQ + ] self.sigAISObjectsSelected.emit(ais_objects) # handle context menu and emit last selected CQ object (if present) @@ -361,10 +395,9 @@ def handleSelection(self): self._export_STEP_action.setEnabled(True) self._clear_current_action.setEnabled(True) self.sigCQObjectSelected.emit(item.shape) - self.properties_editor.setParameters(item.properties, - showTop=False) + self.properties_editor.setParameters(item.properties, showTop=False) self.properties_editor.setEnabled(True) - elif item is self.CQ and item.childCount()>0: + elif item is self.CQ and item.childCount() > 0: self._export_STL_action.setEnabled(True) self._export_STEP_action.setEnabled(True) else: @@ -375,7 +408,7 @@ def handleSelection(self): self.properties_editor.clear() @pyqtSlot(list) - def handleGraphicalSelection(self,shapes): + def handleGraphicalSelection(self, shapes): self.tree.clearSelection() @@ -386,14 +419,11 @@ def handleGraphicalSelection(self,shapes): if any(el.Shape().IsEqual(shape) for el in item.ais): item.setSelected(True) - @pyqtSlot(QTreeWidgetItem,int) - def handleChecked(self,item,col): + @pyqtSlot(QTreeWidgetItem, int) + def handleChecked(self, item, col): if type(item) is ObjectTreeItem: if item.checkState(0): - item.properties['Visible'] = True + item.properties["Visible"] = True else: - item.properties['Visible'] = False - - - + item.properties["Visible"] = False diff --git a/cq_editor/widgets/occt_widget.py b/cq_editor/widgets/occt_widget.py index 172755ea..cd8215f3 100755 --- a/cq_editor/widgets/occt_widget.py +++ b/cq_editor/widgets/occt_widget.py @@ -15,159 +15,157 @@ ZOOM_STEP = 0.9 - + class OCCTWidget(QWidget): - + sigObjectSelected = pyqtSignal(list) - - def __init__(self,parent=None): - - super(OCCTWidget,self).__init__(parent) - + + def __init__(self, parent=None): + + super(OCCTWidget, self).__init__(parent) + self.setAttribute(Qt.WA_NativeWindow) self.setAttribute(Qt.WA_PaintOnScreen) self.setAttribute(Qt.WA_NoSystemBackground) - + self._initialized = False self._needs_update = False - - #OCCT secific things + + # OCCT secific things self.display_connection = Aspect_DisplayConnection() self.graphics_driver = OpenGl_GraphicDriver(self.display_connection) - + self.viewer = V3d_Viewer(self.graphics_driver) self.view = self.viewer.CreateView() self.context = AIS_InteractiveContext(self.viewer) - - #Trihedorn, lights, etc + + # Trihedorn, lights, etc self.prepare_display() - + def prepare_display(self): - + view = self.view - + params = view.ChangeRenderingParams() params.NbMsaaSamples = 8 params.IsAntialiasingEnabled = True - + view.TriedronDisplay( - Aspect_TypeOfTriedronPosition.Aspect_TOTP_RIGHT_LOWER, - Quantity_Color(), 0.1) - + Aspect_TypeOfTriedronPosition.Aspect_TOTP_RIGHT_LOWER, Quantity_Color(), 0.1 + ) + viewer = self.viewer - + viewer.SetDefaultLights() viewer.SetLightOn() - + ctx = self.context - + ctx.SetDisplayMode(AIS_DisplayMode.AIS_Shaded, True) ctx.DefaultDrawer().SetFaceBoundaryDraw(True) - + def wheelEvent(self, event): - + delta = event.angleDelta().y() - factor = ZOOM_STEP if delta<0 else 1/ZOOM_STEP - + factor = ZOOM_STEP if delta < 0 else 1 / ZOOM_STEP + self.view.SetZoom(factor) - - def mousePressEvent(self,event): - + + def mousePressEvent(self, event): + pos = event.pos() - + if event.button() == Qt.LeftButton: self.view.StartRotation(pos.x(), pos.y()) elif event.button() == Qt.RightButton: self.view.StartZoomAtPoint(pos.x(), pos.y()) - + self.old_pos = pos - - def mouseMoveEvent(self,event): - + + def mouseMoveEvent(self, event): + pos = event.pos() - x,y = pos.x(),pos.y() - + x, y = pos.x(), pos.y() + if event.buttons() == Qt.LeftButton: - self.view.Rotation(x,y) - + self.view.Rotation(x, y) + elif event.buttons() == Qt.MiddleButton: - self.view.Pan(x - self.old_pos.x(), - self.old_pos.y() - y, theToStart=True) - + self.view.Pan(x - self.old_pos.x(), self.old_pos.y() - y, theToStart=True) + elif event.buttons() == Qt.RightButton: - self.view.ZoomAtPoint(self.old_pos.x(), y, - x, self.old_pos.y()) - + self.view.ZoomAtPoint(self.old_pos.x(), y, x, self.old_pos.y()) + self.old_pos = pos - - def mouseReleaseEvent(self,event): - + + def mouseReleaseEvent(self, event): + if event.button() == Qt.LeftButton: pos = event.pos() - x,y = pos.x(),pos.y() - - self.context.MoveTo(x,y,self.view,True) - + x, y = pos.x(), pos.y() + + self.context.MoveTo(x, y, self.view, True) + self._handle_selection() - + def _handle_selection(self): - + self.context.Select(True) self.context.InitSelected() - + selected = [] if self.context.HasSelectedShape(): selected.append(self.context.SelectedShape()) - + self.sigObjectSelected.emit(selected) def paintEngine(self): - + return None - + def paintEvent(self, event): - + if not self._initialized: self._initialize() else: self.view.Redraw() def showEvent(self, event): - - super(OCCTWidget,self).showEvent(event) - + + super(OCCTWidget, self).showEvent(event) + def resizeEvent(self, event): - - super(OCCTWidget,self).resizeEvent(event) - + + super(OCCTWidget, self).resizeEvent(event) + self.view.MustBeResized() - + def _initialize(self): wins = { - 'darwin' : self._get_window_osx, - 'linux' : self._get_window_linux, - 'win32': self._get_window_win + "darwin": self._get_window_osx, + "linux": self._get_window_linux, + "win32": self._get_window_win, } - self.view.SetWindow(wins.get(platform,self._get_window_linux)(self.winId())) + self.view.SetWindow(wins.get(platform, self._get_window_linux)(self.winId())) self._initialized = True - - def _get_window_win(self,wid): - + + def _get_window_win(self, wid): + from OCP.WNT import WNT_Window - + return WNT_Window(wid.ascapsule()) - def _get_window_linux(self,wid): - + def _get_window_linux(self, wid): + from OCP.Xw import Xw_Window - - return Xw_Window(self.display_connection,int(wid)) - - def _get_window_osx(self,wid): - + + return Xw_Window(self.display_connection, int(wid)) + + def _get_window_osx(self, wid): + from OCP.Cocoa import Cocoa_Window - + return Cocoa_Window(wid.ascapsule()) diff --git a/cq_editor/widgets/traceback_viewer.py b/cq_editor/widgets/traceback_viewer.py index 1b7ed962..ee6f98ad 100644 --- a/cq_editor/widgets/traceback_viewer.py +++ b/cq_editor/widgets/traceback_viewer.py @@ -1,99 +1,94 @@ from traceback import extract_tb, format_exception_only from itertools import dropwhile -from PyQt5.QtWidgets import (QWidget, QTreeWidget, QTreeWidgetItem, QAction, - QLabel) +from PyQt5.QtWidgets import QWidget, QTreeWidget, QTreeWidgetItem, QAction, QLabel from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal from ..mixins import ComponentMixin from ..utils import layout + class TracebackTree(QTreeWidget): - name = 'Traceback Viewer' + name = "Traceback Viewer" - def __init__(self,parent): + def __init__(self, parent): - super(TracebackTree,self).__init__(parent) + super(TracebackTree, self).__init__(parent) self.setHeaderHidden(False) self.setItemsExpandable(False) self.setRootIsDecorated(False) self.setContextMenuPolicy(Qt.ActionsContextMenu) self.setColumnCount(3) - self.setHeaderLabels(['File','Line','Code']) - + self.setHeaderLabels(["File", "Line", "Code"]) self.root = self.invisibleRootItem() -class TracebackPane(QWidget,ComponentMixin): + +class TracebackPane(QWidget, ComponentMixin): sigHighlightLine = pyqtSignal(int) - def __init__(self,parent): + def __init__(self, parent): - super(TracebackPane,self).__init__(parent) + super(TracebackPane, self).__init__(parent) self.tree = TracebackTree(self) self.current_exception = QLabel(self) - self.current_exception.setStyleSheet(\ - "QLabel {color : red; }"); + self.current_exception.setStyleSheet("QLabel {color : red; }") - layout(self, - (self.current_exception, - self.tree), - self) + layout(self, (self.current_exception, self.tree), self) self.tree.currentItemChanged.connect(self.handleSelection) - @pyqtSlot(object,str) - def addTraceback(self,exc_info,code): + @pyqtSlot(object, str) + def addTraceback(self, exc_info, code): self.tree.clear() if exc_info: - t,exc,tb = exc_info + t, exc, tb = exc_info root = self.tree.root code = code.splitlines() for el in dropwhile( - lambda el: 'string>' not in el.filename, extract_tb(tb) + lambda el: "string>" not in el.filename, extract_tb(tb) ): - #workaround of the traceback module - if el.line == '': - line = code[el.lineno-1].strip() + # workaround of the traceback module + if el.line == "": + line = code[el.lineno - 1].strip() else: line = el.line - root.addChild(QTreeWidgetItem([el.filename, - str(el.lineno), - line])) + root.addChild(QTreeWidgetItem([el.filename, str(el.lineno), line])) exc_name = t.__name__ exc_msg = str(exc) - exc_msg = exc_msg.replace('<', '<').replace('>', '>') #replace <> + exc_msg = exc_msg.replace("<", "<").replace(">", ">") # replace <> - self.current_exception.\ - setText('{}: {}'.format(exc_name,exc_msg)) + self.current_exception.setText("{}: {}".format(exc_name, exc_msg)) # handle the special case of a SyntaxError if t is SyntaxError: - root.addChild(QTreeWidgetItem( - [exc.filename, - str(exc.lineno), - exc.text.strip() if exc.text else ''] - )) + root.addChild( + QTreeWidgetItem( + [ + exc.filename, + str(exc.lineno), + exc.text.strip() if exc.text else "", + ] + ) + ) else: - self.current_exception.setText('') + self.current_exception.setText("") - @pyqtSlot(QTreeWidgetItem,QTreeWidgetItem) - def handleSelection(self,item,*args): + @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem) + def handleSelection(self, item, *args): if item: - f,line = item.data(0,0),int(item.data(1,0)) + f, line = item.data(0, 0), int(item.data(1, 0)) - if '' in f: + if "" in f: self.sigHighlightLine.emit(line) - - diff --git a/cq_editor/widgets/viewer.py b/cq_editor/widgets/viewer.py index 290f8c59..e5439df1 100644 --- a/cq_editor/widgets/viewer.py +++ b/cq_editor/widgets/viewer.py @@ -3,12 +3,19 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal from PyQt5.QtGui import QIcon -from OCP.Graphic3d import Graphic3d_Camera, Graphic3d_StereoMode, Graphic3d_NOM_JADE,\ - Graphic3d_MaterialAspect -from OCP.AIS import AIS_Shaded,AIS_WireFrame, AIS_ColoredShape, AIS_Axis +from OCP.Graphic3d import ( + Graphic3d_Camera, + Graphic3d_StereoMode, + Graphic3d_NOM_JADE, + Graphic3d_MaterialAspect, +) +from OCP.AIS import AIS_Shaded, AIS_WireFrame, AIS_ColoredShape, AIS_Axis from OCP.Aspect import Aspect_GDM_Lines, Aspect_GT_Rectangular -from OCP.Quantity import Quantity_NOC_BLACK as BLACK, Quantity_TOC_RGB as TOC_RGB,\ - Quantity_Color +from OCP.Quantity import ( + Quantity_NOC_BLACK as BLACK, + Quantity_TOC_RGB as TOC_RGB, + Quantity_Color, +) from OCP.Geom import Geom_Axis1Placement from OCP.gp import gp_Ax3, gp_Dir, gp_Pnt, gp_Ax1 @@ -23,34 +30,71 @@ import qtawesome as qta - DEFAULT_EDGE_COLOR = Quantity_Color(BLACK) DEFAULT_EDGE_WIDTH = 2 -class OCCViewer(QWidget,ComponentMixin): - - name = '3D Viewer' - - preferences = Parameter.create(name='Pref',children=[ - {'name': 'Fit automatically', 'type': 'bool', 'value': True}, - {'name': 'Use gradient', 'type': 'bool', 'value': False}, - {'name': 'Background color', 'type': 'color', 'value': (95,95,95)}, - {'name': 'Background color (aux)', 'type': 'color', 'value': (30,30,30)}, - {'name': 'Default object color', 'type': 'color', 'value': "#FF0"}, - {'name': 'Deviation', 'type': 'float', 'value': 1e-5, 'dec': True, 'step': 1}, - {'name': 'Angular deviation', 'type': 'float', 'value': 0.1, 'dec': True, 'step': 1}, - {'name': 'Projection Type', 'type': 'list', 'value': 'Orthographic', - 'values': ['Orthographic', 'Perspective', 'Stereo', 'MonoLeftEye', 'MonoRightEye']}, - {'name': 'Stereo Mode', 'type': 'list', 'value': 'QuadBuffer', - 'values': ['QuadBuffer', 'Anaglyph', 'RowInterlaced', 'ColumnInterlaced', - 'ChessBoard', 'SideBySide', 'OverUnder']}]) - IMAGE_EXTENSIONS = 'png' + +class OCCViewer(QWidget, ComponentMixin): + + name = "3D Viewer" + + preferences = Parameter.create( + name="Pref", + children=[ + {"name": "Fit automatically", "type": "bool", "value": True}, + {"name": "Use gradient", "type": "bool", "value": False}, + {"name": "Background color", "type": "color", "value": (95, 95, 95)}, + {"name": "Background color (aux)", "type": "color", "value": (30, 30, 30)}, + {"name": "Default object color", "type": "color", "value": "#FF0"}, + { + "name": "Deviation", + "type": "float", + "value": 1e-5, + "dec": True, + "step": 1, + }, + { + "name": "Angular deviation", + "type": "float", + "value": 0.1, + "dec": True, + "step": 1, + }, + { + "name": "Projection Type", + "type": "list", + "value": "Orthographic", + "values": [ + "Orthographic", + "Perspective", + "Stereo", + "MonoLeftEye", + "MonoRightEye", + ], + }, + { + "name": "Stereo Mode", + "type": "list", + "value": "QuadBuffer", + "values": [ + "QuadBuffer", + "Anaglyph", + "RowInterlaced", + "ColumnInterlaced", + "ChessBoard", + "SideBySide", + "OverUnder", + ], + }, + ], + ) + IMAGE_EXTENSIONS = "png" sigObjectSelected = pyqtSignal(list) - def __init__(self,parent=None): + def __init__(self, parent=None): - super(OCCViewer,self).__init__(parent) + super(OCCViewer, self).__init__(parent) ComponentMixin.__init__(self) self.canvas = OCCTWidget() @@ -58,10 +102,14 @@ def __init__(self,parent=None): self.create_actions(self) - self.layout_ = layout(self, - [self.canvas,], - top_widget=self, - margin=0) + self.layout_ = layout( + self, + [ + self.canvas, + ], + top_widget=self, + margin=0, + ) self.setup_default_drawer() self.updatePreferences() @@ -80,95 +128,129 @@ def setup_default_drawer(self): line_aspect.SetWidth(DEFAULT_EDGE_WIDTH) line_aspect.SetColor(DEFAULT_EDGE_COLOR) - def updatePreferences(self,*args): + def updatePreferences(self, *args): - color1 = to_occ_color(self.preferences['Background color']) - color2 = to_occ_color(self.preferences['Background color (aux)']) + color1 = to_occ_color(self.preferences["Background color"]) + color2 = to_occ_color(self.preferences["Background color (aux)"]) - if not self.preferences['Use gradient']: + if not self.preferences["Use gradient"]: color2 = color1 - self.canvas.view.SetBgGradientColors(color1,color2,theToUpdate=True) + self.canvas.view.SetBgGradientColors(color1, color2, theToUpdate=True) self.canvas.update() ctx = self.canvas.context - ctx.SetDeviationCoefficient(self.preferences['Deviation']) - ctx.SetDeviationAngle(self.preferences['Angular deviation']) + ctx.SetDeviationCoefficient(self.preferences["Deviation"]) + ctx.SetDeviationAngle(self.preferences["Angular deviation"]) v = self._get_view() camera = v.Camera() - projection_type = self.preferences['Projection Type'] - camera.SetProjectionType(getattr(Graphic3d_Camera, f'Projection_{projection_type}', - Graphic3d_Camera.Projection_Orthographic)) + projection_type = self.preferences["Projection Type"] + camera.SetProjectionType( + getattr( + Graphic3d_Camera, + f"Projection_{projection_type}", + Graphic3d_Camera.Projection_Orthographic, + ) + ) # onle relevant for stereo projection - stereo_mode = self.preferences['Stereo Mode'] + stereo_mode = self.preferences["Stereo Mode"] params = v.ChangeRenderingParams() - params.StereoMode = getattr(Graphic3d_StereoMode, f'Graphic3d_StereoMode_{stereo_mode}', - Graphic3d_StereoMode.Graphic3d_StereoMode_QuadBuffer) - - def create_actions(self,parent): - - self._actions = \ - {'View' : [QAction(qta.icon('fa.arrows-alt'), - 'Fit (Shift+F1)', - parent, - shortcut='shift+F1', - triggered=self.fit), - QAction(QIcon(':/images/icons/isometric_view.svg'), - 'Iso (Shift+F2)', - parent, - shortcut='shift+F2', - triggered=self.iso_view), - QAction(QIcon(':/images/icons/top_view.svg'), - 'Top (Shift+F3)', - parent, - shortcut='shift+F3', - triggered=self.top_view), - QAction(QIcon(':/images/icons/bottom_view.svg'), - 'Bottom (Shift+F4)', - parent, - shortcut='shift+F4', - triggered=self.bottom_view), - QAction(QIcon(':/images/icons/front_view.svg'), - 'Front (Shift+F5)', - parent, - shortcut='shift+F5', - triggered=self.front_view), - QAction(QIcon(':/images/icons/back_view.svg'), - 'Back (Shift+F6)', - parent, - shortcut='shift+F6', - triggered=self.back_view), - QAction(QIcon(':/images/icons/left_side_view.svg'), - 'Left (Shift+F7)', - parent, - shortcut='shift+F7', - triggered=self.left_view), - QAction(QIcon(':/images/icons/right_side_view.svg'), - 'Right (Shift+F8)', - parent, - shortcut='shift+F8', - triggered=self.right_view), - QAction(qta.icon('fa.square-o'), - 'Wireframe (Shift+F9)', - parent, - shortcut='shift+F9', - triggered=self.wireframe_view), - QAction(qta.icon('fa.square'), - 'Shaded (Shift+F10)', - parent, - shortcut='shift+F10', - triggered=self.shaded_view)], - 'Tools' : [QAction(icon('screenshot'), - 'Screenshot', - parent, - triggered=self.save_screenshot)]} + params.StereoMode = getattr( + Graphic3d_StereoMode, + f"Graphic3d_StereoMode_{stereo_mode}", + Graphic3d_StereoMode.Graphic3d_StereoMode_QuadBuffer, + ) + + def create_actions(self, parent): + + self._actions = { + "View": [ + QAction( + qta.icon("fa.arrows-alt"), + "Fit (Shift+F1)", + parent, + shortcut="shift+F1", + triggered=self.fit, + ), + QAction( + QIcon(":/images/icons/isometric_view.svg"), + "Iso (Shift+F2)", + parent, + shortcut="shift+F2", + triggered=self.iso_view, + ), + QAction( + QIcon(":/images/icons/top_view.svg"), + "Top (Shift+F3)", + parent, + shortcut="shift+F3", + triggered=self.top_view, + ), + QAction( + QIcon(":/images/icons/bottom_view.svg"), + "Bottom (Shift+F4)", + parent, + shortcut="shift+F4", + triggered=self.bottom_view, + ), + QAction( + QIcon(":/images/icons/front_view.svg"), + "Front (Shift+F5)", + parent, + shortcut="shift+F5", + triggered=self.front_view, + ), + QAction( + QIcon(":/images/icons/back_view.svg"), + "Back (Shift+F6)", + parent, + shortcut="shift+F6", + triggered=self.back_view, + ), + QAction( + QIcon(":/images/icons/left_side_view.svg"), + "Left (Shift+F7)", + parent, + shortcut="shift+F7", + triggered=self.left_view, + ), + QAction( + QIcon(":/images/icons/right_side_view.svg"), + "Right (Shift+F8)", + parent, + shortcut="shift+F8", + triggered=self.right_view, + ), + QAction( + qta.icon("fa.square-o"), + "Wireframe (Shift+F9)", + parent, + shortcut="shift+F9", + triggered=self.wireframe_view, + ), + QAction( + qta.icon("fa.square"), + "Shaded (Shift+F10)", + parent, + shortcut="shift+F10", + triggered=self.shaded_view, + ), + ], + "Tools": [ + QAction( + icon("screenshot"), + "Screenshot", + parent, + triggered=self.save_screenshot, + ) + ], + } def toolbarActions(self): - return self._actions['View'] - + return self._actions["View"] def clear(self): @@ -179,42 +261,43 @@ def clear(self): context.PurgeDisplay() context.RemoveAll(True) - def _display(self,shape): + def _display(self, shape): ais = make_AIS(shape) - self.canvas.context.Display(shape,True) + self.canvas.context.Display(shape, True) self.displayed_shapes.append(shape) self.displayed_ais.append(ais) - #self.canvas._display.Repaint() + # self.canvas._display.Repaint() @pyqtSlot(object) - def display(self,ais): + def display(self, ais): context = self._get_context() - context.Display(ais,True) + context.Display(ais, True) - if self.preferences['Fit automatically']: self.fit() + if self.preferences["Fit automatically"]: + self.fit() @pyqtSlot(list) - @pyqtSlot(list,bool) - def display_many(self,ais_list,fit=None): + @pyqtSlot(list, bool) + def display_many(self, ais_list, fit=None): context = self._get_context() for ais in ais_list: for el in ais: context.Display(el, False) - if self.preferences['Fit automatically'] and fit is None: + if self.preferences["Fit automatically"] and fit is None: self.fit() elif fit: self.fit() else: self.redraw() - @pyqtSlot(QTreeWidgetItem,int) - def update_item(self,item,col): + @pyqtSlot(QTreeWidgetItem, int) + def update_item(self, item, col): ctx = self._get_context() if item.checkState(0): @@ -227,10 +310,11 @@ def update_item(self,item,col): self.redraw() @pyqtSlot(list) - def remove_items(self,ais_items): + def remove_items(self, ais_items): ctx = self._get_context() - for ais in ais_items: ctx.Erase(ais, False) + for ais in ais_items: + ctx.Erase(ais, False) self.redraw() @@ -246,43 +330,43 @@ def fit(self): def iso_view(self): v = self._get_view() - v.SetProj(1,-1,1) + v.SetProj(1, -1, 1) v.SetTwist(0) def bottom_view(self): v = self._get_view() - v.SetProj(0,0,-1) + v.SetProj(0, 0, -1) v.SetTwist(0) def top_view(self): v = self._get_view() - v.SetProj(0,0,1) + v.SetProj(0, 0, 1) v.SetTwist(0) def front_view(self): v = self._get_view() - v.SetProj(0,1,0) + v.SetProj(0, 1, 0) v.SetTwist(0) def back_view(self): v = self._get_view() - v.SetProj(0,-1,0) + v.SetProj(0, -1, 0) v.SetTwist(0) def left_view(self): v = self._get_view() - v.SetProj(-1,0,0) + v.SetProj(-1, 0, 0) v.SetTwist(0) def right_view(self): v = self._get_view() - v.SetProj(1,0,0) + v.SetProj(1, 0, 0) v.SetTwist(0) def shaded_view(self): @@ -295,61 +379,55 @@ def wireframe_view(self): c = self._get_context() c.SetDisplayMode(AIS_WireFrame, True) - def show_grid(self, - step=1., - size=10.+1e-6, - color1=(.7,.7,.7), - color2=(0,0,0)): + def show_grid( + self, step=1.0, size=10.0 + 1e-6, color1=(0.7, 0.7, 0.7), color2=(0, 0, 0) + ): viewer = self._get_viewer() - viewer.ActivateGrid(Aspect_GT_Rectangular, - Aspect_GDM_Lines) + viewer.ActivateGrid(Aspect_GT_Rectangular, Aspect_GDM_Lines) viewer.SetRectangularGridGraphicValues(size, size, 0) viewer.SetRectangularGridValues(0, 0, step, step, 0) grid = viewer.Grid() - grid.SetColors(Quantity_Color(*color1,TOC_RGB), - Quantity_Color(*color2,TOC_RGB)) + grid.SetColors( + Quantity_Color(*color1, TOC_RGB), Quantity_Color(*color2, TOC_RGB) + ) def hide_grid(self): viewer = self._get_viewer() viewer.DeactivateGrid() - @pyqtSlot(bool,float) + @pyqtSlot(bool, float) @pyqtSlot(bool) - def toggle_grid(self, - value : bool, - dim : float = 10.): + def toggle_grid(self, value: bool, dim: float = 10.0): if value: - self.show_grid(step=dim/20,size=dim+1e-9) + self.show_grid(step=dim / 20, size=dim + 1e-9) else: self.hide_grid() @pyqtSlot(gp_Ax3) - def set_grid_orientation(self,orientation : gp_Ax3): + def set_grid_orientation(self, orientation: gp_Ax3): viewer = self._get_viewer() viewer.SetPrivilegedPlane(orientation) - def show_axis(self,origin = (0,0,0), direction=(0,0,1)): + def show_axis(self, origin=(0, 0, 0), direction=(0, 0, 1)): - ax_placement = Geom_Axis1Placement(gp_Ax1(gp_Pnt(*origin), - gp_Dir(*direction))) + ax_placement = Geom_Axis1Placement(gp_Ax1(gp_Pnt(*origin), gp_Dir(*direction))) ax = AIS_Axis(ax_placement) self._display_ais(ax) def save_screenshot(self): fname = get_save_filename(self.IMAGE_EXTENSIONS) - if fname != '': - self._get_view().Dump(fname) + if fname != "": + self._get_view().Dump(fname) - def _display_ais(self,ais): + def _display_ais(self, ais): self._get_context().Display(ais) - def _get_view(self): return self.canvas.view @@ -363,18 +441,18 @@ def _get_context(self): return self.canvas.context @pyqtSlot(list) - def handle_selection(self,obj): + def handle_selection(self, obj): self.sigObjectSelected.emit(obj) @pyqtSlot(list) - def set_selected(self,ais): + def set_selected(self, ais): ctx = self._get_context() ctx.ClearSelected(False) for obj in ais: - ctx.AddOrRemoveSelected(obj,False) + ctx.AddOrRemoveSelected(obj, False) self.redraw() @@ -391,10 +469,10 @@ def set_selected(self,ais): dlg.setFixedHeight(400) dlg.setFixedWidth(600) - layout(dlg,(viewer,),dlg) + layout(dlg, (viewer,), dlg) dlg.show() - box = BRepPrimAPI_MakeBox(20,20,30) + box = BRepPrimAPI_MakeBox(20, 20, 30) box_ais = AIS_ColoredShape(box.Shape()) viewer.display(box_ais) From ece596314836a48cc427b9365af11ec6eeea714e Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 3 Oct 2024 19:54:07 +0200 Subject: [PATCH 11/45] Better name retrival --- cq_editor/widgets/debugger.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index 248ec2c7..7d009eb2 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -26,7 +26,7 @@ from spyder.utils.icon_manager import icon from random import randrange as rrr, seed -from ..cq_utils import find_cq_objects, reload_cq +from ..cq_utils import find_cq_objects, reload_cq, is_cq_obj from ..mixins import ComponentMixin DUMMY_FILE = "" @@ -270,11 +270,16 @@ def _show_object(obj, name=None, options={}): # get locals of the enclosing scope d = currentframe().f_back.f_locals - # try to find the name - try: - name = list(d.keys())[list(d.values()).index(obj)] - except ValueError: - # use id if not found + # try to find the arg name + name = None + + for k, v in d.items(): + if v is obj: + name = k + break + + # use id if not found + if name is None: name = str(id(obj)) cq_objects.update( From d404c4ac2930b66c1e50197f38eba4ef772e47f2 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 3 Oct 2024 20:34:12 +0200 Subject: [PATCH 12/45] Add type validation to show and fix Vector support --- cq_editor/cq_utils.py | 29 ++++++++++++++++++++++++++--- cq_editor/widgets/debugger.py | 26 ++++++++++++++++---------- cq_editor/widgets/object_tree.py | 4 ++++ 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/cq_editor/cq_utils.py b/cq_editor/cq_utils.py index 8d62038f..e4a152dd 100644 --- a/cq_editor/cq_utils.py +++ b/cq_editor/cq_utils.py @@ -1,7 +1,7 @@ import cadquery as cq from cadquery.occ_impl.assembly import toCAF -from typing import List, Union +from typing import List, Union, Any from importlib import reload from types import SimpleNamespace @@ -9,11 +9,11 @@ from OCP.XCAFPrs import XCAFPrs_AISObject from OCP.TopoDS import TopoDS_Shape -from OCP.AIS import AIS_InteractiveObject, AIS_Shape, AIS_Trihedron +from OCP.AIS import AIS_InteractiveObject, AIS_Shape, AIS_Trihedron, AIS_Point from OCP.Prs3d import Prs3d_DatumParts, Prs3d_DatumMode from OCP.TCollection import TCollection_ExtendedString from OCP.gp import gp_Ax2 -from OCP.Geom import Geom_Axis2Placement +from OCP.Geom import Geom_Axis2Placement, Geom_CartesianPoint from OCP.Quantity import ( Quantity_TOC_RGB as TOC_RGB, Quantity_Color, @@ -34,6 +34,26 @@ AISLike = Union[CompoundLike, cq.Location, cq.Plane, cq.Vector, AIS_InteractiveObject] AISLikeLists = Union[tuple(List[T] for T in AISLike.__args__)] +Showable = Union[AISLike, AISLikeLists] + + +def is_showable(obj: Any) -> bool: + """ + Check if object is showable. + """ + + return isinstance(obj, Showable) + + +def ensure_showable(obj: Any): + """ + Raise if object is not showable. + """ + + # validate + if not is_showable(obj): + raise ValueError(f"{obj} has incorrect type {type(obj)}.") + def is_cq_obj(obj): @@ -148,6 +168,9 @@ def make_AIS( shape = to_compound(obj) ais = AIS_Shape(shape.wrapped) + elif isinstance(obj, cq.Vector): + ais = AIS_Point(Geom_CartesianPoint(obj.toPnt())) + elif isinstance(obj, AISLikeLists): rv = [make_AIS(el, options)[0][0] for el in obj] diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index 7d009eb2..02bfd2c1 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -24,9 +24,9 @@ from path import Path from pyqtgraph.parametertree import Parameter from spyder.utils.icon_manager import icon -from random import randrange as rrr, seed +from random import randrange, seed -from ..cq_utils import find_cq_objects, reload_cq, is_cq_obj +from ..cq_utils import find_cq_objects, reload_cq, ensure_showable, Showable from ..mixins import ComponentMixin DUMMY_FILE = "" @@ -244,17 +244,17 @@ def _rand_color(alpha=0.0, cfloat=False): upper = 100 # not too high to keep color brightness in check if cfloat: # for two output types depending on need return ( - (rrr(lower, upper) / 255), - (rrr(lower, upper) / 255), - (rrr(lower, upper) / 255), + (randrange(lower, upper) / 255), + (randrange(lower, upper) / 255), + (randrange(lower, upper) / 255), alpha, ) return { "alpha": alpha, "color": ( - rrr(lower, upper), - rrr(lower, upper), - rrr(lower, upper), + randrange(lower, upper), + randrange(lower, upper), + randrange(lower, upper), ), } @@ -262,7 +262,10 @@ def _inject_locals(self, module): cq_objects = {} - def _show_object(obj, name=None, options={}): + def _show_object(obj: Showable, name=None, options={}): + + # this throws + ensure_showable(obj) if name: cq_objects.update({name: SimpleNamespace(shape=obj, options=options)}) @@ -291,7 +294,7 @@ def _show_object(obj, name=None, options={}): } ) - def _debug(obj, name=None): + def _debug(obj: Showable, name=None): _show_object(obj, name, options=dict(color="red", alpha=0.2)) @@ -342,6 +345,9 @@ def render(self): # collect all CQ objects if no explicit show_object was called if len(cq_objects) == 0: cq_objects = find_cq_objects(module.__dict__) + + # check type + self.sigRendered.emit(cq_objects) self.sigTraceback.emit(None, cq_script) self.sigLocals.emit(module.__dict__) diff --git a/cq_editor/widgets/object_tree.py b/cq_editor/widgets/object_tree.py index b883700d..6f095d36 100644 --- a/cq_editor/widgets/object_tree.py +++ b/cq_editor/widgets/object_tree.py @@ -23,6 +23,7 @@ is_obj_empty, get_occ_color, set_color, + ensure_showable, ) from .viewer import DEFAULT_FACE_COLOR from ..utils import splitter, layout, get_save_filename @@ -311,6 +312,9 @@ def addObject(self, obj, name="", options={}): root = self.CQ + # this throws + ensure_showable(obj) + ais, shape_display = make_AIS(obj, options) root.addChild( From 3b0f36673d0b7db3a15969a86100759a834481da Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 3 Oct 2024 20:44:11 +0200 Subject: [PATCH 13/45] Start adding some annotations --- cq_editor/cq_utils.py | 4 ++-- cq_editor/widgets/object_tree.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cq_editor/cq_utils.py b/cq_editor/cq_utils.py index e4a152dd..46e6c03a 100644 --- a/cq_editor/cq_utils.py +++ b/cq_editor/cq_utils.py @@ -1,7 +1,7 @@ import cadquery as cq from cadquery.occ_impl.assembly import toCAF -from typing import List, Union, Any +from typing import List, Union, Any, Optional from importlib import reload from types import SimpleNamespace @@ -142,7 +142,7 @@ def make_trihedron(ax): def make_AIS( obj: Union[AISLike, AISLikeLists], options={}, -) -> List[AIS_InteractiveObject]: +) -> Union[List[AIS_InteractiveObject], Optional[cq.Shape]]: shape = None ais = None diff --git a/cq_editor/widgets/object_tree.py b/cq_editor/widgets/object_tree.py index 6f095d36..b7f3d736 100644 --- a/cq_editor/widgets/object_tree.py +++ b/cq_editor/widgets/object_tree.py @@ -10,7 +10,7 @@ from pyqtgraph.parametertree import Parameter, ParameterTree -from OCP.AIS import AIS_Line +from OCP.AIS import AIS_Line, AIS_InteractiveObject from OCP.Geom import Geom_Line from OCP.gp import gp_Dir, gp_Pnt, gp_Ax1 @@ -24,6 +24,7 @@ get_occ_color, set_color, ensure_showable, + Showable, ) from .viewer import DEFAULT_FACE_COLOR from ..utils import splitter, layout, get_save_filename @@ -37,6 +38,9 @@ def __init__(self, *args, **kwargs): class ObjectTreeItem(QTreeWidgetItem): + ais: list[AIS_InteractiveObject] + shape: Showable + props = [ {"name": "Name", "type": "str", "value": ""}, {"name": "Color", "type": "color", "value": "#f4a824"}, From 204a54af11942fd548c99fdcc5fc168f7ce4ec39 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 5 Oct 2024 22:03:29 +0200 Subject: [PATCH 14/45] Disable reload and fix is_cq_obj --- cq_editor/cq_utils.py | 51 +++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/cq_editor/cq_utils.py b/cq_editor/cq_utils.py index 46e6c03a..7348128e 100644 --- a/cq_editor/cq_utils.py +++ b/cq_editor/cq_utils.py @@ -59,7 +59,7 @@ def is_cq_obj(obj): from cadquery import Workplane, Shape, Assembly, Sketch - return isinstance(obj, Workplane, Shape, Assembly, Sketch) + return isinstance(obj, Union[Workplane, Shape, Assembly, Sketch]) def find_cq_objects(results: dict): @@ -140,8 +140,7 @@ def make_trihedron(ax): def make_AIS( - obj: Union[AISLike, AISLikeLists], - options={}, + obj: Union[AISLike, AISLikeLists], options={}, ) -> Union[List[AIS_InteractiveObject], Optional[cq.Shape]]: shape = None @@ -264,29 +263,29 @@ def set_transparency(ais: AIS_Shape, alpha: float) -> AIS_Shape: def reload_cq(): - # NB: order of reloads is important - reload(cq.types) - reload(cq.occ_impl.geom) - reload(cq.occ_impl.shapes) - reload(cq.occ_impl.shapes) - reload(cq.occ_impl.importers.dxf) - reload(cq.occ_impl.importers) - reload(cq.occ_impl.solver) - reload(cq.occ_impl.assembly) - reload(cq.occ_impl.sketch_solver) - reload(cq.hull) - reload(cq.selectors) - reload(cq.sketch) - reload(cq.occ_impl.exporters.svg) - reload(cq.cq) - reload(cq.occ_impl.exporters.utils) - reload(cq.occ_impl.exporters.dxf) - reload(cq.occ_impl.exporters.amf) - reload(cq.occ_impl.exporters.json) - # reload(cq.occ_impl.exporters.assembly) - reload(cq.occ_impl.exporters) - reload(cq.assembly) - reload(cq) + # # NB: order of reloads is important + # reload(cq.types) + # reload(cq.occ_impl.geom) + # reload(cq.occ_impl.shapes) + # reload(cq.occ_impl.shapes) + # reload(cq.occ_impl.importers.dxf) + # reload(cq.occ_impl.importers) + # reload(cq.occ_impl.solver) + # reload(cq.occ_impl.assembly) + # reload(cq.occ_impl.sketch_solver) + # reload(cq.hull) + # reload(cq.selectors) + # reload(cq.sketch) + # reload(cq.occ_impl.exporters.svg) + # reload(cq.cq) + # reload(cq.occ_impl.exporters.dxf) + # reload(cq.occ_impl.exporters.amf) + # reload(cq.occ_impl.exporters.json) + # # reload(cq.occ_impl.exporters.assembly) + # reload(cq.occ_impl.exporters) + # reload(cq.assembly) + # reload(cq) + pass def is_obj_empty(obj: Union[cq.Workplane, cq.Shape]) -> bool: From 69c37751a441e5bd4dce0443e761576ae5a7ac5a Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 6 Oct 2024 10:58:25 +0200 Subject: [PATCH 15/45] Fix TopoDS_Shape support --- cq_editor/cq_utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cq_editor/cq_utils.py b/cq_editor/cq_utils.py index 7348128e..473ddd41 100644 --- a/cq_editor/cq_utils.py +++ b/cq_editor/cq_utils.py @@ -31,7 +31,9 @@ DEFAULT_TRIHEDRON_SIZE = 0.1 CompoundLike = Union[cq.Shape, cq.Workplane, cq.Sketch, cq.Assembly] -AISLike = Union[CompoundLike, cq.Location, cq.Plane, cq.Vector, AIS_InteractiveObject] +AISLike = Union[ + CompoundLike, cq.Location, cq.Plane, cq.Vector, AIS_InteractiveObject, TopoDS_Shape +] AISLikeLists = Union[tuple(List[T] for T in AISLike.__args__)] Showable = Union[AISLike, AISLikeLists] @@ -170,6 +172,9 @@ def make_AIS( elif isinstance(obj, cq.Vector): ais = AIS_Point(Geom_CartesianPoint(obj.toPnt())) + elif isinstance(obj, TopoDS_Shape): + ais = AIS_Shape(obj) + elif isinstance(obj, AISLikeLists): rv = [make_AIS(el, options)[0][0] for el in obj] From afbbfc381a4b06e360763b673117d9db1c4f6b87 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 6 Oct 2024 11:03:45 +0200 Subject: [PATCH 16/45] fix tests --- tests/test_app.py | 1186 ++++++++++++++++++++++++--------------------- 1 file changed, 628 insertions(+), 558 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index aeac8c30..84a26e3c 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,7 +1,7 @@ from path import Path import os, sys, asyncio -if sys.platform == 'win32': +if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) from multiprocessing import Process @@ -17,61 +17,54 @@ from cq_editor.widgets.editor import Editor from cq_editor.cq_utils import export, get_occ_color -code = \ -'''import cadquery as cq +code = """import cadquery as cq result = cq.Workplane("XY" ) result = result.box(3, 3, 0.5) -result = result.edges("|Z").fillet(0.125)''' +result = result.edges("|Z").fillet(0.125)""" -code_bigger_object = \ -'''import cadquery as cq +code_bigger_object = """import cadquery as cq result = cq.Workplane("XY" ) result = result.box(20, 20, 0.5) result = result.edges("|Z").fillet(0.125) -''' +""" -code_show_Workplane = \ -'''import cadquery as cq +code_show_Workplane = """import cadquery as cq result = cq.Workplane("XY" ) result = result.box(3, 3, 0.5) result = result.edges("|Z").fillet(0.125) show_object(result) -''' +""" -code_show_Workplane_named = \ -'''import cadquery as cq +code_show_Workplane_named = """import cadquery as cq result = cq.Workplane("XY" ) result = result.box(3, 3, 0.5) result = result.edges("|Z").fillet(0.125) log('test') show_object(result,name='test') -''' +""" -code_show_Shape = \ -'''import cadquery as cq +code_show_Shape = """import cadquery as cq result = cq.Workplane("XY" ) result = result.box(3, 3, 0.5) result = result.edges("|Z").fillet(0.125) show_object(result.val()) -''' +""" -code_debug_Workplane = \ -'''import cadquery as cq +code_debug_Workplane = """import cadquery as cq result = cq.Workplane("XY" ) result = result.box(3, 3, 0.5) result = result.edges("|Z").fillet(0.125) debug(result) -''' +""" -code_multi = \ -'''import cadquery as cq +code_multi = """import cadquery as cq result1 = cq.Workplane("XY" ).box(3, 3, 0.5) result2 = cq.Workplane("XY" ).box(3, 3, 0.5).translate((0,15,0)) -''' +""" code_nested_top = """import test_nested_bottom """ @@ -91,31 +84,35 @@ sk = cq.Sketch().rect(1,1) """ + def _modify_file(code, path="test.py"): with open(path, "w", 1) as f: f.write(code) def modify_file(code, path="test.py"): - p = Process(target=_modify_file, args=(code,path)) + p = Process(target=_modify_file, args=(code, path)) p.start() p.join() + def get_center(widget): pos = widget.pos() - pos.setX(pos.x()+widget.width()//2) - pos.setY(pos.y()+widget.height()//2) + pos.setX(pos.x() + widget.width() // 2) + pos.setY(pos.y() + widget.height() // 2) return pos + def get_bottom_left(widget): pos = widget.pos() - pos.setY(pos.y()+widget.height()) + pos.setY(pos.y() + widget.height()) return pos + def get_rgba(ais): alpha = ais.Transparency() @@ -123,28 +120,30 @@ def get_rgba(ais): return color.redF(), color.greenF(), color.blueF(), alpha + @pytest.fixture -def main(qtbot,mocker): +def main(qtbot, mocker): - mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes) + mocker.patch.object(QMessageBox, "question", return_value=QMessageBox.Yes) win = MainWindow() win.show() qtbot.addWidget(win) - editor = win.components['editor'] + editor = win.components["editor"] editor.set_text(code) - debugger = win.components['debugger'] - debugger._actions['Run'][0].triggered.emit() + debugger = win.components["debugger"] + debugger._actions["Run"][0].triggered.emit() return qtbot, win + @pytest.fixture -def main_clean(qtbot,mocker): +def main_clean(qtbot, mocker): - mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes) + mocker.patch.object(QMessageBox, "question", return_value=QMessageBox.Yes) win = MainWindow() win.show() @@ -152,15 +151,16 @@ def main_clean(qtbot,mocker): qtbot.addWidget(win) qtbot.waitForWindowShown(win) - editor = win.components['editor'] + editor = win.components["editor"] editor.set_text(code) return qtbot, win + @pytest.fixture -def main_clean_do_not_close(qtbot,mocker): +def main_clean_do_not_close(qtbot, mocker): - mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.No) + mocker.patch.object(QMessageBox, "question", return_value=QMessageBox.No) win = MainWindow() win.show() @@ -168,16 +168,17 @@ def main_clean_do_not_close(qtbot,mocker): qtbot.addWidget(win) qtbot.waitForWindowShown(win) - editor = win.components['editor'] + editor = win.components["editor"] editor.set_text(code) return qtbot, win + @pytest.fixture -def main_multi(qtbot,mocker): +def main_multi(qtbot, mocker): - mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes) - mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.step','')) + mocker.patch.object(QMessageBox, "question", return_value=QMessageBox.Yes) + mocker.patch.object(QFileDialog, "getSaveFileName", return_value=("out.step", "")) win = MainWindow() win.show() @@ -185,116 +186,120 @@ def main_multi(qtbot,mocker): qtbot.addWidget(win) qtbot.waitForWindowShown(win) - editor = win.components['editor'] + editor = win.components["editor"] editor.set_text(code_multi) - debugger = win.components['debugger'] - debugger._actions['Run'][0].triggered.emit() + debugger = win.components["debugger"] + debugger._actions["Run"][0].triggered.emit() return qtbot, win + def test_render(main): qtbot, win = main - obj_tree_comp = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - console = win.components['console'] - log = win.components['log'] + obj_tree_comp = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + console = win.components["console"] + log = win.components["log"] # enable CQ reloading - debugger.preferences['Reload CQ'] = True + debugger.preferences["Reload CQ"] = True # check that object was rendered - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that object was rendered usin explicit show_object call editor.set_text(code_show_Workplane) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that cq.Shape object was rendered using explicit show_object call editor.set_text(code_show_Shape) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # test rendering via console console.execute(code_show_Workplane) - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 console.execute(code_show_Shape) - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 # check object rendering using show_object call with a name specified and # debug call editor.set_text(code_show_Workplane_named) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() qtbot.wait(100) - assert(obj_tree_comp.CQ.child(0).text(0) == 'test') - assert('test' in log.toPlainText().splitlines()[-1]) + assert obj_tree_comp.CQ.child(0).text(0) == "test" + assert "test" in log.toPlainText().splitlines()[-1] # cq reloading check obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 editor.set_text(code_reload_issue) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() qtbot.wait(100) - assert(obj_tree_comp.CQ.childCount() == 3) + assert obj_tree_comp.CQ.childCount() == 3 - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() qtbot.wait(100) - assert(obj_tree_comp.CQ.childCount() == 3) + assert obj_tree_comp.CQ.childCount() == 3 -def test_export(main,mocker): + +def test_export(main, mocker): qtbot, win = main - debugger = win.components['debugger'] - debugger._actions['Run'][0].triggered.emit() + debugger = win.components["debugger"] + debugger._actions["Run"][0].triggered.emit() - #set focus - obj_tree = win.components['object_tree'].tree - obj_tree_comp = win.components['object_tree'] + # set focus + obj_tree = win.components["object_tree"].tree + obj_tree_comp = win.components["object_tree"] qtbot.mouseClick(obj_tree, Qt.LeftButton) qtbot.keyClick(obj_tree, Qt.Key_Down) qtbot.keyClick(obj_tree, Qt.Key_Down) - #export STL - mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.stl','')) + # export STL + mocker.patch.object(QFileDialog, "getSaveFileName", return_value=("out.stl", "")) obj_tree_comp._export_STL_action.triggered.emit() - assert(os.path.isfile('out.stl')) + assert os.path.isfile("out.stl") - #export STEP - mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.step','')) + # export STEP + mocker.patch.object(QFileDialog, "getSaveFileName", return_value=("out.step", "")) obj_tree_comp._export_STEP_action.triggered.emit() - assert(os.path.isfile('out.step')) + assert os.path.isfile("out.step") + + # clean + os.remove("out.step") + os.remove("out.stl") - #clean - os.remove('out.step') - os.remove('out.stl') def number_visible_items(viewer): from OCP.AIS import AIS_ListOfInteractive + l = AIS_ListOfInteractive() viewer_ctx = viewer._get_context() @@ -302,186 +307,226 @@ def number_visible_items(viewer): return l.Extent() + def test_inspect(main): qtbot, win = main - #set focus and make invisible - obj_tree = win.components['object_tree'].tree + # set focus and make invisible + obj_tree = win.components["object_tree"].tree qtbot.mouseClick(obj_tree, Qt.LeftButton) qtbot.keyClick(obj_tree, Qt.Key_Down) qtbot.keyClick(obj_tree, Qt.Key_Down) qtbot.keyClick(obj_tree, Qt.Key_Space) - #enable object inspector - insp = win.components['cq_object_inspector'] + # enable object inspector + insp = win.components["cq_object_inspector"] insp._toolbar_actions[0].toggled.emit(True) - #check if all stack items are visible in the tree - assert(insp.root.childCount() == 3) + # check if all stack items are visible in the tree + assert insp.root.childCount() == 3 - #check if correct number of items is displayed - viewer = win.components['viewer'] + # check if correct number of items is displayed + viewer = win.components["viewer"] insp.setCurrentItem(insp.root.child(0)) - assert(number_visible_items(viewer) == 4) + assert number_visible_items(viewer) == 4 insp.setCurrentItem(insp.root.child(1)) - assert(number_visible_items(viewer) == 7) + assert number_visible_items(viewer) == 7 insp.setCurrentItem(insp.root.child(2)) - assert(number_visible_items(viewer) == 4) + assert number_visible_items(viewer) == 4 insp._toolbar_actions[0].toggled.emit(False) - assert(number_visible_items(viewer) == 3) + assert number_visible_items(viewer) == 3 + class event_loop(object): - '''Used to mock the QEventLoop for the debugger component - ''' + """Used to mock the QEventLoop for the debugger component + """ - def __init__(self,callbacks): + def __init__(self, callbacks): self.callbacks = callbacks self.i = 0 def exec_(self): - if self.i 0) - assert(conv_line_ends(editor.get_text_with_eol()) == code) + # check that loading from file works properly + editor.load_from_file("test.py") + assert len(editor.get_text_with_eol()) > 0 + assert conv_line_ends(editor.get_text_with_eol()) == code - #check that loading from file works properly + # check that loading from file works properly editor.new() - assert(editor.get_text_with_eol() == '') + assert editor.get_text_with_eol() == "" - #monkeypatch QFileDialog methods + # monkeypatch QFileDialog methods def filename(*args, **kwargs): - return 'test.py',None + return "test.py", None def filename2(*args, **kwargs): - return 'test2.py',None + return "test2.py", None - monkeypatch.setattr(QFileDialog, 'getOpenFileName', - staticmethod(filename)) + monkeypatch.setattr(QFileDialog, "getOpenFileName", staticmethod(filename)) - monkeypatch.setattr(QFileDialog, 'getSaveFileName', - staticmethod(filename2)) + monkeypatch.setattr(QFileDialog, "getSaveFileName", staticmethod(filename2)) - #check that open file works properly + # check that open file works properly editor.open() - assert(conv_line_ends(editor.get_text_with_eol()) == code) + assert conv_line_ends(editor.get_text_with_eol()) == code - #check that save file works properly + # check that save file works properly editor.new() qtbot.mouseClick(editor, Qt.LeftButton) - qtbot.keyClick(editor,Qt.Key_A) + qtbot.keyClick(editor, Qt.Key_A) - assert(editor.document().isModified() == True) + assert editor.document().isModified() == True - editor.filename = 'test2.py' + editor.filename = "test2.py" editor.save() - assert(editor.document().isModified() == False) + assert editor.document().isModified() == False - monkeypatch.setattr(QFileDialog, 'getOpenFileName', - staticmethod(filename2)) + monkeypatch.setattr(QFileDialog, "getOpenFileName", staticmethod(filename2)) editor.open() - assert(editor.get_text_with_eol() == 'a') + assert editor.get_text_with_eol() == "a" - #check that save as works properly - os.remove('test2.py') + # check that save as works properly + os.remove("test2.py") editor.save_as() - assert(os.path.exists(filename2()[0])) + assert os.path.exists(filename2()[0]) - #test persistance - settings = QSettings('test') + # test persistance + settings = QSettings("test") editor.saveComponentState(settings) editor.new() - assert(editor.get_text_with_eol() == '') + assert editor.get_text_with_eol() == "" editor.restoreComponentState(settings) - assert(editor.get_text_with_eol() == 'a') + assert editor.get_text_with_eol() == "a" - #test error handling - os.remove('test2.py') - assert(not os.path.exists('test2.py')) + # test error handling + os.remove("test2.py") + assert not os.path.exists("test2.py") editor.restoreComponentState(settings) + @pytest.mark.repeat(1) -def test_editor_autoreload(monkeypatch,editor): +def test_editor_autoreload(monkeypatch, editor): qtbot, editor = editor @@ -668,13 +712,13 @@ def test_editor_autoreload(monkeypatch,editor): # start out with autoreload enabled editor.autoreload(True) - with open('test.py','w') as f: + with open("test.py", "w") as f: f.write(code) - assert(editor.get_text_with_eol() == '') + assert editor.get_text_with_eol() == "" - editor.load_from_file('test.py') - assert(len(editor.get_text_with_eol()) > 0) + editor.load_from_file("test.py") + assert len(editor.get_text_with_eol()) > 0 # wait for reload. with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): @@ -682,7 +726,7 @@ def test_editor_autoreload(monkeypatch,editor): modify_file(code_bigger_object) # check that editor has updated file contents - assert(code_bigger_object.splitlines()[2] in editor.get_text_with_eol()) + assert code_bigger_object.splitlines()[2] in editor.get_text_with_eol() # disable autoreload editor.autoreload(False) @@ -696,7 +740,7 @@ def test_editor_autoreload(monkeypatch,editor): modify_file(code) # editor should continue showing old contents since autoreload is disabled. - assert(code_bigger_object.splitlines()[2] in editor.get_text_with_eol()) + assert code_bigger_object.splitlines()[2] in editor.get_text_with_eol() # Saving a file with autoreload disabled should not trigger a rerender. with pytest.raises(pytestqt.exceptions.TimeoutError): @@ -709,6 +753,7 @@ def test_editor_autoreload(monkeypatch,editor): with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): editor.save() + def test_autoreload_nested(editor): qtbot, editor = editor @@ -716,156 +761,157 @@ def test_autoreload_nested(editor): TIMEOUT = 500 editor.autoreload(True) - editor.preferences['Autoreload: watch imported modules'] = True + editor.preferences["Autoreload: watch imported modules"] = True - with open('test_nested_top.py','w') as f: + with open("test_nested_top.py", "w") as f: f.write(code_nested_top) - with open('test_nested_bottom.py','w') as f: + with open("test_nested_bottom.py", "w") as f: f.write("") - assert(editor.get_text_with_eol() == '') + assert editor.get_text_with_eol() == "" - editor.load_from_file('test_nested_top.py') - assert(len(editor.get_text_with_eol()) > 0) + editor.load_from_file("test_nested_top.py") + assert len(editor.get_text_with_eol()) > 0 # wait for reload. with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): # modify file - NB: separate process is needed to avoid Windows quirks - modify_file(code_nested_bottom, 'test_nested_bottom.py') + modify_file(code_nested_bottom, "test_nested_bottom.py") + def test_console(main): qtbot, win = main - console = win.components['console'] + console = win.components["console"] # test execute_command a = [] - console.push_vars({'a' : a}) - console.execute_command('a.append(1)') - assert(len(a) == 1) + console.push_vars({"a": a}) + console.execute_command("a.append(1)") + assert len(a) == 1 # test print_text pos_orig = console._prompt_pos - console.print_text('a') - assert(console._prompt_pos == pos_orig + len('a')) + console.print_text("a") + assert console._prompt_pos == pos_orig + len("a") + def test_viewer(main): qtbot, win = main - viewer = win.components['viewer'] + viewer = win.components["viewer"] + + # not sure how to test this, so only smoke tests - #not sure how to test this, so only smoke tests + # trigger all 'View' actions + actions = viewer._actions["View"] + for a in actions: + a.trigger() - #trigger all 'View' actions - actions = viewer._actions['View'] - for a in actions: a.trigger() -code_module = \ -'''def dummy(): return True''' +code_module = """def dummy(): return True""" + +code_import = """from module import dummy +assert(dummy())""" -code_import = \ -'''from module import dummy -assert(dummy())''' def test_module_import(main): qtbot, win = main - editor = win.components['editor'] - debugger = win.components['debugger'] - traceback_view = win.components['traceback_viewer'] + editor = win.components["editor"] + debugger = win.components["debugger"] + traceback_view = win.components["traceback_viewer"] - #save the dummy module - with open('module.py','w') as f: + # save the dummy module + with open("module.py", "w") as f: f.write(code_module) - #run the code importing this module + # run the code importing this module editor.set_text(code_import) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() - #verify that no exception was generated - assert(traceback_view.current_exception.text() == '') + # verify that no exception was generated + assert traceback_view.current_exception.text() == "" -def test_auto_fit_view(main_clean): - def concat(eye,proj,scale): - return eye+proj+(scale,) +def test_auto_fit_view(main_clean): + def concat(eye, proj, scale): + return eye + proj + (scale,) - def approx_view_properties(eye,proj,scale): + def approx_view_properties(eye, proj, scale): - return pytest.approx(eye+proj+(scale,)) + return pytest.approx(eye + proj + (scale,)) qtbot, win = main_clean - editor = win.components['editor'] - debugger = win.components['debugger'] - viewer = win.components['viewer'] - object_tree = win.components['object_tree'] + editor = win.components["editor"] + debugger = win.components["debugger"] + viewer = win.components["viewer"] + object_tree = win.components["object_tree"] view = viewer.canvas.view - viewer.preferences['Fit automatically'] = False - eye0,proj0,scale0 = view.Eye(),view.Proj(),view.Scale() + viewer.preferences["Fit automatically"] = False + eye0, proj0, scale0 = view.Eye(), view.Proj(), view.Scale() # check if camera position is adjusted automatically when rendering for the # first time debugger.render() - eye1,proj1,scale1 = view.Eye(),view.Proj(),view.Scale() - assert( concat(eye0,proj0,scale0) != \ - approx_view_properties(eye1,proj1,scale1) ) + eye1, proj1, scale1 = view.Eye(), view.Proj(), view.Scale() + assert concat(eye0, proj0, scale0) != approx_view_properties(eye1, proj1, scale1) # check if camera position is not changed fter code change editor.set_text(code_bigger_object) debugger.render() - eye2,proj2,scale2 = view.Eye(),view.Proj(),view.Scale() - assert( concat(eye1,proj1,scale1) == \ - approx_view_properties(eye2,proj2,scale2) ) + eye2, proj2, scale2 = view.Eye(), view.Proj(), view.Scale() + assert concat(eye1, proj1, scale1) == approx_view_properties(eye2, proj2, scale2) # check if position is adjusted automatically after erasing all objects object_tree.removeObjects() debugger.render() - eye3,proj3,scale3 = view.Eye(),view.Proj(),view.Scale() - assert( concat(eye2,proj2,scale2) != \ - approx_view_properties(eye3,proj3,scale3) ) + eye3, proj3, scale3 = view.Eye(), view.Proj(), view.Scale() + assert concat(eye2, proj2, scale2) != approx_view_properties(eye3, proj3, scale3) # check if position is adjusted automatically if settings are changed - viewer.preferences['Fit automatically'] = True + viewer.preferences["Fit automatically"] = True editor.set_text(code) debugger.render() - eye4,proj4,scale4 = view.Eye(),view.Proj(),view.Scale() - assert( concat(eye3,proj3,scale3) != \ - approx_view_properties(eye4,proj4,scale4) ) + eye4, proj4, scale4 = view.Eye(), view.Proj(), view.Scale() + assert concat(eye3, proj3, scale3) != approx_view_properties(eye4, proj4, scale4) + def test_preserve_properties(main): qtbot, win = main - debugger = win.components['debugger'] - debugger._actions['Run'][0].triggered.emit() + debugger = win.components["debugger"] + debugger._actions["Run"][0].triggered.emit() - object_tree = win.components['object_tree'] - object_tree.preferences['Preserve properties on reload'] = True + object_tree = win.components["object_tree"] + object_tree.preferences["Preserve properties on reload"] = True - assert(object_tree.CQ.childCount() == 1) + assert object_tree.CQ.childCount() == 1 props = object_tree.CQ.child(0).properties - props['Visible'] = False - props['Color'] = '#caffee' - props['Alpha'] = 0.5 + props["Visible"] = False + props["Color"] = "#caffee" + props["Alpha"] = 0.5 - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() - assert(object_tree.CQ.childCount() == 1) + assert object_tree.CQ.childCount() == 1 props = object_tree.CQ.child(0).properties - assert(props['Visible'] == False) - assert(props['Color'].name() == '#caffee') - assert(props['Alpha'] == 0.5) + assert props["Visible"] == False + assert props["Color"].name() == "#caffee" + assert props["Alpha"] == 0.5 + -def test_selection(main_multi,mocker): +def test_selection(main_multi, mocker): qtbot, win = main_multi - viewer = win.components['viewer'] - object_tree = win.components['object_tree'] + viewer = win.components["viewer"] + object_tree = win.components["object_tree"] CQ = object_tree.CQ obj1 = CQ.child(0) @@ -876,23 +922,23 @@ def test_selection(main_multi,mocker): obj2.setSelected(True) object_tree._export_STEP_action.triggered.emit() - imported = cq.importers.importStep('out.step') - assert(len(imported.solids().vals()) == 2) + imported = cq.importers.importStep("out.step") + assert len(imported.solids().vals()) == 2 # export with one selected objects obj2.setSelected(False) object_tree._export_STEP_action.triggered.emit() - imported = cq.importers.importStep('out.step') - assert(len(imported.solids().vals()) == 1) + imported = cq.importers.importStep("out.step") + assert len(imported.solids().vals()) == 1 # export with one selected objects obj1.setSelected(False) CQ.setSelected(True) object_tree._export_STEP_action.triggered.emit() - imported = cq.importers.importStep('out.step') - assert(len(imported.solids().vals()) == 2) + imported = cq.importers.importStep("out.step") + assert len(imported.solids().vals()) == 2 # check if viewer and object tree are properly connected CQ.setSelected(False) @@ -905,15 +951,15 @@ def test_selection(main_multi,mocker): while ctx.MoreSelected(): shapes.append(ctx.SelectedShape()) ctx.NextSelected() - assert(len(shapes) == 2) + assert len(shapes) == 2 viewer.fit() qtbot.mouseClick(viewer.canvas, Qt.LeftButton) - assert(len(object_tree.tree.selectedItems()) == 0) + assert len(object_tree.tree.selectedItems()) == 0 viewer.sigObjectSelected.emit([obj1.shape_display.wrapped]) - assert(len(object_tree.tree.selectedItems()) == 1) + assert len(object_tree.tree.selectedItems()) == 1 # go through different handleSelection paths qtbot.mouseClick(object_tree.tree, Qt.LeftButton) @@ -922,111 +968,121 @@ def test_selection(main_multi,mocker): qtbot.keyClick(object_tree.tree, Qt.Key_Down) qtbot.keyClick(object_tree.tree, Qt.Key_Down) - assert(object_tree._export_STL_action.isEnabled() == False) - assert(object_tree._export_STEP_action.isEnabled() == False) - assert(object_tree._clear_current_action.isEnabled() == False) - assert(object_tree.properties_editor.isEnabled() == False) + assert object_tree._export_STL_action.isEnabled() == False + assert object_tree._export_STEP_action.isEnabled() == False + assert object_tree._clear_current_action.isEnabled() == False + assert object_tree.properties_editor.isEnabled() == False + def test_closing(main_clean_do_not_close): - qtbot,win = main_clean_do_not_close + qtbot, win = main_clean_do_not_close - editor = win.components['editor'] + editor = win.components["editor"] # make sure that windows is visible - assert(win.isVisible()) + assert win.isVisible() # should not quit win.close() - assert(win.isVisible()) + assert win.isVisible() # should quit editor.reset_modified() win.close() - assert(not win.isVisible()) + assert not win.isVisible() -def test_check_for_updates(main,mocker): - qtbot,win = main +def test_check_for_updates(main, mocker): + + qtbot, win = main # patch requests import requests - mocker.patch.object(requests.models.Response,'json', - return_value=[{'tag_name' : '0.0.2','draft' : False}]) + + mocker.patch.object( + requests.models.Response, + "json", + return_value=[{"tag_name": "0.0.2", "draft": False}], + ) # stub QMessageBox about about_stub = mocker.stub() - mocker.patch.object(QMessageBox, 'about', about_stub) + mocker.patch.object(QMessageBox, "about", about_stub) import cadquery - cadquery.__version__ = '0.0.1' + cadquery.__version__ = "0.0.1" win.check_for_cq_updates() - assert(about_stub.call_args[0][1] == 'Updates available') + assert about_stub.call_args[0][1] == "Updates available" - cadquery.__version__ = '0.0.3' + cadquery.__version__ = "0.0.3" win.check_for_cq_updates() - assert(about_stub.call_args[0][1] == 'No updates available') + assert about_stub.call_args[0][1] == "No updates available" + -@pytest.mark.skipif(sys.platform.startswith('linux'),reason='Segfault workaround for linux') -def test_screenshot(main,mocker): +@pytest.mark.skipif( + sys.platform.startswith("linux"), reason="Segfault workaround for linux" +) +def test_screenshot(main, mocker): - qtbot,win = main + qtbot, win = main - mocker.patch.object(QFileDialog, 'getSaveFileName', return_value=('out.png','')) + mocker.patch.object(QFileDialog, "getSaveFileName", return_value=("out.png", "")) - viewer = win.components['viewer'] - viewer._actions['Tools'][0].triggered.emit() + viewer = win.components["viewer"] + viewer._actions["Tools"][0].triggered.emit() + + assert os.path.exists("out.png") - assert(os.path.exists('out.png')) def test_resize(main): - qtbot,win = main - editor = win.components['editor'] + qtbot, win = main + editor = win.components["editor"] editor.hide() qtbot.wait(50) editor.show() qtbot.wait(50) -code_simple_step = \ -'''import cadquery as cq + +code_simple_step = """import cadquery as cq imported = cq.importers.importStep('shape.step') -''' +""" + def test_relative_references(main): # create code with a relative reference in a subdirectory - p = Path('test_relative_references') + p = Path("test_relative_references") p.mkdir_p() - p_code = p.joinpath('code.py') + p_code = p.joinpath("code.py") p_code.write_text(code_simple_step) # create the referenced step file shape = cq.Workplane("XY").box(1, 1, 1) - p_step = p.joinpath('shape.step') + p_step = p.joinpath("shape.step") export(shape, "step", p_step) # open code qtbot, win = main - editor = win.components['editor'] + editor = win.components["editor"] editor.load_from_file(p_code) # render - debugger = win.components['debugger'] - debugger._actions['Run'][0].triggered.emit() + debugger = win.components["debugger"] + debugger._actions["Run"][0].triggered.emit() # assert no errors - traceback_view = win.components['traceback_viewer'] - assert(traceback_view.current_exception.text() == '') + traceback_view = win.components["traceback_viewer"] + assert traceback_view.current_exception.text() == "" # assert one object has been rendered - obj_tree_comp = win.components['object_tree'] - assert(obj_tree_comp.CQ.childCount() == 1) + obj_tree_comp = win.components["object_tree"] + assert obj_tree_comp.CQ.childCount() == 1 # clean up p_code.remove_p() p_step.remove_p() p.rmdir_p() -code_color = \ -''' +code_color = """ import cadquery as cq result = cq.Workplane("XY" ).box(1, 1, 1) @@ -1037,270 +1093,274 @@ def test_relative_references(main): show_object(result, name ='5', options=dict(alpha=0.5,color=(1.,0,0))) show_object(result, name ='6', options=dict(rgba=(1.,0,0,.5))) show_object(result, name ='7', options=dict(color=('ff','cc','dd'))) -''' +""" + def test_render_colors(main_clean): qtbot, win = main_clean - obj_tree = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - log = win.components['log'] + obj_tree = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + log = win.components["log"] editor.set_text(code_color) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() CQ = obj_tree.CQ # object 1 (defualt color) - assert not CQ.child(0).ais.HasColor() + assert not CQ.child(0).ais[0].HasColor() # object 2 - r,g,b,a = get_rgba(CQ.child(1).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) - assert( g == 0.0 ) + r, g, b, a = get_rgba(CQ.child(1).ais[0]) + assert a == 0.5 + assert r == 1.0 + assert g == 0.0 # object 3 - r,g,b,a = get_rgba(CQ.child(2).ais) - assert( a == 0.5) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(2).ais[0]) + assert a == 0.5 + assert r == 1.0 # object 4 - r,g,b,a = get_rgba(CQ.child(3).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(3).ais[0]) + assert a == 0.5 + assert r == 1.0 # object 5 - r,g,b,a = get_rgba(CQ.child(4).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(4).ais[0]) + assert a == 0.5 + assert r == 1.0 # object 6 - r,g,b,a = get_rgba(CQ.child(5).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(5).ais[0]) + assert a == 0.5 + assert r == 1.0 # check if error occured qtbot.wait(100) - assert('Unknown color format' in log.toPlainText().splitlines()[-1]) + assert "Unknown color format" in log.toPlainText().splitlines()[-1] + def test_render_colors_console(main_clean): qtbot, win = main_clean - obj_tree = win.components['object_tree'] - log = win.components['log'] - console = win.components['console'] + obj_tree = win.components["object_tree"] + log = win.components["log"] + console = win.components["console"] console.execute_command(code_color) CQ = obj_tree.CQ # object 1 (defualt color) - assert not CQ.child(0).ais.HasColor() + assert not CQ.child(0).ais[0].HasColor() # object 2 - r,g,b,a = get_rgba(CQ.child(1).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(1).ais[0]) + assert a == 0.5 + assert r == 1.0 # object 3 - r,g,b,a = get_rgba(CQ.child(2).ais) - assert( a == 0.5) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(2).ais[0]) + assert a == 0.5 + assert r == 1.0 # object 4 - r,g,b,a = get_rgba(CQ.child(3).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(3).ais[0]) + assert a == 0.5 + assert r == 1.0 # object 5 - r,g,b,a = get_rgba(CQ.child(4).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(4).ais[0]) + assert a == 0.5 + assert r == 1.0 # object 6 - r,g,b,a = get_rgba(CQ.child(5).ais) - assert( a == 0.5 ) - assert( r == 1.0 ) + r, g, b, a = get_rgba(CQ.child(5).ais[0]) + assert a == 0.5 + assert r == 1.0 # check if error occured qtbot.wait(100) - assert('Unknown color format' in log.toPlainText().splitlines()[-1]) + assert "Unknown color format" in log.toPlainText().splitlines()[-1] + -code_shading = \ -''' +code_shading = """ import cadquery as cq res1 = cq.Workplane('XY').box(5, 7, 5) res2 = cq.Workplane('XY').box(8, 5, 4) show_object(res1) show_object(res2,options={"alpha":0}) -''' +""" + def test_shading_aspect(main_clean): qtbot, win = main_clean - obj_tree = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] + obj_tree = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] editor.set_text(code_shading) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() CQ = obj_tree.CQ # get material aspects - ma1 = CQ.child(0).ais.Attributes().ShadingAspect().Material() - ma2 = CQ.child(1).ais.Attributes().ShadingAspect().Material() + ma1 = CQ.child(0).ais[0].Attributes().ShadingAspect().Material() + ma2 = CQ.child(1).ais[0].Attributes().ShadingAspect().Material() # verify that they are the same assert ma1.Shininess() == ma2.Shininess() -def test_confirm_new(monkeypatch,editor): + +def test_confirm_new(monkeypatch, editor): qtbot, editor = editor - #check that initial state is as expected - assert(editor.modified == False) + # check that initial state is as expected + assert editor.modified == False editor.document().setPlainText(code) - assert(editor.modified == True) + assert editor.modified == True - #monkeypatch the confirmation dialog and run both scenarios + # monkeypatch the confirmation dialog and run both scenarios def cancel(*args, **kwargs): return QMessageBox.No def ok(*args, **kwargs): return QMessageBox.Yes - monkeypatch.setattr(QMessageBox, 'question', - staticmethod(cancel)) + monkeypatch.setattr(QMessageBox, "question", staticmethod(cancel)) editor.new() - assert(editor.modified == True) - assert(conv_line_ends(editor.get_text_with_eol()) == code) + assert editor.modified == True + assert conv_line_ends(editor.get_text_with_eol()) == code - monkeypatch.setattr(QMessageBox, 'question', - staticmethod(ok)) + monkeypatch.setattr(QMessageBox, "question", staticmethod(ok)) editor.new() - assert(editor.modified == False) - assert(editor.get_text_with_eol() == '') + assert editor.modified == False + assert editor.get_text_with_eol() == "" + -code_show_topods = \ -''' +code_show_topods = """ import cadquery as cq result = cq.Workplane("XY" ).box(1, 1, 1) show_object(result.val().wrapped) -''' +""" + def test_render_topods(main): qtbot, win = main - obj_tree_comp = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - console = win.components['console'] + obj_tree_comp = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + console = win.components["console"] # check that object was rendered - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that object was rendered usin explicit show_object call editor.set_text(code_show_topods) - debugger._actions['Run'][0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 1) + debugger._actions["Run"][0].triggered.emit() + assert obj_tree_comp.CQ.childCount() == 1 # test rendering of topods object via console - console.execute('show(result.val().wrapped)') - assert(obj_tree_comp.CQ.childCount() == 2) + console.execute("show(result.val().wrapped)") + assert obj_tree_comp.CQ.childCount() == 2 # test rendering of list of topods object via console - console.execute('show([result.val().wrapped,result.val().wrapped])') - assert(obj_tree_comp.CQ.childCount() == 3) + console.execute("show([result.val().wrapped,result.val().wrapped])") + assert obj_tree_comp.CQ.childCount() == 3 -code_show_shape_list = \ -''' +code_show_shape_list = """ import cadquery as cq result1 = cq.Workplane("XY" ).box(1, 1, 1).val() result2 = cq.Workplane("XY",origin=(0,1,1)).box(1, 1, 1).val() show_object(result1) show_object([result1,result2]) -''' +""" + def test_render_shape_list(main): qtbot, win = main - log = win.components['log'] + log = win.components["log"] - obj_tree_comp = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - console = win.components['console'] + obj_tree_comp = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + console = win.components["console"] # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that object was rendered usin explicit show_object call editor.set_text(code_show_shape_list) - debugger._actions['Run'][0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 2) + debugger._actions["Run"][0].triggered.emit() + assert obj_tree_comp.CQ.childCount() == 2 # test rendering of Shape via console - console.execute('show(result1)') - console.execute('show([result1,result2])') - assert(obj_tree_comp.CQ.childCount() == 4) + console.execute("show(result1)") + console.execute("show([result1,result2])") + assert obj_tree_comp.CQ.childCount() == 4 # smoke test exception in show console.execute('show("a")') -code_show_assy = \ -'''import cadquery as cq + +code_show_assy = """import cadquery as cq result1 = cq.Workplane("XY" ).box(3, 3, 0.5) assy = cq.Assembly(result1) show_object(assy) -''' +""" + def test_render_assy(main): qtbot, win = main - obj_tree_comp = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - console = win.components['console'] + obj_tree_comp = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + console = win.components["console"] # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that object was rendered usin explicit show_object call editor.set_text(code_show_assy) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() qtbot.wait(500) - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 # test rendering via console - console.execute('show(assy)') + console.execute("show(assy)") qtbot.wait(500) - assert(obj_tree_comp.CQ.childCount() == 2) + assert obj_tree_comp.CQ.childCount() == 2 + -code_show_ais = \ -'''import cadquery as cq +code_show_ais = """import cadquery as cq from cadquery.occ_impl.assembly import toCAF import OCP @@ -1312,101 +1372,107 @@ def test_render_assy(main): ais = OCP.XCAFPrs.XCAFPrs_AISObject(lab) show_object(ais) -''' +""" + def test_render_ais(main): qtbot, win = main - obj_tree_comp = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - console = win.components['console'] + obj_tree_comp = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + console = win.components["console"] # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that object was rendered usin explicit show_object call editor.set_text(code_show_ais) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() qtbot.wait(500) - assert(obj_tree_comp.CQ.childCount() == 1) + assert obj_tree_comp.CQ.childCount() == 1 # test rendering via console - console.execute('show(ais)') + console.execute("show(ais)") qtbot.wait(500) - assert(obj_tree_comp.CQ.childCount() == 2) + assert obj_tree_comp.CQ.childCount() == 2 -code_show_sketch = \ -'''import cadquery as cq + +code_show_sketch = """import cadquery as cq s1 = cq.Sketch().rect(1,1) s2 = cq.Sketch().segment((0,0), (0,3.),"s1") show_object(s1) show_object(s2) -''' +""" + def test_render_sketch(main): qtbot, win = main - obj_tree_comp = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - console = win.components['console'] + obj_tree_comp = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + console = win.components["console"] # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that object was rendered usin explicit show_object call editor.set_text(code_show_sketch) - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() qtbot.wait(500) - assert(obj_tree_comp.CQ.childCount() == 2) + assert obj_tree_comp.CQ.childCount() == 2 # test rendering via console - console.execute('show(s1); show(s2)') + console.execute("show(s1); show(s2)") qtbot.wait(500) - assert(obj_tree_comp.CQ.childCount() == 4) + assert obj_tree_comp.CQ.childCount() == 4 + def test_window_title(monkeypatch, main): - fname = 'test_window_title.py' + fname = "test_window_title.py" - with open(fname, 'w') as f: + with open(fname, "w") as f: f.write(code) qtbot, win = main - #monkeypatch QFileDialog methods + # monkeypatch QFileDialog methods def filename(*args, **kwargs): return fname, None - monkeypatch.setattr(QFileDialog, 'getOpenFileName', - staticmethod(filename)) + monkeypatch.setattr(QFileDialog, "getOpenFileName", staticmethod(filename)) win.components["editor"].open() - assert(win.windowTitle().endswith(fname)) + assert win.windowTitle().endswith(fname) # handle a new file win.components["editor"].new() # I don't really care what the title is, as long as it's not a filename - assert(not win.windowTitle().endswith('.py')) + assert not win.windowTitle().endswith(".py") + def test_module_discovery(tmp_path, editor): qtbot, editor = editor - with open(tmp_path.joinpath('main.py'), 'w') as f: - f.write('import b') + with open(tmp_path.joinpath("main.py"), "w") as f: + f.write("import b") - assert editor.get_imported_module_paths(str(tmp_path.joinpath('main.py'))) == [] + assert editor.get_imported_module_paths(str(tmp_path.joinpath("main.py"))) == [] - tmp_path.joinpath('b.py').touch() + tmp_path.joinpath("b.py").touch() + + assert editor.get_imported_module_paths(str(tmp_path.joinpath("main.py"))) == [ + str(tmp_path.joinpath("b.py")) + ] - assert editor.get_imported_module_paths(str(tmp_path.joinpath('main.py'))) == [str(tmp_path.joinpath('b.py'))] def test_launch_syntax_error(tmp_path): @@ -1421,23 +1487,23 @@ def test_launch_syntax_error(tmp_path): editor.load_from_file(inputfile) win.show() - assert(win.isVisible()) + assert win.isVisible() -code_import_module_makebox = \ -""" + +code_import_module_makebox = """ from module_makebox import * z = 1 r = makebox(z) """ -code_module_makebox = \ -""" +code_module_makebox = """ import cadquery as cq def makebox(z): zval = z + 1 return cq.Workplane().box(1, 1, zval) """ + def test_reload_import_handle_error(tmp_path, main): TIMEOUT = 500 @@ -1458,18 +1524,18 @@ def test_reload_import_handle_error(tmp_path, main): # run, verify that no exception was generated editor.load_from_file(script) debugger._actions["Run"][0].triggered.emit() - assert(traceback_view.current_exception.text() == "") + assert traceback_view.current_exception.text() == "" # save the module with an error with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): lines = code_module_makebox.splitlines() - lines.remove(" zval = z + 1") # introduce NameError + lines.remove(" zval = z + 1") # introduce NameError lines = "\n".join(lines) modify_file(lines, module_file) # verify NameError is generated debugger._actions["Run"][0].triggered.emit() - assert("NameError" in traceback_view.current_exception.text()) + assert "NameError" in traceback_view.current_exception.text() # revert the error, verify rerender is triggered with qtbot.waitSignal(editor.triggerRerender, timeout=TIMEOUT): @@ -1477,7 +1543,8 @@ def test_reload_import_handle_error(tmp_path, main): # verify that no exception was generated debugger._actions["Run"][0].triggered.emit() - assert(traceback_view.current_exception.text() == "") + assert traceback_view.current_exception.text() == "" + def test_modulefinder(tmp_path, main): @@ -1486,7 +1553,7 @@ def test_modulefinder(tmp_path, main): editor = win.components["editor"] debugger = win.components["debugger"] traceback_view = win.components["traceback_viewer"] - log = win.components['log'] + log = win.components["log"] editor.autoreload(True) editor.preferences["Autoreload: watch imported modules"] = True @@ -1499,56 +1566,58 @@ def test_modulefinder(tmp_path, main): modify_file("import emptydir", script) qtbot.wait(100) - assert("Cannot determine imported modules" in log.toPlainText().splitlines()[-1]) + assert "Cannot determine imported modules" in log.toPlainText().splitlines()[-1] + def test_show_all(main): qtbot, win = main - editor = win.components['editor'] - debugger = win.components['debugger'] - object_tree = win.components['object_tree'] + editor = win.components["editor"] + debugger = win.components["debugger"] + object_tree = win.components["object_tree"] # remove all objects object_tree.removeObjects() - assert(object_tree.CQ.childCount() == 0) + assert object_tree.CQ.childCount() == 0 # add code wtih Shape, Workplane, Assy, Sketch editor.set_text(code_show_all) # Run and check if all are shown - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() - assert(object_tree.CQ.childCount() == 4) + assert object_tree.CQ.childCount() == 4 -code_randcolor = \ -"""import cadquery as cq + +code_randcolor = """import cadquery as cq b = cq.Workplane().box(8, 3, 4) for i in range(10): show_object(b.translate((0,5*i,0)), options=rand_color(alpha=0)) show_object(b.translate((0,5*i,0)), options=rand_color(0, True)) """ + def test_randcolor(main): qtbot, win = main - obj_tree_comp = win.components['object_tree'] - editor = win.components['editor'] - debugger = win.components['debugger'] - console = win.components['console'] + obj_tree_comp = win.components["object_tree"] + editor = win.components["editor"] + debugger = win.components["debugger"] + console = win.components["console"] # check that object was removed obj_tree_comp._toolbar_actions[0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 0) + assert obj_tree_comp.CQ.childCount() == 0 # check that object was rendered usin explicit show_object call editor.set_text(code_randcolor) - debugger._actions['Run'][0].triggered.emit() - assert(obj_tree_comp.CQ.childCount() == 2*10) + debugger._actions["Run"][0].triggered.emit() + assert obj_tree_comp.CQ.childCount() == 2 * 10 -code_show_wo_name = \ -""" + +code_show_wo_name = """ import cadquery as cq res = cq.Workplane().box(1,1,1) @@ -1557,28 +1626,29 @@ def test_randcolor(main): show_object(cq.Workplane().box(1,1,1)) """ + def test_show_without_name(main): qtbot, win = main - editor = win.components['editor'] - debugger = win.components['debugger'] - object_tree = win.components['object_tree'] + editor = win.components["editor"] + debugger = win.components["debugger"] + object_tree = win.components["object_tree"] # remove all objects object_tree.removeObjects() - assert(object_tree.CQ.childCount() == 0) + assert object_tree.CQ.childCount() == 0 # add code wtih Shape, Workplane, Assy, Sketch editor.set_text(code_show_wo_name) # Run and check if all are shown - debugger._actions['Run'][0].triggered.emit() + debugger._actions["Run"][0].triggered.emit() - assert(object_tree.CQ.childCount() == 2) + assert object_tree.CQ.childCount() == 2 # Check the name of the first object - assert(object_tree.CQ.child(0).text(0) == "res") + assert object_tree.CQ.child(0).text(0) == "res" # Check that the name of the seconf object is an int int(object_tree.CQ.child(1).text(0)) From 75ecbe95af79bb59ddc15c795d41ece9ab0b4f28 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 6 Oct 2024 11:20:54 +0200 Subject: [PATCH 17/45] Update traceback/debug test --- tests/test_app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 84a26e3c..4858d81f 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -389,7 +389,7 @@ def assert_func(x): debugger = win.components["debugger"] actions = debugger._actions["Run"] - run, debug, step, step_in, cont = actions + run, run_cell, debug, step, step_in, cont = actions variables = win.components["variables_viewer"] @@ -572,7 +572,7 @@ def test_traceback(main): traceback_view = win.components["traceback_viewer"] actions = debugger._actions["Run"] - run, debug, step, step_in, cont = actions + run, run_cell, debug, step, step_in, cont = actions editor.set_text(code_err1) run.triggered.emit() From 5c9b64be044ffd76875d39250e4cd7733183374e Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 6 Oct 2024 17:30:54 +0200 Subject: [PATCH 18/45] Fix test_debug --- tests/test_app.py | 46 +++++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 4858d81f..69fa84c0 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -370,6 +370,10 @@ def patch_debugger(debugger, event_loop_mock): def test_debug(main, mocker): + # magic numbers + N_variables = 6 + N_visible = 3 + # store the tracing function trace_function = sys.gettrace() @@ -413,23 +417,23 @@ def check_no_error_occured(): ev = event_loop( [ lambda: ( - assert_func(variables.model().rowCount() == 5), - assert_func(number_visible_items(viewer) == 3), + assert_func(variables.model().rowCount() == N_variables), + assert_func(number_visible_items(viewer) == N_visible), step.triggered.emit(), ), lambda: ( - assert_func(variables.model().rowCount() == 5), - assert_func(number_visible_items(viewer) == 3), + assert_func(variables.model().rowCount() == N_variables), + assert_func(number_visible_items(viewer) == N_visible), step.triggered.emit(), ), lambda: ( - assert_func(variables.model().rowCount() == 6), - assert_func(number_visible_items(viewer) == 3), + assert_func(variables.model().rowCount() == N_variables + 1), + assert_func(number_visible_items(viewer) == N_visible), step.triggered.emit(), ), lambda: ( - assert_func(variables.model().rowCount() == 6), - assert_func(number_visible_items(viewer) == 4), + assert_func(variables.model().rowCount() == N_variables + 1), + assert_func(number_visible_items(viewer) == N_visible + 1), cont.triggered.emit(), ), ] @@ -442,15 +446,15 @@ def check_no_error_occured(): check_no_error_occured() assert variables.model().rowCount() == 2 - assert number_visible_items(viewer) == 4 + assert number_visible_items(viewer) == N_visible + 1 # test exit debug ev = event_loop( [ lambda: (step.triggered.emit(),), lambda: ( - assert_func(variables.model().rowCount() == 5), - assert_func(number_visible_items(viewer) == 3), + assert_func(variables.model().rowCount() == N_variables), + assert_func(number_visible_items(viewer) == N_visible), debug.triggered.emit(False), ), ] @@ -463,15 +467,15 @@ def check_no_error_occured(): check_no_error_occured() assert variables.model().rowCount() == 1 - assert number_visible_items(viewer) == 3 + assert number_visible_items(viewer) == N_visible # test breakpoint ev = event_loop( [ lambda: (cont.triggered.emit(),), lambda: ( - assert_func(variables.model().rowCount() == 6), - assert_func(number_visible_items(viewer) == 4), + assert_func(variables.model().rowCount() == N_variables + 1), + assert_func(number_visible_items(viewer) == N_visible + 1), cont.triggered.emit(), ), ] @@ -486,15 +490,15 @@ def check_no_error_occured(): check_no_error_occured() assert variables.model().rowCount() == 2 - assert number_visible_items(viewer) == 4 + assert number_visible_items(viewer) == N_visible + 1 # test breakpoint without using singals ev = event_loop( [ lambda: (cont.triggered.emit(),), lambda: ( - assert_func(variables.model().rowCount() == 6), - assert_func(number_visible_items(viewer) == 4), + assert_func(variables.model().rowCount() == N_variables + 1), + assert_func(number_visible_items(viewer) == N_visible + 1), cont.triggered.emit(), ), ] @@ -509,15 +513,15 @@ def check_no_error_occured(): check_no_error_occured() assert variables.model().rowCount() == 2 - assert number_visible_items(viewer) == 4 + assert number_visible_items(viewer) == N_visible + 1 # test debug() without using singals ev = event_loop( [ lambda: (cont.triggered.emit(),), lambda: ( - assert_func(variables.model().rowCount() == 6), - assert_func(number_visible_items(viewer) == 4), + assert_func(variables.model().rowCount() == N_variables + 1), + assert_func(number_visible_items(viewer) == N_visible + 1), cont.triggered.emit(), ), ] @@ -540,7 +544,7 @@ def check_no_error_occured(): assert r == 1.0 assert variables.model().rowCount() == 2 - assert number_visible_items(viewer) == 4 + assert number_visible_items(viewer) == N_visible + 1 # restore the tracing function sys.settrace(trace_function) From 86506277d25ab14034f80784d0586f0b4adaf163 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 6 Oct 2024 17:39:21 +0200 Subject: [PATCH 19/45] Fix stash and selection --- cq_editor/widgets/object_tree.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/cq_editor/widgets/object_tree.py b/cq_editor/widgets/object_tree.py index b7f3d736..9e372347 100644 --- a/cq_editor/widgets/object_tree.py +++ b/cq_editor/widgets/object_tree.py @@ -10,8 +10,7 @@ from pyqtgraph.parametertree import Parameter, ParameterTree -from OCP.AIS import AIS_Line, AIS_InteractiveObject -from OCP.Geom import Geom_Line +from OCP.AIS import AIS_InteractiveObject, AIS_Axis from OCP.gp import gp_Dir, gp_Pnt, gp_Ax1 from ..mixins import ComponentMixin @@ -242,8 +241,7 @@ def addLines(self): ("red", "lawngreen", "blue"), ((1, 0, 0), (0, 1, 0), (0, 0, 1)), ): - line_placement = Geom_Line(gp_Ax1(gp_Pnt(*origin), gp_Dir(*direction))) - line = AIS_Line(line_placement) + line = AIS_Axis(gp_Ax1(gp_Pnt(*origin), gp_Dir(*direction)), 10) line.SetColor(to_occ_color(color)) self.Helpers.addChild(ObjectTreeItem(name, ais=[line])) @@ -349,12 +347,12 @@ def stashObjects(self, action: bool): if action: self._stash = self.CQ.takeChildren() - removed_items_ais = [ch.ais for ch in self._stash] + removed_items_ais = [el for ch in self._stash for el in ch.ais] self.sigObjectsRemoved.emit(removed_items_ais) else: self.removeObjects() self.CQ.addChildren(self._stash) - ais_list = [el.ais for el in self._stash] + ais_list = [obj.ais for obj in self._stash] self.sigObjectsAdded.emit(ais_list) @pyqtSlot() @@ -392,7 +390,7 @@ def handleSelection(self): # emit list of all selected ais objects (might be empty) ais_objects = [ - el for item in items for el in item.ais if item.parent() is self.CQ + el for item in items if item.parent() is self.CQ for el in item.ais ] self.sigAISObjectsSelected.emit(ais_objects) From 1108dfe8d83a84eae1812a9f536bad13f1f70f48 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 6 Oct 2024 17:48:09 +0200 Subject: [PATCH 20/45] Fix inspector --- cq_editor/widgets/cq_object_inspector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cq_editor/widgets/cq_object_inspector.py b/cq_editor/widgets/cq_object_inspector.py index 9fc01eba..99bdc2fa 100644 --- a/cq_editor/widgets/cq_object_inspector.py +++ b/cq_editor/widgets/cq_object_inspector.py @@ -114,7 +114,7 @@ def handleSelection(self): ais = AIS_ColoredShape(obj.wrapped) inspected_items.append(ais) - self.sigDisplayObjects.emit(inspected_items, False) + self.sigDisplayObjects.emit([[el] for el in inspected_items], False) @pyqtSlot(object) def setObject(self, cq_obj): From 9f4f2860e59afb46064e44a71c6d2350cdd5840a Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 6 Oct 2024 18:07:12 +0200 Subject: [PATCH 21/45] Use Path.absolute --- cq_editor/widgets/debugger.py | 4 ++-- cq_editor/widgets/editor.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cq_editor/widgets/debugger.py b/cq_editor/widgets/debugger.py index 02bfd2c1..d4fa27ae 100644 --- a/cq_editor/widgets/debugger.py +++ b/cq_editor/widgets/debugger.py @@ -203,7 +203,7 @@ def get_current_script_path(self): filename = self.parent().components["editor"].filename if filename: - return Path(filename).abspath() + return Path(filename).absolute() def get_breakpoints(self): @@ -224,7 +224,7 @@ def compile_code(self, cq_script, cq_script_path=None): def _exec(self, code, locals_dict, globals_dict): with ExitStack() as stack: - p = (self.get_current_script_path() or Path("")).abspath().dirname() + p = (self.get_current_script_path() or Path("")).absolute().dirname() if self.preferences["Add script dir to path"] and p.exists(): sys.path.insert(0, p) diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index 4713be82..0a50dce6 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -169,7 +169,7 @@ def open(self): if not self.confirm_discard(): return - curr_dir = Path(self.filename).abspath().dirname() + curr_dir = Path(self.filename).absolute().dirname() fname = get_open_filename(self.EXTENSIONS, curr_dir) if fname != "": self.load_from_file(fname) From a041fb7aade526d9f7de3e898673d2b75cb9b0ea Mon Sep 17 00:00:00 2001 From: AU Date: Mon, 7 Oct 2024 21:55:24 +0200 Subject: [PATCH 22/45] Print a bt on segfault --- appveyor.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 24e019c2..619d2b9d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -45,10 +45,10 @@ test_script: on_success: - codecov -#on_failure: -# - qtdiag -# - ls /cores/core.* -# - lldb --core `ls /cores/core.*` --batch --one-line "bt" +on_failure: + - qtdiag + - ls /cores/core.* + - lldb --core `ls /cores/core.*` --batch --one-line "bt" on_finish: # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) From 3d4a48237975f4dd3d3e9ae4518962f7b56bb128 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 8 Oct 2024 08:08:41 +0200 Subject: [PATCH 23/45] Disable test_render on linux for now --- tests/test_app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_app.py b/tests/test_app.py index 69fa84c0..db2de0dd 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -194,7 +194,9 @@ def main_multi(qtbot, mocker): return qtbot, win - +@pytest.mark.skipif( + sys.platform.startswith("linux"), reason="Segfault workaround for linux" +) def test_render(main): qtbot, win = main From 494e4d5627a6d1d4fcf492fb344b03ca2e7e935b Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 8 Oct 2024 08:45:37 +0200 Subject: [PATCH 24/45] Disable another test for now --- tests/test_app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_app.py b/tests/test_app.py index db2de0dd..0acb0c1f 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1340,7 +1340,9 @@ def test_render_shape_list(main): show_object(assy) """ - +@pytest.mark.skipif( + sys.platform.startswith("linux"), reason="Segfault workaround for linux" +) def test_render_assy(main): qtbot, win = main From 1b6769c5e0ba991ed2156ff057a0cef2561c1d34 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 10 Oct 2024 07:47:00 +0200 Subject: [PATCH 25/45] Change context menu order --- cq_editor/widgets/object_tree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cq_editor/widgets/object_tree.py b/cq_editor/widgets/object_tree.py index 9e372347..80d83452 100644 --- a/cq_editor/widgets/object_tree.py +++ b/cq_editor/widgets/object_tree.py @@ -185,10 +185,10 @@ def __init__(self, parent): ) self._toolbar_actions = [ + self._clear_current_action, QAction( icon("delete-many"), "Clear all", self, triggered=self.removeObjects ), - self._clear_current_action, ] self.prepareMenu() From 7eccb4904e2e39d12883dd15e31eedef6ebfff34 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 10 Oct 2024 08:22:38 +0200 Subject: [PATCH 26/45] Fix selection handling Thanks to @neri-engineering --- cq_editor/widgets/occt_widget.py | 39 ++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/cq_editor/widgets/occt_widget.py b/cq_editor/widgets/occt_widget.py index cd8215f3..fce71644 100755 --- a/cq_editor/widgets/occt_widget.py +++ b/cq_editor/widgets/occt_widget.py @@ -1,8 +1,8 @@ from sys import platform - +from typing import Optional from PyQt5.QtWidgets import QWidget, QApplication -from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QEvent +from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QEvent, QPointF import OCP @@ -14,12 +14,18 @@ ZOOM_STEP = 0.9 +SELECT_MOVE_THRESHOLD = 2 class OCCTWidget(QWidget): sigObjectSelected = pyqtSignal(list) + # auxiliary variables for handling selection and mouse + pending_select: bool + click_pos: Optional[QPointF] + previous_pos: Optional[QPointF] + def __init__(self, parent=None): super(OCCTWidget, self).__init__(parent) @@ -42,6 +48,11 @@ def __init__(self, parent=None): # Trihedorn, lights, etc self.prepare_display() + # init state for handling selection and mouse + self.pending_select = False + self.click_pos = None + self.previous_pos = None + def prepare_display(self): view = self.view @@ -77,10 +88,12 @@ def mousePressEvent(self, event): if event.button() == Qt.LeftButton: self.view.StartRotation(pos.x(), pos.y()) + self.pending_select = True elif event.button() == Qt.RightButton: self.view.StartZoomAtPoint(pos.x(), pos.y()) - self.old_pos = pos + self.previous_pos = pos + self.click_pos = pos def mouseMoveEvent(self, event): @@ -90,13 +103,19 @@ def mouseMoveEvent(self, event): if event.buttons() == Qt.LeftButton: self.view.Rotation(x, y) + # if mouse was moved too much cancel selection + if (pos - self.click_pos).manhattanLength() > SELECT_MOVE_THRESHOLD: + self.pending_select = False + elif event.buttons() == Qt.MiddleButton: - self.view.Pan(x - self.old_pos.x(), self.old_pos.y() - y, theToStart=True) + self.view.Pan( + x - self.previous_pos.x(), self.previous_pos.y() - y, theToStart=True + ) elif event.buttons() == Qt.RightButton: - self.view.ZoomAtPoint(self.old_pos.x(), y, x, self.old_pos.y()) + self.view.ZoomAtPoint(self.previous_pos.x(), y, x, self.previous_pos.y()) - self.old_pos = pos + self.previous_pos = pos def mouseReleaseEvent(self, event): @@ -104,11 +123,13 @@ def mouseReleaseEvent(self, event): pos = event.pos() x, y = pos.x(), pos.y() - self.context.MoveTo(x, y, self.view, True) + if self.pending_select: + self._handle_selection(x, y) + self.pending_select = False - self._handle_selection() + def _handle_selection(self, x: float, y: float): - def _handle_selection(self): + self.context.MoveTo(x, y, self.view, True) self.context.Select(True) self.context.InitSelected() From db7a26a00a3d681b01f0de182940af5a191917d0 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 10 Oct 2024 08:49:06 +0200 Subject: [PATCH 27/45] Final black pass --- cq_editor/icons.py | 9 ++------- cq_editor/preferences.py | 4 +--- cq_editor/widgets/viewer.py | 9 +-------- 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/cq_editor/icons.py b/cq_editor/icons.py index 06ea06b5..b4da8237 100644 --- a/cq_editor/icons.py +++ b/cq_editor/icons.py @@ -32,9 +32,7 @@ ("fa.save", "fa.pencil"), { "options": [ - { - "scale_factor": 1, - }, + {"scale_factor": 1,}, {"scale_factor": 0.8, "offset": (0.2, 0.2)}, ] }, @@ -42,10 +40,7 @@ "run": (("fa.play",), {}), "delete": (("fa.trash",), {}), "delete-many": ( - ( - "fa.trash", - "fa.trash", - ), + ("fa.trash", "fa.trash",), { "options": [ {"scale_factor": 0.8, "offset": (0.2, 0.2), "color": "gray"}, diff --git a/cq_editor/preferences.py b/cq_editor/preferences.py index e37c9ff8..6e69ead0 100644 --- a/cq_editor/preferences.py +++ b/cq_editor/preferences.py @@ -8,9 +8,7 @@ class PreferencesTreeItem(QTreeWidgetItem): def __init__( - self, - name, - widget, + self, name, widget, ): super(PreferencesTreeItem, self).__init__(name) diff --git a/cq_editor/widgets/viewer.py b/cq_editor/widgets/viewer.py index e5439df1..dcf60273 100644 --- a/cq_editor/widgets/viewer.py +++ b/cq_editor/widgets/viewer.py @@ -102,14 +102,7 @@ def __init__(self, parent=None): self.create_actions(self) - self.layout_ = layout( - self, - [ - self.canvas, - ], - top_widget=self, - margin=0, - ) + self.layout_ = layout(self, [self.canvas,], top_widget=self, margin=0,) self.setup_default_drawer() self.updatePreferences() From 3153c84faa1b2f55eb24a063c83e555be3036562 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 10 Oct 2024 08:49:43 +0200 Subject: [PATCH 28/45] Blacken tests --- tests/test_app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_app.py b/tests/test_app.py index 0acb0c1f..d0d4cb4a 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -194,6 +194,7 @@ def main_multi(qtbot, mocker): return qtbot, win + @pytest.mark.skipif( sys.platform.startswith("linux"), reason="Segfault workaround for linux" ) @@ -1340,6 +1341,7 @@ def test_render_shape_list(main): show_object(assy) """ + @pytest.mark.skipif( sys.platform.startswith("linux"), reason="Segfault workaround for linux" ) From 8f420e0531fed7be172516e25881df1d73d703e4 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 14 Oct 2024 07:38:25 +0200 Subject: [PATCH 29/45] Debugging test_render --- tests/test_app.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index d0d4cb4a..d5b3d988 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -195,9 +195,9 @@ def main_multi(qtbot, mocker): return qtbot, win -@pytest.mark.skipif( - sys.platform.startswith("linux"), reason="Segfault workaround for linux" -) +# @pytest.mark.skipif( +# sys.platform.startswith("linux"), reason="Segfault workaround for linux" +# ) def test_render(main): qtbot, win = main @@ -224,6 +224,8 @@ def test_render(main): assert obj_tree_comp.CQ.childCount() == 1 + return + obj_tree_comp._toolbar_actions[0].triggered.emit() assert obj_tree_comp.CQ.childCount() == 0 From 93646b29c749d25e7fa499eecfb856d9f0e36103 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 14 Oct 2024 10:53:03 +0200 Subject: [PATCH 30/45] Advance --- tests/test_app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index d5b3d988..50fa5632 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -224,8 +224,6 @@ def test_render(main): assert obj_tree_comp.CQ.childCount() == 1 - return - obj_tree_comp._toolbar_actions[0].triggered.emit() assert obj_tree_comp.CQ.childCount() == 0 @@ -235,6 +233,8 @@ def test_render(main): assert obj_tree_comp.CQ.childCount() == 1 + return + obj_tree_comp._toolbar_actions[0].triggered.emit() assert obj_tree_comp.CQ.childCount() == 0 From 7ce9b608753df57e630f876cde1c82ba3ad6c3e2 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 14 Oct 2024 11:25:12 +0200 Subject: [PATCH 31/45] Advance --- tests/test_app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 50fa5632..0472d75f 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -233,8 +233,6 @@ def test_render(main): assert obj_tree_comp.CQ.childCount() == 1 - return - obj_tree_comp._toolbar_actions[0].triggered.emit() assert obj_tree_comp.CQ.childCount() == 0 @@ -242,6 +240,8 @@ def test_render(main): console.execute(code_show_Workplane) assert obj_tree_comp.CQ.childCount() == 1 + return + obj_tree_comp._toolbar_actions[0].triggered.emit() assert obj_tree_comp.CQ.childCount() == 0 From 93aacb4077d8b277613790720376f91cd227ab63 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 14 Oct 2024 11:36:18 +0200 Subject: [PATCH 32/45] Advance --- tests/test_app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 0472d75f..c40bf8ff 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -240,14 +240,14 @@ def test_render(main): console.execute(code_show_Workplane) assert obj_tree_comp.CQ.childCount() == 1 - return - obj_tree_comp._toolbar_actions[0].triggered.emit() assert obj_tree_comp.CQ.childCount() == 0 console.execute(code_show_Shape) assert obj_tree_comp.CQ.childCount() == 1 + return + # check object rendering using show_object call with a name specified and # debug call editor.set_text(code_show_Workplane_named) From 14fccc68ebba6c18226db881a9c40211960ade79 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 14 Oct 2024 11:58:06 +0200 Subject: [PATCH 33/45] Advance --- tests/test_app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index c40bf8ff..b7acd768 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -246,8 +246,6 @@ def test_render(main): console.execute(code_show_Shape) assert obj_tree_comp.CQ.childCount() == 1 - return - # check object rendering using show_object call with a name specified and # debug call editor.set_text(code_show_Workplane_named) @@ -257,6 +255,8 @@ def test_render(main): assert obj_tree_comp.CQ.child(0).text(0) == "test" assert "test" in log.toPlainText().splitlines()[-1] + return + # cq reloading check obj_tree_comp._toolbar_actions[0].triggered.emit() assert obj_tree_comp.CQ.childCount() == 0 From d3a7933d22318c5088e51ab3157cd1348af4d4f9 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 14 Oct 2024 14:14:24 +0200 Subject: [PATCH 34/45] Advance --- tests/test_app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index b7acd768..3b8ad53b 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -255,8 +255,6 @@ def test_render(main): assert obj_tree_comp.CQ.child(0).text(0) == "test" assert "test" in log.toPlainText().splitlines()[-1] - return - # cq reloading check obj_tree_comp._toolbar_actions[0].triggered.emit() assert obj_tree_comp.CQ.childCount() == 0 @@ -264,6 +262,8 @@ def test_render(main): editor.set_text(code_reload_issue) debugger._actions["Run"][0].triggered.emit() + return + qtbot.wait(100) assert obj_tree_comp.CQ.childCount() == 3 From cc801ce4f374294eed9a4a349de71dea6a6fd522 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 14 Oct 2024 14:29:00 +0200 Subject: [PATCH 35/45] Advance --- tests/test_app.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 3b8ad53b..58787114 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -262,8 +262,6 @@ def test_render(main): editor.set_text(code_reload_issue) debugger._actions["Run"][0].triggered.emit() - return - qtbot.wait(100) assert obj_tree_comp.CQ.childCount() == 3 From 0841d1c7e9a0fd69eb1ff949fdaa6ab6365efd69 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 14 Oct 2024 15:30:56 +0200 Subject: [PATCH 36/45] Advance --- tests/test_app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 58787114..75d21761 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1342,9 +1342,9 @@ def test_render_shape_list(main): """ -@pytest.mark.skipif( - sys.platform.startswith("linux"), reason="Segfault workaround for linux" -) +# @pytest.mark.skipif( +# sys.platform.startswith("linux"), reason="Segfault workaround for linux" +# ) def test_render_assy(main): qtbot, win = main From 7e8b000048b2b6e76fb3b0f0183192b4fa7de3c4 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 15 Oct 2024 07:53:12 +0200 Subject: [PATCH 37/45] Backtrack --- tests/test_app.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 75d21761..6133b949 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -195,9 +195,6 @@ def main_multi(qtbot, mocker): return qtbot, win -# @pytest.mark.skipif( -# sys.platform.startswith("linux"), reason="Segfault workaround for linux" -# ) def test_render(main): qtbot, win = main @@ -1342,9 +1339,6 @@ def test_render_shape_list(main): """ -# @pytest.mark.skipif( -# sys.platform.startswith("linux"), reason="Segfault workaround for linux" -# ) def test_render_assy(main): qtbot, win = main @@ -1364,6 +1358,8 @@ def test_render_assy(main): qtbot.wait(500) assert obj_tree_comp.CQ.childCount() == 1 + return + # test rendering via console console.execute("show(assy)") qtbot.wait(500) From f4b50573e9fcf27444ddb5c0cb0915519f9159c8 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 15 Oct 2024 08:07:12 +0200 Subject: [PATCH 38/45] Change to miniforge --- appveyor.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 619d2b9d..99169d14 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -13,11 +13,11 @@ environment: install: - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then sudo apt update; sudo apt -y --force-yes install libglu1-mesa xvfb libgl1-mesa-dri mesa-common-dev libglu1-mesa-dev; fi - - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then curl -fsSL -o miniconda.sh https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-Linux-x86_64.sh; fi - - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE == "macOS"* ]]; then curl -fsSL -o miniconda.sh https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-MacOSX-x86_64.sh; fi + - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then curl -fsSL -o miniconda.sh https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh; fi + - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE == "macOS"* ]]; then curl -fsSL -o miniconda.sh https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-MacOSX-x86_64.sh; fi - sh: bash miniconda.sh -b -p $HOME/miniconda - sh: source $HOME/miniconda/bin/activate - - cmd: curl -fsSL -o miniconda.exe https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-Windows-x86_64.exe + - cmd: curl -fsSL -o miniconda.exe https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Windows-x86_64.exe - cmd: miniconda.exe /S /InstallationType=JustMe /D=%MINICONDA_DIRNAME% - cmd: set "PATH=%MINICONDA_DIRNAME%;%MINICONDA_DIRNAME%\\Scripts;%PATH%" - cmd: activate From ec59f00cbbf8fbb320b8be677c535fb7d14d8008 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 16 Oct 2024 19:47:23 +0200 Subject: [PATCH 39/45] Enable faulthandler --- tests/test_app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_app.py b/tests/test_app.py index 6133b949..6004c41c 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,6 +1,9 @@ from path import Path import os, sys, asyncio +import faulthandler +faulthandler.enable() + if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) @@ -1358,7 +1361,6 @@ def test_render_assy(main): qtbot.wait(500) assert obj_tree_comp.CQ.childCount() == 1 - return # test rendering via console console.execute("show(assy)") From 9872e855a06a20a52dd05622eac4a3454b0698eb Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 16 Oct 2024 20:30:25 +0200 Subject: [PATCH 40/45] Debug --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 99169d14..105c7a81 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -52,5 +52,5 @@ on_failure: on_finish: # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) -# - sh: export APPVEYOR_SSH_BLOCK=true -# - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e - + - sh: export APPVEYOR_SSH_BLOCK=true + - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e - From 6e5f28ba42b589046aa32844d0b0ee7ae0ae5804 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 16 Oct 2024 21:31:00 +0200 Subject: [PATCH 41/45] Debug --- appveyor.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index 105c7a81..8d888a4e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -38,6 +38,8 @@ before_test: test_script: - sh: export PYTHONPATH=$(pwd) - cmd: set PYTHONPATH=%cd% + - sh: export APPVEYOR_SSH_BLOCK=true + - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e - - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then xvfb-run -s '-screen 0 1920x1080x24 +iglx' pytest -v --cov=cq_editor; fi - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE == "macOS"* ]]; then pytest -v --cov=cq_editor; fi - cmd: pytest -v --cov=cq_editor From ac496bccbda1326f183408004e932d215517a9de Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 16 Oct 2024 21:47:25 +0200 Subject: [PATCH 42/45] Add ssh key --- appveyor.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 8d888a4e..34a5965e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,6 +10,8 @@ environment: CODECOV_TOKEN: secure: ZggK9wgDeFdTp0pu0MEV+SY4i/i1Ls0xrEC2MxSQOQ0JQV+TkpzJJzI4au7L8TpD MINICONDA_DIRNAME: C:\FreshMiniconda + APPVEYOR_SSH_KEY: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsgcwomtoEzSX+10ZVzey/IvTGv7FnWlonMzUmIpyE1buvSObrHfltfPbBhlPOa1/qvbz1T9IHfQ6oni/XZXjWO+ZCS45NJgjmX+h2cgvSLwDyr9FyxQYbaOMbI6Ta76lHRV2effB2bZb/YIDP7ZOZoFY2jQMBAMZM2HFBJRqEmI8CIQJQOwiVWph1h8nxK/mKauQ9GrZ7v/mApObIRLmoD/r+4iLW896yMgchHxOvUOeNZeZlFzk1kvMc6ez1+bQlJkK/RiNUQYer4X/rPtIh6egYQ7e7B3CeV4GtegeujPW+KAHMn2cr4V883h4mKZgrfcpindgKx7+GPVkjooJ9 lo-re@DESKTOP-I8IOEA1 + APPVEYOR_SSH_BLOCK: true install: - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then sudo apt update; sudo apt -y --force-yes install libglu1-mesa xvfb libgl1-mesa-dri mesa-common-dev libglu1-mesa-dev; fi @@ -38,7 +40,6 @@ before_test: test_script: - sh: export PYTHONPATH=$(pwd) - cmd: set PYTHONPATH=%cd% - - sh: export APPVEYOR_SSH_BLOCK=true - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e - - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then xvfb-run -s '-screen 0 1920x1080x24 +iglx' pytest -v --cov=cq_editor; fi - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE == "macOS"* ]]; then pytest -v --cov=cq_editor; fi From ca3f94fbf786dba6fe1da43a2941ebce562e4e50 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 16 Oct 2024 22:12:21 +0200 Subject: [PATCH 43/45] Explicit block --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index 34a5965e..56f4da6f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -40,6 +40,7 @@ before_test: test_script: - sh: export PYTHONPATH=$(pwd) - cmd: set PYTHONPATH=%cd% + - sh: export APPVEYOR_SSH_BLOCK=true - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e - - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then xvfb-run -s '-screen 0 1920x1080x24 +iglx' pytest -v --cov=cq_editor; fi - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE == "macOS"* ]]; then pytest -v --cov=cq_editor; fi From ce20122aa0b6e2534fdc488522568eb59b696195 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 16 Oct 2024 22:23:50 +0200 Subject: [PATCH 44/45] Change key --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 56f4da6f..7baaa951 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,7 +10,7 @@ environment: CODECOV_TOKEN: secure: ZggK9wgDeFdTp0pu0MEV+SY4i/i1Ls0xrEC2MxSQOQ0JQV+TkpzJJzI4au7L8TpD MINICONDA_DIRNAME: C:\FreshMiniconda - APPVEYOR_SSH_KEY: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsgcwomtoEzSX+10ZVzey/IvTGv7FnWlonMzUmIpyE1buvSObrHfltfPbBhlPOa1/qvbz1T9IHfQ6oni/XZXjWO+ZCS45NJgjmX+h2cgvSLwDyr9FyxQYbaOMbI6Ta76lHRV2effB2bZb/YIDP7ZOZoFY2jQMBAMZM2HFBJRqEmI8CIQJQOwiVWph1h8nxK/mKauQ9GrZ7v/mApObIRLmoD/r+4iLW896yMgchHxOvUOeNZeZlFzk1kvMc6ez1+bQlJkK/RiNUQYer4X/rPtIh6egYQ7e7B3CeV4GtegeujPW+KAHMn2cr4V883h4mKZgrfcpindgKx7+GPVkjooJ9 lo-re@DESKTOP-I8IOEA1 + APPVEYOR_SSH_KEY: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD2h9wAkQCGPSiAzOjPiI9mAGMk9/eRPlrSUab/CxcP7vtpMt6T6i7q5WEbyzSkSuDNzfQ2nbxpZi1qOrz8mKpP11LRvEvl8oae2pNuhKrPge+QJ49CFIjiUj0SL+OJtCWb3IogIYhsF6a0GDkDmTof5+RGJpFssVC3L21vujqKuJzgrUFLrWq61zhBmB13nw1G2jb+wfxHKXHn33ZHqodzyjipJ70MHcrpeezmSELoAVYyX/rmMXshMX3RWBg7T7mi0j4KbhTjV0eRaXXRAmFnRybMZy0KMsdBXAepa7N8D9sH1nUX1z/2Uq8SUC/lYknR2/btoT3FcH5JcvujJCbn APPVEYOR_SSH_BLOCK: true install: From dfc552721dcbb0daffc8b7f5e630b76280eb1d93 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 16 Oct 2024 23:02:56 +0200 Subject: [PATCH 45/45] Try another key --- appveyor.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 7baaa951..51ceb5a6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,7 +10,7 @@ environment: CODECOV_TOKEN: secure: ZggK9wgDeFdTp0pu0MEV+SY4i/i1Ls0xrEC2MxSQOQ0JQV+TkpzJJzI4au7L8TpD MINICONDA_DIRNAME: C:\FreshMiniconda - APPVEYOR_SSH_KEY: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD2h9wAkQCGPSiAzOjPiI9mAGMk9/eRPlrSUab/CxcP7vtpMt6T6i7q5WEbyzSkSuDNzfQ2nbxpZi1qOrz8mKpP11LRvEvl8oae2pNuhKrPge+QJ49CFIjiUj0SL+OJtCWb3IogIYhsF6a0GDkDmTof5+RGJpFssVC3L21vujqKuJzgrUFLrWq61zhBmB13nw1G2jb+wfxHKXHn33ZHqodzyjipJ70MHcrpeezmSELoAVYyX/rmMXshMX3RWBg7T7mi0j4KbhTjV0eRaXXRAmFnRybMZy0KMsdBXAepa7N8D9sH1nUX1z/2Uq8SUC/lYknR2/btoT3FcH5JcvujJCbn + APPVEYOR_SSH_KEY: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCuakRkp1nBMgt2ctzpDtMXYe9fH7LzGStxhpS+QFBE29gaVI1AnXGJwIa5ODyPq7bY7xCmYAHTYyUdHkCOytcF4oezqLpMTH1bZkD7pLbnZjCIs4ISB3RiHhbLsupzxQwEULG9Hj2PMfZ05lH9TGbROKzh+2Po+ubP8Xjf8aIOhkEZunDIFnrEPxFk8i5AacZlGLGncD1Pe/K//qsJyXxcyU887kntV0FX4+WjpW85NDuLROaEbWYsq98bVOvbe3cSf1Rbi/Uflp0GGWy9FUJwzZ4S76BtObThoHKbLDVUGtgBttbaXAAgYDBGKMDncWg7UeFHwPSA44cUfBys7CuX rsa-key-20241016 APPVEYOR_SSH_BLOCK: true install: @@ -41,6 +41,7 @@ test_script: - sh: export PYTHONPATH=$(pwd) - cmd: set PYTHONPATH=%cd% - sh: export APPVEYOR_SSH_BLOCK=true + - sh: echo $APPVEYOR_SSH_KEY - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e - - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE != "macOS"* ]]; then xvfb-run -s '-screen 0 1920x1080x24 +iglx' pytest -v --cov=cq_editor; fi - sh: if [[ $APPVEYOR_BUILD_WORKER_IMAGE == "macOS"* ]]; then pytest -v --cov=cq_editor; fi