From e436f2951b5041d3f2ac991b5f516b95b751cd21 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Thu, 2 Jul 2026 07:35:16 -0400 Subject: [PATCH 1/4] Put the axis helpers on the topmost z-layer so there is no z-fighting --- cq_editor/widgets/object_tree.py | 2 ++ cq_editor/widgets/viewer.py | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/cq_editor/widgets/object_tree.py b/cq_editor/widgets/object_tree.py index f3dcb57b..5b1b19dc 100644 --- a/cq_editor/widgets/object_tree.py +++ b/cq_editor/widgets/object_tree.py @@ -13,6 +13,7 @@ from OCP.AIS import AIS_Line from OCP.Geom import Geom_Line from OCP.gp import gp_Dir, gp_Pnt, gp_Ax1 +from OCP.Graphic3d import Graphic3d_ZLayerId_Topmost from ..mixins import ComponentMixin from ..icons import icon @@ -243,6 +244,7 @@ def addLines(self): line_placement = Geom_Line(gp_Ax1(gp_Pnt(*origin), gp_Dir(*direction))) line = AIS_Line(line_placement) line.SetColor(to_occ_color(color)) + line.SetZLayer(Graphic3d_ZLayerId_Topmost) self.Helpers.addChild(ObjectTreeItem(name, ais=line)) diff --git a/cq_editor/widgets/viewer.py b/cq_editor/widgets/viewer.py index 1e9cf343..9364041f 100644 --- a/cq_editor/widgets/viewer.py +++ b/cq_editor/widgets/viewer.py @@ -8,6 +8,7 @@ Graphic3d_StereoMode, Graphic3d_NOM_JADE, Graphic3d_MaterialAspect, + Graphic3d_ZLayerId_Topmost, ) from OCP.AIS import AIS_Shaded, AIS_WireFrame, AIS_ColoredShape, AIS_Axis from OCP.Aspect import Aspect_GDM_Lines, Aspect_GT_Rectangular @@ -119,6 +120,7 @@ def __init__(self, parent=None): ) self.setup_default_drawer() + self.setup_helper_layer() self.updatePreferences() def setup_default_drawer(self): @@ -135,6 +137,16 @@ def setup_default_drawer(self): line_aspect.SetWidth(DEFAULT_EDGE_WIDTH) line_aspect.SetColor(DEFAULT_EDGE_COLOR) + def setup_helper_layer(self): + """ + Set up a render layer specifically for the axis helper lines. + """ + viewer = self.canvas.context.CurrentViewer() + settings = viewer.ZLayerSettings(Graphic3d_ZLayerId_Topmost) + settings.SetEnableDepthTest(False) # lines never depth-compare with the model + settings.SetEnableDepthWrite(False) # lines never write into the depth buffer + viewer.SetZLayerSettings(Graphic3d_ZLayerId_Topmost, settings) + def updatePreferences(self, *args): color1 = to_occ_color(self.preferences["Background color"]) From 78a2bed8e8327260b0ff06a8188c7b3117b977d6 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Thu, 2 Jul 2026 10:12:07 -0400 Subject: [PATCH 2/4] Route around test failures with CadQuery 2.8.0 and newest pytest --- cq_editor/widgets/editor.py | 4 ++++ tests/test_app.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/cq_editor/widgets/editor.py b/cq_editor/widgets/editor.py index 8efbe66d..0f45c748 100644 --- a/cq_editor/widgets/editor.py +++ b/cq_editor/widgets/editor.py @@ -485,6 +485,10 @@ def _file_changed(self): # neovim writes a file by removing it first so must re-add each time self._watch_paths() + # QFileSystemWater reports deletions as changes + if not Path(self._filename).exists(): + return + # Save the current cursor position and selection cursor = self.textCursor() cursor_position = cursor.position() diff --git a/tests/test_app.py b/tests/test_app.py index 8189fa4c..dcc3d82c 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -211,7 +211,7 @@ def test_render(main): log = win.components["log"] # enable CQ reloading - debugger.preferences["Reload CQ"] = True + debugger.preferences["Reload CQ"] = False # check that object was rendered assert obj_tree_comp.CQ.childCount() == 1 From c724c0230b3edf3354766775d6b0ead259b9e3fc Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Thu, 2 Jul 2026 15:48:59 -0400 Subject: [PATCH 3/4] Added automatically resizing axis helper lines --- cq_editor/main_window.py | 3 ++ cq_editor/widgets/object_tree.py | 54 +++++++++++++++++++++++++++----- cq_editor/widgets/viewer.py | 27 ++++++++++++++-- 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index cde9d2ac..bece4094 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -456,6 +456,9 @@ def prepare_actions(self): self.components["object_tree"].sigAISObjectsSelected.connect( self.components["viewer"].set_selected ) + self.components["object_tree"].sigHelpersResized.connect( + self.components["viewer"].redisplay + ) self.components["viewer"].sigObjectSelected.connect( self.components["object_tree"].handleGraphicalSelection diff --git a/cq_editor/widgets/object_tree.py b/cq_editor/widgets/object_tree.py index 5b1b19dc..5edf20fc 100644 --- a/cq_editor/widgets/object_tree.py +++ b/cq_editor/widgets/object_tree.py @@ -11,9 +11,11 @@ from pyqtgraph.parametertree import Parameter, ParameterTree from OCP.AIS import AIS_Line -from OCP.Geom import Geom_Line -from OCP.gp import gp_Dir, gp_Pnt, gp_Ax1 +from OCP.Geom import Geom_CartesianPoint +from OCP.gp import gp_Pnt from OCP.Graphic3d import Graphic3d_ZLayerId_Topmost +from OCP.Bnd import Bnd_Box +from OCP.BRepBndLib import BRepBndLib from ..mixins import ComponentMixin from ..icons import icon @@ -29,6 +31,9 @@ from ..utils import splitter, layout, get_save_filename +# Default size of the axis helper lines half-length +DEFAULT_AXIS_HALF_LEN = 100.0 + class TopTreeItem(QTreeWidgetItem): def __init__(self, *args, **kwargs): @@ -136,6 +141,7 @@ class ObjectTree(QWidget, ComponentMixin): sigAISObjectsSelected = pyqtSignal(list) sigItemChanged = pyqtSignal(QTreeWidgetItem, int) sigObjectPropertiesChanged = pyqtSignal() + sigHelpersResized = pyqtSignal(list) def __init__(self, parent): @@ -198,6 +204,37 @@ def __init__(self, parent): self.prepareLayout() + def _axis_points(self, direction, halfLen): + """Calculates the points needed to draw the axis helper lines""" + p1 = Geom_CartesianPoint(gp_Pnt(*(-halfLen * d for d in direction))) + p2 = Geom_CartesianPoint(gp_Pnt(*(halfLen * d for d in direction))) + + return p1, p2 + + def _rescale_helpers(self): + """Called to resize the axis helper lines to the model's bounding box.""" + + # Bounding box over all currently displayed CQ shapes + bbox = Bnd_Box() + for i in range(self.CQ.childCount()): + shape = self.CQ.child(i).shape + if shape is not None: + BRepBndLib.Add_s(shape.wrapped, bbox) + + if bbox.IsVoid(): + halfLen = DEFAULT_AXIS_HALF_LEN + else: + halfLen = 2.0 * bbox.CornerMin().Distance(bbox.CornerMax()) + + resized = [] + for item, direction in self._helper_dirs: + p1, p2 = self._axis_points(direction, halfLen) + item.ais.SetPoints(p1, p2) # update geometry in place + resized.append(item.ais) + + if resized: + self.sigHelpersResized.emit(resized) + def prepareMenu(self): self.tree.setContextMenuPolicy(Qt.CustomContextMenu) @@ -232,21 +269,22 @@ def toolbarActions(self): return self._toolbar_actions def addLines(self): - - origin = (0, 0, 0) ais_list = [] + self._helper_dirs = [] # (item, direction) pairs, for later rescaling 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) + p1, p2 = self._axis_points(direction, DEFAULT_AXIS_HALF_LEN) + line = AIS_Line(p1, p2) line.SetColor(to_occ_color(color)) line.SetZLayer(Graphic3d_ZLayerId_Topmost) - self.Helpers.addChild(ObjectTreeItem(name, ais=line)) + item = ObjectTreeItem(name, ais=line) + self.Helpers.addChild(item) + self._helper_dirs.append((item, direction)) ais_list.append(line) @@ -311,6 +349,8 @@ def addObjects(self, objects, clean=False, root=None): else: self.sigObjectsAdded[list].emit(ais_list) + self._rescale_helpers() + @pyqtSlot(object, str, object) def addObject(self, obj, name="", options=None): diff --git a/cq_editor/widgets/viewer.py b/cq_editor/widgets/viewer.py index 9364041f..02929276 100644 --- a/cq_editor/widgets/viewer.py +++ b/cq_editor/widgets/viewer.py @@ -10,7 +10,7 @@ Graphic3d_MaterialAspect, Graphic3d_ZLayerId_Topmost, ) -from OCP.AIS import AIS_Shaded, AIS_WireFrame, AIS_ColoredShape, AIS_Axis +from OCP.AIS import AIS_Shaded, AIS_WireFrame, AIS_ColoredShape, AIS_Axis, AIS_Line, AIS_ListOfInteractive from OCP.Aspect import Aspect_GDM_Lines, Aspect_GT_Rectangular from OCP.Quantity import ( Quantity_NOC_BLACK as BLACK, @@ -19,6 +19,7 @@ ) from OCP.Geom import Geom_Axis1Placement from OCP.gp import gp_Ax3, gp_Dir, gp_Pnt, gp_Ax1 +from OCP.Bnd import Bnd_Box from ..utils import layout, get_save_filename from ..mixins import ComponentMixin @@ -326,6 +327,13 @@ def update_item(self, item, col): else: ctx.Erase(item.ais, True) + @pyqtSlot(list) + def redisplay(self, ais_list): + ctx = self._get_context() + for ais in ais_list: + if ctx.IsDisplayed(ais): + ctx.Redisplay(ais, True) + @pyqtSlot(list) def remove_items(self, ais_items): @@ -339,8 +347,23 @@ def redraw(self): self._get_viewer().Redraw() def fit(self): + ctx = self._get_context() + view = self.canvas.view + + displayed = AIS_ListOfInteractive() + ctx.DisplayedObjects(displayed) - self.canvas.view.FitAll() + # Accumulate the bounding box for displayed objects + bbox = Bnd_Box() + for ais in displayed: + if not isinstance(ais, AIS_Line): + bbox.Add(ais.BoundingBox()) + + # If nothing but the axis helpers are visible, fit to default + if bbox.IsVoid(): + view.FitAll() + else: + view.FitAll(bbox, 0.01, True) def iso_view(self): From d2172223c24b5996f8e4233bbf42c8912d73f866 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Thu, 2 Jul 2026 15:59:17 -0400 Subject: [PATCH 4/4] Fix for shape types --- cq_editor/widgets/object_tree.py | 9 ++++----- cq_editor/widgets/viewer.py | 9 ++++++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/cq_editor/widgets/object_tree.py b/cq_editor/widgets/object_tree.py index 5edf20fc..f8d4bdb9 100644 --- a/cq_editor/widgets/object_tree.py +++ b/cq_editor/widgets/object_tree.py @@ -15,7 +15,6 @@ from OCP.gp import gp_Pnt from OCP.Graphic3d import Graphic3d_ZLayerId_Topmost from OCP.Bnd import Bnd_Box -from OCP.BRepBndLib import BRepBndLib from ..mixins import ComponentMixin from ..icons import icon @@ -30,10 +29,10 @@ from .viewer import DEFAULT_FACE_COLOR from ..utils import splitter, layout, get_save_filename - # Default size of the axis helper lines half-length DEFAULT_AXIS_HALF_LEN = 100.0 + class TopTreeItem(QTreeWidgetItem): def __init__(self, *args, **kwargs): @@ -217,9 +216,9 @@ def _rescale_helpers(self): # Bounding box over all currently displayed CQ shapes bbox = Bnd_Box() for i in range(self.CQ.childCount()): - shape = self.CQ.child(i).shape - if shape is not None: - BRepBndLib.Add_s(shape.wrapped, bbox) + ais = self.CQ.child(i).ais + if ais is not None: + bbox.Add(ais.BoundingBox()) if bbox.IsVoid(): halfLen = DEFAULT_AXIS_HALF_LEN diff --git a/cq_editor/widgets/viewer.py b/cq_editor/widgets/viewer.py index 02929276..1aeca99d 100644 --- a/cq_editor/widgets/viewer.py +++ b/cq_editor/widgets/viewer.py @@ -10,7 +10,14 @@ Graphic3d_MaterialAspect, Graphic3d_ZLayerId_Topmost, ) -from OCP.AIS import AIS_Shaded, AIS_WireFrame, AIS_ColoredShape, AIS_Axis, AIS_Line, AIS_ListOfInteractive +from OCP.AIS import ( + AIS_Shaded, + AIS_WireFrame, + AIS_ColoredShape, + AIS_Axis, + AIS_Line, + AIS_ListOfInteractive, +) from OCP.Aspect import Aspect_GDM_Lines, Aspect_GT_Rectangular from OCP.Quantity import ( Quantity_NOC_BLACK as BLACK,