From a49b954ce1998288abeaa26d9da40cd5ab1ebdda Mon Sep 17 00:00:00 2001 From: thetalkingdrum <254868117+thetalkingdrum@users.noreply.github.com> Date: Fri, 7 Aug 2026 23:10:43 +0200 Subject: [PATCH 1/4] Create bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 113 ++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..4f7d56a6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,113 @@ +name: Bug Report +description: Report unexpected color, artifact, or crash behavior in NegPy +title: "[Bug]: " +labels: ["bug", "needs-triage"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report a bug. Color and artifact issues in particular are + near-impossible to diagnose from a rendered screenshot alone — please fill in as much of + this as you can, and attach a source file if possible. + + - type: input + id: negpy-version + attributes: + label: NegPy version / build + description: Version number, or commit hash / branch if built from source + placeholder: "e.g. 0.48, or main @ a1b2c3d" + validations: + required: true + + - type: input + id: os + attributes: + label: OS + placeholder: "e.g. macOS 14 Sonoma, Windows 11, Ubuntu 24.04" + validations: + required: true + + - type: dropdown + id: scanner + attributes: + label: Scanner + description: Select your scanner, or choose "Other" and specify below + options: + - Pakon (F135/F235) + - Nikon Coolscan (specify model below) + - Noritsu + - Minolta Dimage Scan Elite + - Digital camera (macro/copy setup) + - Other / not sure + - N/A (not a scan-related issue) + validations: + required: true + + - type: input + id: scanner-detail + attributes: + label: Scanner model / details + description: If you picked Coolscan or "Other" above, specify exact model here. If a digital camera setup, list camera + lens. + placeholder: "e.g. Coolscan LS-9000, or Sony A7RIV + 90mm macro" + + - type: input + id: light-source + attributes: + label: Light source + description: What's illuminating the film — scanner's built-in lamp/LED, or an external light source for camera-scanning setups + placeholder: "e.g. stock Pakon halogen, Kaiser slimlite plano, narrowband LED (specify wavelengths if known)" + validations: + required: true + + - type: dropdown + id: correction-mode + attributes: + label: Correction mode + description: What was the Narrowband Trichrome Correction toggle set to when the issue occurred? + options: + - "On (narrowband trichrome correction enabled)" + - "Off" + - Not sure / didn't touch this setting + - N/A + validations: + required: true + + - type: input + id: input-profile + attributes: + label: Input ICC profile (if any) + description: Which profile was selected under Input ICC, if you set one explicitly + placeholder: "e.g. RGBScan.icc, none/default" + + - type: textarea + id: settings + attributes: + label: Relevant process-tab settings + description: Any non-default values for Dye Separation, Separation Damping, Dye Mute, ICE, or other sliders you changed + placeholder: "e.g. Dye Separation 1.50, Separation Damping 1.00" + + - type: textarea + id: description + attributes: + label: What happened + description: What did you expect, and what did you get instead? + validations: + required: true + + - type: textarea + id: source-file + attributes: + label: Source file + description: | + Please attach (or link to) the original scan file, not just the rendered/exported image — + the raw file is usually required to actually reproduce and diagnose color/artifact bugs. + If you can't share the full file, a small cropped region of the raw scan still helps enormously. + placeholder: "Link to file, or drag-and-drop attachment here" + validations: + required: true + + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: Before/after, or comparison images, are still useful alongside the source file From f4962928c410ea6ed882f6acbee64c51b2dd6cf6 Mon Sep 17 00:00:00 2001 From: thetalkingdrum <254868117+thetalkingdrum@users.noreply.github.com> Date: Fri, 7 Aug 2026 23:14:18 +0200 Subject: [PATCH 2/4] Create config.yml --- .github/ISSUE_TEMPLATE/.github/ISSUE_TEMPLATE/config.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/.github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..e41f303b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Questions / general discussion + url: https://github.com/marcinz606/NegPy/discussions + about: For questions, setup help, or general discussion, please use Discussions instead of an issue. From 38b3f00a7318e62babe25a6edf262e1d97b645b1 Mon Sep 17 00:00:00 2001 From: thetalkingdrum <254868117+thetalkingdrum@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:06:06 +0200 Subject: [PATCH 3/4] fix: move issue-template config.yml out of nested .github path --- .github/ISSUE_TEMPLATE/{.github/ISSUE_TEMPLATE => }/config.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/ISSUE_TEMPLATE/{.github/ISSUE_TEMPLATE => }/config.yml (100%) diff --git a/.github/ISSUE_TEMPLATE/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/.github/ISSUE_TEMPLATE/config.yml rename to .github/ISSUE_TEMPLATE/config.yml From 994d39870192074d70bb60e2bc7f1b5798621bbe Mon Sep 17 00:00:00 2001 From: thetalkingdrum <254868117+thetalkingdrum@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:30:22 +0200 Subject: [PATCH 4/4] fix(analysis): show only the density histogram during Peek Negative Peek Negative applies no curve, so the print histogram, curve line and zone strip were still showing the last real render's data as if it described the un-inverted scan on screen. Co-Authored-By: Claude Sonnet 5 --- docs/USER_GUIDE.md | 2 + negpy/desktop/view/sidebar/right_panel.py | 18 +- negpy/desktop/view/widgets/charts.py | 220 +++++++++++---------- tests/test_negative_peek_analysis_chart.py | 57 ++++++ tests/test_step_wedge_widget.py | 9 + 5 files changed, 202 insertions(+), 104 deletions(-) create mode 100644 tests/test_negative_peek_analysis_chart.py diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 16d21076..849c645c 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -250,6 +250,8 @@ Two different histograms share the chart. Behind the curve, rising from the bott Read them against each other. The density histogram tells you which part of the horizontal axis your negative occupies, and the curve tells you what happens to it. If the negative's data sits entirely on the flat toe, no amount of contrast pulls those shadows apart. Move the exposure so the data lands on the steep middle instead. +Peek Negative shows the scan before any curve ran, so the chart drops the curve, the output histogram and the zone strip and leaves only the density histogram. + #### LIN / LOG toggle Bottom-right of the chart. It switches the histogram's *height* axis (how many pixels), not the tone axis. **LIN** is literal, so a big flat sky dwarfs everything else. **LOG** compresses the tall peaks so the thin tails become visible, which is where the few hundred pixels of deep shadow or specular highlight live. Use LOG to hunt for clipping, LIN to judge where the bulk of the frame sits. NegPy remembers the choice between sessions. diff --git a/negpy/desktop/view/sidebar/right_panel.py b/negpy/desktop/view/sidebar/right_panel.py index 4b7b6db6..4c45143e 100644 --- a/negpy/desktop/view/sidebar/right_panel.py +++ b/negpy/desktop/view/sidebar/right_panel.py @@ -345,6 +345,19 @@ def _update_histograms(self, metrics: Dict[str, Any]) -> None: zone_warnings, ) + self.curve_widget.set_density_histogram(metrics.get("histogram_density")) + + # Peek Negative applied no curve, so the print histogram, curve and zone strip + # would describe a print that was never made. Density is unaffected: it reads + # the scan itself, before the curve. + if self.controller.state.negative_peek: + self.curve_widget.set_output_histogram(None) + self.curve_widget.set_show_print(False) + self._clip_fracs = (None, None) + self.zone_strip.setVisible(False) + return + self.curve_widget.set_show_print(True) + source = metrics.get("histogram_raw") if source is None: source = metrics.get("analysis_buffer") @@ -358,7 +371,6 @@ def _update_histograms(self, metrics: Dict[str, Any]) -> None: bins = output_histogram(source) self.curve_widget.set_output_histogram(bins) - self.curve_widget.set_density_histogram(metrics.get("histogram_density")) self._clip_fracs = output_clip_fractions(bins) if bins is not None else (None, None) # A flat log master has no print zones, so hide them rather than mislead. @@ -399,6 +411,10 @@ def _update_analysis(self) -> None: self.curve_widget.update_curve(flat_cfg, slope=gain, pivot=lift, flat=True) # A flat master has no print curve, so there is no wedge to print through it. self.step_wedge.setVisible(False) + elif self.controller.state.negative_peek: + # No curve ran, so there is nothing to plot or to print the wedge through; + # the chart itself already suppressed the curve in _update_histograms. + self.step_wedge.setVisible(False) else: # Mirror PhotometricProcessor, so the plotted curve matches the render under the Auto # Grade, Auto Density and Cast Removal toggles. diff --git a/negpy/desktop/view/widgets/charts.py b/negpy/desktop/view/widgets/charts.py index 957da0ee..c0c78d46 100644 --- a/negpy/desktop/view/widgets/charts.py +++ b/negpy/desktop/view/widgets/charts.py @@ -47,6 +47,9 @@ def __init__(self, parent=None): # plt_x = 1 - val_center(i). self._density_bins: np.ndarray | None = None self._output_counts: np.ndarray | None = None # (4, 256) [R, G, B, L] + # False during Peek Negative: no curve was applied, so the curve, print histogram + # and zone traces would describe a print that was never made. + self._show_print: bool = True self._log_scale: bool = False self._clip_low: dict[int, bool] = {} self._clip_high: dict[int, bool] = {} @@ -80,6 +83,14 @@ def set_log_scale(self, enabled: bool) -> None: self._log_scale = enabled self.update() + def set_show_print(self, visible: bool) -> None: + """Hide the curve, print histogram, zone traces and clip marks; density stays.""" + visible = bool(visible) + if visible == self._show_print: + return + self._show_print = visible + self.update() + def set_marker(self, rgb: tuple | None) -> None: """Marks the hovered pixel's R/G/B values (0-255) with vertical lines; None clears.""" if rgb == self._marker: @@ -351,111 +362,114 @@ def paintEvent(self, event) -> None: # Histograms under everything else so the curve/zone tints stay readable. self._draw_density_histogram(painter, w, h) - self._draw_output_histogram(painter, w, h) - - # Build the main curve path (reused for fill and line) - curve_path = QPainterPath() - curve_path.moveTo(self._wx(self._curve_pts[0][0], w), self._wy(self._curve_pts[0][1], h)) - for px, py in self._curve_pts[1:]: - curve_path.lineTo(self._wx(px, w), self._wy(py, h)) - - # P4: toe zone shading. Warm amber, right side, dense silver = shadows - self._draw_zone_shading(painter, w, h, self._toe_mask, self._toe_strength, QColor(255, 140, 50)) - - # P4: shoulder zone shading. Cool blue, left side, thin silver = highlights - self._draw_zone_shading(painter, w, h, self._shoulder_mask, self._shoulder_strength, QColor(60, 130, 255)) - - # Glow the dragged slider's zone at fixed strength, so it reads at zero and negative values - if self._active_param in ("toe", "toe_width"): - self._draw_zone_shading(painter, w, h, self._toe_mask, 0.5, QColor(255, 140, 50)) - elif self._active_param in ("shoulder", "shoulder_width"): - self._draw_zone_shading(painter, w, h, self._shoulder_mask, 0.5, QColor(60, 130, 255)) - # P2: Gradient luminance fill under the curve - fill_path = QPainterPath(curve_path) - bot = self._wy(self._Y_MIN, h) - fill_path.lineTo(self._wx(self._curve_pts[-1][0], w), bot) - fill_path.lineTo(self._wx(self._curve_pts[0][0], w), bot) - fill_path.closeSubpath() - - gradient = QLinearGradient(0.0, 0.0, float(w), 0.0) - gradient.setColorAt(0.0, QColor(0, 0, 0, 55)) - gradient.setColorAt(1.0, QColor(255, 255, 255, 55)) - painter.setBrush(QBrush(gradient)) - painter.setPen(Qt.PenStyle.NoPen) - painter.drawPath(fill_path) - - # P5: zone tick marks along the bottom (Adams Zone I to IX) - painter.setPen(QPen(QColor("#3A3A3A"), 1)) - for i in range(1, 10): - zx = int(self._wx(i * 0.1, w)) - painter.drawLine(zx, h - 5, zx, h - 1) - - self._draw_mask_band(painter, w, h) - - # Pre-drag ghost curve - if self._ghost_pts: - ghost_path = QPainterPath() - ghost_path.moveTo(self._wx(self._ghost_pts[0][0], w), self._wy(self._ghost_pts[0][1], h)) - for px, py in self._ghost_pts[1:]: - ghost_path.lineTo(self._wx(px, w), self._wy(py, h)) - painter.setBrush(Qt.BrushStyle.NoBrush) - painter.setPen(QPen(QColor(200, 200, 200, 90), 1, Qt.PenStyle.DashLine)) - painter.drawPath(ghost_path) - if self._ghost_pivot: - painter.setBrush(QBrush(QColor(200, 200, 200, 90))) - painter.setPen(Qt.PenStyle.NoPen) - painter.drawEllipse(QPointF(self._wx(self._ghost_pivot[0], w), self._wy(self._ghost_pivot[1], h)), 2.5, 2.5) - - # Curve line on top; per-channel traces replace the white line when present. - painter.setBrush(Qt.BrushStyle.NoBrush) - if self._channel_curves: - for color, pts in self._channel_curves: - ch_path = QPainterPath() - ch_path.moveTo(self._wx(pts[0][0], w), self._wy(pts[0][1], h)) - for px, py in pts[1:]: - ch_path.lineTo(self._wx(px, w), self._wy(py, h)) - painter.setPen(QPen(color, 1.5)) - painter.drawPath(ch_path) - else: - painter.setPen(QPen(QColor("#FFFFFF"), 1.5)) - painter.drawPath(curve_path) - - # P3: Pivot crosshairs + dot - if self._pivot_pt: - wpx = self._wx(self._pivot_pt[0], w) - wpy = self._wy(self._pivot_pt[1], h) - - # Grade and Density act about the pivot, so brighten its crosshair while dragged. - cross_alpha = 110 if self._active_param in ("grade", "density") else 45 - painter.setPen(QPen(QColor(200, 200, 200, cross_alpha), 1, Qt.PenStyle.DotLine)) - painter.drawLine(int(wpx), 0, int(wpx), h) - painter.drawLine(0, int(wpy), w, int(wpy)) - - painter.setBrush(QBrush(QColor("#FFFFFF"))) - painter.setPen(QPen(QColor("#050505"), 1)) - painter.drawEllipse(QPointF(wpx, wpy), 3.5, 3.5) - - # Spot-densitometer tracking dot - if self._tracking_val is not None: - plt_x = float(np.clip(1.0 - self._tracking_val, self._X_MIN, self._X_MAX)) - idx = round((plt_x - self._X_MIN) / (self._X_MAX - self._X_MIN) * (len(self._curve_pts) - 1)) - idx = max(0, min(len(self._curve_pts) - 1, idx)) - tx, ty = self._curve_pts[idx] + if self._show_print: + self._draw_output_histogram(painter, w, h) + + # Build the main curve path (reused for fill and line) + curve_path = QPainterPath() + curve_path.moveTo(self._wx(self._curve_pts[0][0], w), self._wy(self._curve_pts[0][1], h)) + for px, py in self._curve_pts[1:]: + curve_path.lineTo(self._wx(px, w), self._wy(py, h)) + + # P4: toe zone shading. Warm amber, right side, dense silver = shadows + self._draw_zone_shading(painter, w, h, self._toe_mask, self._toe_strength, QColor(255, 140, 50)) + + # P4: shoulder zone shading. Cool blue, left side, thin silver = highlights + self._draw_zone_shading(painter, w, h, self._shoulder_mask, self._shoulder_strength, QColor(60, 130, 255)) + + # Glow the dragged slider's zone at fixed strength, so it reads at zero and negative values + if self._active_param in ("toe", "toe_width"): + self._draw_zone_shading(painter, w, h, self._toe_mask, 0.5, QColor(255, 140, 50)) + elif self._active_param in ("shoulder", "shoulder_width"): + self._draw_zone_shading(painter, w, h, self._shoulder_mask, 0.5, QColor(60, 130, 255)) + + # P2: Gradient luminance fill under the curve + fill_path = QPainterPath(curve_path) + bot = self._wy(self._Y_MIN, h) + fill_path.lineTo(self._wx(self._curve_pts[-1][0], w), bot) + fill_path.lineTo(self._wx(self._curve_pts[0][0], w), bot) + fill_path.closeSubpath() + + gradient = QLinearGradient(0.0, 0.0, float(w), 0.0) + gradient.setColorAt(0.0, QColor(0, 0, 0, 55)) + gradient.setColorAt(1.0, QColor(255, 255, 255, 55)) + painter.setBrush(QBrush(gradient)) + painter.setPen(Qt.PenStyle.NoPen) + painter.drawPath(fill_path) + + # P5: zone tick marks along the bottom (Adams Zone I to IX) + painter.setPen(QPen(QColor("#3A3A3A"), 1)) + for i in range(1, 10): + zx = int(self._wx(i * 0.1, w)) + painter.drawLine(zx, h - 5, zx, h - 1) + + self._draw_mask_band(painter, w, h) + + # Pre-drag ghost curve + if self._ghost_pts: + ghost_path = QPainterPath() + ghost_path.moveTo(self._wx(self._ghost_pts[0][0], w), self._wy(self._ghost_pts[0][1], h)) + for px, py in self._ghost_pts[1:]: + ghost_path.lineTo(self._wx(px, w), self._wy(py, h)) + painter.setBrush(Qt.BrushStyle.NoBrush) + painter.setPen(QPen(QColor(200, 200, 200, 90), 1, Qt.PenStyle.DashLine)) + painter.drawPath(ghost_path) + if self._ghost_pivot: + painter.setBrush(QBrush(QColor(200, 200, 200, 90))) + painter.setPen(Qt.PenStyle.NoPen) + painter.drawEllipse(QPointF(self._wx(self._ghost_pivot[0], w), self._wy(self._ghost_pivot[1], h)), 2.5, 2.5) + + # Curve line on top; per-channel traces replace the white line when present. painter.setBrush(Qt.BrushStyle.NoBrush) - painter.setPen(QPen(QColor(255, 255, 255, 220), 1.5)) - painter.drawEllipse(QPointF(self._wx(tx, w), self._wy(ty, h)), 4.0, 4.0) - - # Hovered-pixel marker lines - if self._marker is not None: - for value, color_hex in zip(self._marker, (THEME.channel_red, THEME.channel_green, THEME.channel_blue)): - c = QColor(color_hex) - c.setAlpha(220) - painter.setPen(QPen(c, 1, Qt.PenStyle.DashLine)) - mx = int(value / 255 * (w - 1)) - painter.drawLine(mx, 0, mx, h) - - self._draw_clip_indicators(painter, w, h) + if self._channel_curves: + for color, pts in self._channel_curves: + ch_path = QPainterPath() + ch_path.moveTo(self._wx(pts[0][0], w), self._wy(pts[0][1], h)) + for px, py in pts[1:]: + ch_path.lineTo(self._wx(px, w), self._wy(py, h)) + painter.setPen(QPen(color, 1.5)) + painter.drawPath(ch_path) + else: + painter.setPen(QPen(QColor("#FFFFFF"), 1.5)) + painter.drawPath(curve_path) + + # P3: Pivot crosshairs + dot + if self._pivot_pt: + wpx = self._wx(self._pivot_pt[0], w) + wpy = self._wy(self._pivot_pt[1], h) + + # Grade and Density act about the pivot, so brighten its crosshair while dragged. + cross_alpha = 110 if self._active_param in ("grade", "density") else 45 + painter.setPen(QPen(QColor(200, 200, 200, cross_alpha), 1, Qt.PenStyle.DotLine)) + painter.drawLine(int(wpx), 0, int(wpx), h) + painter.drawLine(0, int(wpy), w, int(wpy)) + + painter.setBrush(QBrush(QColor("#FFFFFF"))) + painter.setPen(QPen(QColor("#050505"), 1)) + painter.drawEllipse(QPointF(wpx, wpy), 3.5, 3.5) + + # Spot-densitometer tracking dot + if self._tracking_val is not None: + plt_x = float(np.clip(1.0 - self._tracking_val, self._X_MIN, self._X_MAX)) + idx = round((plt_x - self._X_MIN) / (self._X_MAX - self._X_MIN) * (len(self._curve_pts) - 1)) + idx = max(0, min(len(self._curve_pts) - 1, idx)) + tx, ty = self._curve_pts[idx] + painter.setBrush(Qt.BrushStyle.NoBrush) + painter.setPen(QPen(QColor(255, 255, 255, 220), 1.5)) + painter.drawEllipse(QPointF(self._wx(tx, w), self._wy(ty, h)), 4.0, 4.0) + + # Hovered-pixel marker lines + if self._marker is not None: + for value, color_hex in zip(self._marker, (THEME.channel_red, THEME.channel_green, THEME.channel_blue)): + c = QColor(color_hex) + c.setAlpha(220) + painter.setPen(QPen(c, 1, Qt.PenStyle.DashLine)) + mx = int(value / 255 * (w - 1)) + painter.drawLine(mx, 0, mx, h) + + self._draw_clip_indicators(painter, w, h) + self._draw_scale_toggle(painter, w, h) def _draw_mask_band(self, painter: QPainter, w: int, h: int) -> None: diff --git a/tests/test_negative_peek_analysis_chart.py b/tests/test_negative_peek_analysis_chart.py new file mode 100644 index 00000000..af3009bd --- /dev/null +++ b/tests/test_negative_peek_analysis_chart.py @@ -0,0 +1,57 @@ +"""Peek Negative applies no curve, so the Analysis chart's print histogram, curve and zone +strip would describe a print that was never made; only the pre-curve density histogram +still applies.""" + +from unittest.mock import MagicMock + +from negpy.desktop.view.sidebar.right_panel import RightPanel +from negpy.desktop.view.widgets.charts import PhotometricCurveWidget + + +def _panel_stub(negative_peek: bool) -> MagicMock: + panel = MagicMock() + panel.controller.state.negative_peek = negative_peek + panel.controller.state.flat_peek = False + panel.curve_widget = MagicMock() + panel.zone_strip = MagicMock() + return panel + + +def test_a_negative_peek_shows_only_the_density_histogram() -> None: + panel = _panel_stub(negative_peek=True) + metrics = {"histogram_density": [1.0, 2.0], "histogram_raw": object()} + + RightPanel._update_histograms(panel, metrics) + + panel.curve_widget.set_density_histogram.assert_called_once_with(metrics["histogram_density"]) + panel.curve_widget.set_output_histogram.assert_called_once_with(None) + panel.curve_widget.set_show_print.assert_called_once_with(False) + panel.zone_strip.setVisible.assert_called_once_with(False) + assert panel._clip_fracs == (None, None) + + +def test_a_normal_render_still_shows_the_print(monkeypatch) -> None: + import numpy as np + + panel = _panel_stub(negative_peek=False) + metrics = {"histogram_density": None, "histogram_raw": np.zeros((4, 4, 3), dtype=np.float32)} + + RightPanel._update_histograms(panel, metrics) + + panel.curve_widget.set_show_print.assert_called_once_with(True) + panel.curve_widget.set_output_histogram.assert_called_once() + assert panel.curve_widget.set_output_histogram.call_args[0][0] is not None + + +def test_show_print_false_leaves_the_density_histogram_paintable() -> None: + """set_show_print only gates the print-derived traces; the widget must not blank + entirely just because there is no curve to draw.""" + widget = PhotometricCurveWidget() + widget.resize(200, 120) + widget._curve_pts = [(0.0, 0.0), (1.0, 1.0)] + widget.set_density_histogram([1.0, 2.0, 3.0]) + widget.set_show_print(False) + + assert widget._show_print is False + # paintEvent must not raise with print traces suppressed but density data present. + widget.grab() diff --git a/tests/test_step_wedge_widget.py b/tests/test_step_wedge_widget.py index 6617d355..8a0afc3f 100644 --- a/tests/test_step_wedge_widget.py +++ b/tests/test_step_wedge_widget.py @@ -117,6 +117,7 @@ def _panel_stub(flat_peek: bool) -> MagicMock: panel = MagicMock() panel._clip_fracs = (None, None) panel.controller.state.flat_peek = flat_peek + panel.controller.state.negative_peek = False panel.controller.session.state.config = WorkspaceConfig() panel.controller.session.state.last_metrics = {} panel.controller.display_transform_params.return_value = ("sRGB", None, None) @@ -132,6 +133,14 @@ def test_a_flat_peek_hides_the_wedge() -> None: assert not panel.step_wedge.isVisibleTo(panel.step_wedge) +def test_a_negative_peek_hides_the_wedge() -> None: + """No curve ran, so there is nothing to print the wedge through.""" + panel = _panel_stub(flat_peek=False) + panel.controller.state.negative_peek = True + RightPanel._update_analysis(panel) + assert not panel.step_wedge.isVisibleTo(panel.step_wedge) + + def test_the_wedge_is_fed_the_curve_the_chart_just_plotted() -> None: """One solve, two consumers: the chart and the wedge must not resolve slope and pivot separately, or Auto Grade could move one and not the other."""