12.2a: folders duplicate with their contents, folder opacity, format version 8, View > Snap (Compositor 1.1.5–1.1.6)

This commit is contained in:
雷电芽衣 committed 2026-09-26 18:17:14 -04:00
1 parent ee34c338d4
commit f1b2c65c1e
52 files changed
+909 -239

No files matched your search

+1 -1
View File
@@ -81,7 +81,7 @@ void CanvasView::dragCrop(QPointF point, Qt::KeyboardModifiers modifiers)
const std::optional<double> ratio = m_session.cropRatio();
const CropDrag &drag = m_cropDrag.value();
QRectF next = drag.updated(pixel, ratio, symmetric);
if (!modifiers.testFlag(Qt::ControlModifier))
if (m_session.snappingEnabled() && !modifiers.testFlag(Qt::ControlModifier))
next = m_cropSnap.value().apply(next, drag, pixel, ratio, symmetric);
if (CropGeometry::valid(next))
m_session.setCropRect(next);
+32
View File
@@ -0,0 +1,32 @@
#include "Rendering/EditorCanvas.h"
#include <QPainterPath>
#include <cmath>
// One-screen-pixel lines on document pixel boundaries, over the image only.
void CanvasView::drawPixelGrid(const QRectF &view, const CanvasDocument &document, QPainter &context) const
{
const CanvasViewport &viewport = m_session.viewport;
const double points = viewport.pointsPerPixel();
const QRectF canvas(viewport.viewPoint(QPointF(0, 0), document.size()), QSizeF(document.width * points, document.height * points));
const QRectF area = view.intersected(canvas);
if (area.isEmpty())
return;
const QPointF first = viewport.documentPoint(area.topLeft(), document.size());
const QPointF last = viewport.documentPoint(area.bottomRight(), document.size());
const double hairline = 1 / viewport.backingScale;
// Crossings fill once, as CoreGraphics fills the union.
QPainterPath path;
path.setFillRule(Qt::WindingFill);
for (int column = int(std::ceil(first.x())); column <= int(std::floor(last.x())); ++column) {
const double x = viewport.viewPoint(QPointF(column, 0), document.size()).x();
path.addRect(QRectF(x - hairline / 2, area.top(), hairline, area.height()));
}
for (int row = int(std::ceil(first.y())); row <= int(std::floor(last.y())); ++row) {
const double y = viewport.viewPoint(QPointF(0, row), document.size()).y();
path.addRect(QRectF(area.left(), y - hairline / 2, area.width(), hairline));
}
context.save();
context.setRenderHint(QPainter::Antialiasing, true);
context.fillPath(path, QColor::fromRgbF(0.55, 0.55, 0.55, 0.45));
context.restore();
}
+9 -34
View File
@@ -81,6 +81,8 @@ CanvasView::DisplayState CanvasView::displayState() const
[](const ImageLayer &layer) { return layer.maskSourceID.has_value(); });
const std::vector<ImageLayer> rendered = liveMasks ? std::vector<ImageLayer>() : document->renderLayers();
const QSet<QUuid> visible = document->effectiveVisibleIDs();
// A folder's opacity reaches the canvas through its layers.
const QHash<QUuid, double> opacities = document->effectiveOpacities();
for (const ImageLayer &layer : liveMasks ? document->layers : rendered) {
// A blank layer counts while a draft sits on it.
if (!layer.asset && !layer.adjustment && !(m_session.shapeDraft() && layer.id == m_session.activeLayerID()))
@@ -88,7 +90,7 @@ CanvasView::DisplayState CanvasView::displayState() const
state.layers.push_back({.id = layer.id, .transform = m_session.displayedTransform(layer),
.imageID = layer.asset ? std::optional(layer.asset->identity()) : std::nullopt,
.maskID = enabledMaskID(layer.mask), .maskSourceID = layer.maskSourceID, .parentID = layer.parentID,
.visible = visible.contains(layer.id), .opacity = layer.opacity,
.visible = visible.contains(layer.id), .opacity = opacities.value(layer.id),
.blendMode = m_session.displayedBlendMode(layer), .maskPlacement = m_session.displayedMaskPlacement(layer),
.adjustment = layer.adjustment, .effects = layer.effects});
}
@@ -308,6 +310,8 @@ void CanvasView::drawLayers(const CanvasDocument &document, double scale, const
if (!byID.contains(id) || (m_session.textDraft() && m_session.textDraft().value().layerID == id))
return;
const ImageLayer &layer = byID.at(id);
// Its folders dim it with everything inside them.
const double opacity = layer.effectiveOpacity(byID);
// A stroke or pixel move stands in for the pixels.
const PixelMove *move = m_session.pixelMove();
const std::optional<GradientEdit> &gradient = m_session.gradientEdit();
@@ -323,7 +327,7 @@ void CanvasView::drawLayers(const CanvasDocument &document, double scale, const
const std::optional<QImage> mask = layer.mask ? layer.mask->clipImage(layer.maskTransform(), canvas, warp->width, warp->height, 2048)
: std::nullopt;
LayerRenderer::draw(warp->image(), canvas, center(canvas.center()), target,
{.scale = scale, .opacity = layer.opacity, .blendMode = m_session.displayedBlendMode(layer),
{.scale = scale, .opacity = opacity, .blendMode = m_session.displayedBlendMode(layer),
.mask = mask.value_or(QImage()), .clip = clip});
return;
}
@@ -344,12 +348,12 @@ void CanvasView::drawLayers(const CanvasDocument &document, double scale, const
const QSize size = owner.asset ? owner.asset->size() : QSize(int(std::round(base.size.width())), int(std::round(base.size.height())));
mask = layer.mask->clipImage(placement, base, size.width(), size.height(), m_session.transformEdit() ? std::min(2048.0, steady) : steady);
}
const LayerRenderer::Options options{.scale = scale, .opacity = layer.opacity, .blendMode = m_session.displayedBlendMode(layer),
const LayerRenderer::Options options{.scale = scale, .opacity = opacity, .blendMode = m_session.displayedBlendMode(layer),
.mask = mask.value_or(QImage()), .clip = clip};
// Swift asks twice; once, with what shows, is one request.
const std::optional<EffectsPreviewCache::Result> effects =
stroke ? std::nullopt : m_session.effectsPreviews.preview(layer, mask, transform, placement, repaint);
const LayerRenderer::Options bare{.scale = scale, .opacity = layer.opacity, .blendMode = options.blendMode, .clip = clip};
const LayerRenderer::Options bare{.scale = scale, .opacity = opacity, .blendMode = options.blendMode, .clip = clip};
// A pending distortion warps the effects along with the layer.
if (const auto warped = effects ? m_session.distortedEffects(layer, effects->image, effects->inset) : std::nullopt) {
LayerRenderer::draw(warped->image, warped->transform, center(warped->transform.center()), target, bare);
@@ -409,7 +413,7 @@ void CanvasView::drawLayers(const CanvasDocument &document, double scale, const
};
LiveMaskRenderer live([&](QUuid id) { return byID.contains(id) ? byID.at(id).maskSourceID : std::nullopt; }, drawOwnWithDraft);
live.adjustment = [&](QUuid id) { return byID.contains(id) ? byID.at(id).adjustment : std::nullopt; };
live.adjustmentOpacity = [&](QUuid id) { return byID.at(id).opacity; };
live.adjustmentOpacity = [&](QUuid id) { return byID.at(id).effectiveOpacity(byID); };
live.adjustmentClip = [&](QUuid id, const QPainter &painter, QImage &coverage) {
const ImageLayer &layer = byID.at(id);
if (!layer.mask || !layer.mask->isEnabled)
@@ -468,32 +472,3 @@ void CanvasView::drawDocumentPixels(const QRectF &view, const QRectF &pixels, co
context.drawImage(target, raster);
context.restore();
}
// One-screen-pixel lines on document pixel boundaries, over the image only.
void CanvasView::drawPixelGrid(const QRectF &view, const CanvasDocument &document, QPainter &context) const
{
const CanvasViewport &viewport = m_session.viewport;
const double points = viewport.pointsPerPixel();
const QRectF canvas(viewport.viewPoint(QPointF(0, 0), document.size()), QSizeF(document.width * points, document.height * points));
const QRectF area = view.intersected(canvas);
if (area.isEmpty())
return;
const QPointF first = viewport.documentPoint(area.topLeft(), document.size());
const QPointF last = viewport.documentPoint(area.bottomRight(), document.size());
const double hairline = 1 / viewport.backingScale;
// Crossings fill once, as CoreGraphics fills the union.
QPainterPath path;
path.setFillRule(Qt::WindingFill);
for (int column = int(std::ceil(first.x())); column <= int(std::floor(last.x())); ++column) {
const double x = viewport.viewPoint(QPointF(column, 0), document.size()).x();
path.addRect(QRectF(x - hairline / 2, area.top(), hairline, area.height()));
}
for (int row = int(std::ceil(first.y())); row <= int(std::floor(last.y())); ++row) {
const double y = viewport.viewPoint(QPointF(0, row), document.size()).y();
path.addRect(QRectF(area.left(), y - hairline / 2, area.width(), hairline));
}
context.save();
context.setRenderHint(QPainter::Antialiasing, true);
context.fillPath(path, QColor::fromRgbF(0.55, 0.55, 0.55, 0.45));
context.restore();
}
+1 -2
View File
@@ -348,8 +348,7 @@ void TransformOverlay::drawTransformHandles(QPainter &context, const QPalette &p
context.save();
context.setRenderHint(QPainter::Antialiasing, true);
context.setBrush(Qt::NoBrush);
context.setPen(QPen(QColor(0, 0, 0, 179), 3));
context.drawPath(path);
// The accent alone: a dark line read as a halo.
context.setPen(QPen(accent, 1));
context.drawPath(path);
context.setBrush(Qt::white);