#include "strokecanvasrenderer.hpp" #include "strokecanvasitem.hpp" #include #include #include #include #include namespace ZShell::internal { using Qt::StringLiterals::operator""_s; static const QString kStrokeHoleShader = u":/shaders/strokeholemask.frag.qsb"_s; namespace { struct HoleUniforms { QVector4D data1; // center.xy, halfSize.xy QVector4D data3; // x: radius, y: feather in logical px QVector4D data4; // x: hole enabled }; static HoleUniforms holeUniforms( const QRectF& hole, qreal radius, qreal devicePixelRatio) { const bool active = hole.isValid() && hole.width() > 0 && hole.height() > 0; if (!active) return {QVector4D(), QVector4D(), QVector4D()}; const QPointF center = hole.center(); const QPointF half(hole.width() * 0.5, hole.height() * 0.5); const qreal maxRadius = qMin(half.x(), half.y()); return { QVector4D( static_cast(center.x()), static_cast(center.y()), static_cast(half.x()), static_cast(half.y())), QVector4D( static_cast(qMin(radius, maxRadius)), static_cast(1.0 / devicePixelRatio), 0.0f, 0.0f), QVector4D(1.0, 0.0, 0.0, 0.0), }; } static void applyHole(QCanvasCustomBrush& brush, const HoleUniforms& u) { brush.setData1(u.data1); brush.setData3(u.data3); brush.setData4(u.data4); } }; // namespace void StrokeCanvasRenderer::updateBrushHole() { const HoleUniforms uniforms = holeUniforms(m_hole, m_holeRadius, m_devicePixelRatio); if (uniforms.data1 == m_lastHoleData1 && uniforms.data3 == m_lastHoleData3 && uniforms.data4 == m_lastHoleData4) return; m_lastHoleData1 = uniforms.data1; m_lastHoleData3 = uniforms.data3; m_lastHoleData4 = uniforms.data4; ++m_holeToken; for (auto& entry : m_brushes) { if (entry.holeToken == m_holeToken) continue; applyHole(entry.brush, uniforms); entry.holeToken = m_holeToken; } } QCanvasCustomBrush& StrokeCanvasRenderer::brushFor(const QColor& color) { const QRgb key = color.rgba(); if (auto it = m_brushes.find(key); it != m_brushes.end()) return it->brush; const HoleUniforms uniforms = holeUniforms(m_hole, m_holeRadius, m_devicePixelRatio); BrushEntry entry; entry.brush.setFragmentShader(kStrokeHoleShader); applyHole(entry.brush, uniforms); entry.brush.setData2( QVector4D(color.redF(), color.greenF(), color.blueF(), color.alphaF())); entry.holeToken = m_holeToken; return m_brushes.insert(key, entry)->brush; } void StrokeCanvasRenderer::drawStroke( QCanvasPainter* painter, const QVector& points, const QColor& color, qreal width, bool polyline) { if (points.isEmpty()) return; QCanvasCustomBrush& brush = brushFor(color); painter->setStrokeStyle(brush); painter->setFillStyle(brush); painter->setLineWidth(width); painter->setLineCap(QCanvasPainter::LineCap::Round); painter->setLineJoin(QCanvasPainter::LineJoin::Round); if (polyline) { painter->beginPath(); painter->moveTo(points[0]); for (int i = 1; i < points.size(); i++) painter->lineTo(points[i]); painter->stroke(); return; } if (points.size() == 1) { painter->beginPath(); painter->circle(points.front(), width * 0.5); painter->fill(); return; } auto catmullToBezier = [](const QPointF& p0, const QPointF& p1, const QPointF& p2, const QPointF& p3, qreal tension, QPointF& cp1, QPointF& cp2) { cp1 = p1 + (p2 - p0) * tension / 3.0; cp2 = p2 - (p3 - p1) * tension / 3.0; }; const qreal tension = 0.5; // increase toward 1.0 for tighter curves painter->beginPath(); painter->moveTo(points[0]); for (int i = 0; i < points.size() - 1; ++i) { const QPointF& p0 = points[qMax(i - 1, 0)]; const QPointF& p1 = points[i]; const QPointF& p2 = points[i + 1]; const QPointF& p3 = points[qMin(i + 2, points.size() - 1)]; QPointF cp1, cp2; catmullToBezier(p0, p1, p2, p3, tension, cp1, cp2); painter->bezierCurveTo(cp1, cp2, p2); } painter->stroke(); } void StrokeCanvasRenderer::drawHoverCursor( QCanvasPainter* painter, const QPointF& point, qreal penWidth, QColor penColor, bool isDrawing) { const qreal radius = penWidth * 0.5; if (isDrawing) { painter->setFillStyle(brushFor(penColor)); painter->beginPath(); painter->circle(point, radius); painter->fill(); } const qreal lineWidth = 1.5; const qreal crosshairSize = 6.0; const bool useDashes = penWidth > 10.0; auto drawOutline = [&](const QColor& color, qreal width) { painter->setStrokeStyle(brushFor(color)); painter->setLineWidth(width); painter->setLineCap(QCanvasPainter::LineCap::Round); if (useDashes) { const int dashCount = 12; const qreal fullAngle = 2.0 * M_PI; const qreal dashAngle = fullAngle / dashCount * 0.5; const qreal gapAngle = fullAngle / dashCount * 0.5; qreal angle = 0.0; for (int i = 0; i < dashCount; ++i) { painter->beginPath(); painter->arc( point, radius, angle, angle + dashAngle, QCanvasPainter::PathWinding::ClockWise, QCanvasPainter::PathConnection::NotConnected); painter->stroke(); angle += dashAngle + gapAngle; } } else { painter->beginPath(); painter->circle(point, radius); painter->stroke(); } }; auto drawCrosshair = [&](const QColor& color, qreal width) { painter->setStrokeStyle(brushFor(color)); painter->setLineWidth(width); painter->setLineCap(QCanvasPainter::LineCap::Round); const qreal inner = radius + 3.0; const qreal outer = radius + 3.0 + crosshairSize; painter->beginPath(); painter->moveTo(point + QPointF(0, -outer)); painter->lineTo(point + QPointF(0, -inner)); painter->moveTo(point + QPointF(0, outer)); painter->lineTo(point + QPointF(0, inner)); painter->moveTo(point + QPointF(-outer, 0)); painter->lineTo(point + QPointF(-inner, 0)); painter->moveTo(point + QPointF(outer, 0)); painter->lineTo(point + QPointF(inner, 0)); painter->stroke(); }; drawOutline(QColor(0, 0, 0, 160), lineWidth + 1.0); drawCrosshair(QColor(0, 0, 0, 160), lineWidth + 1.0); drawOutline(QColor(255, 255, 255, 220), lineWidth); drawCrosshair(QColor(255, 255, 255, 220), lineWidth); } void StrokeCanvasRenderer::synchronizeData(QCanvasPainterItem* item) { auto* canvas = static_cast(item); m_penColor = canvas->m_penColor; m_penWidth = canvas->m_penWidth; m_hole = canvas->m_hole; m_holeRadius = canvas->m_holeRadius; m_devicePixelRatio = canvas->window() ? canvas->window()->effectiveDevicePixelRatio() : 1.0; QSet canvasGroupIds; for (const auto& stroke : canvas->m_strokes) canvasGroupIds.insert(stroke.groupId); for (int i = static_cast(m_strokes.size()) - 1; i >= 0; --i) { if (!canvasGroupIds.contains(m_strokes[i].groupId)) { if (m_strokes[i].groupId >= 0) m_pendingGroupRemovals.append(m_strokes[i].groupId); m_strokes.remove(i); } } QSet rendererGroupIds; for (const auto& stroke : m_strokes) rendererGroupIds.insert(stroke.groupId); for (const auto& stroke : canvas->m_strokes) { if (!rendererGroupIds.contains(stroke.groupId)) m_strokes.append(stroke); } m_currentStroke = canvas->m_currentStroke; m_hoverVisible = canvas->m_hoverVisible; m_hoverPoint = canvas->m_hoverPoint; m_isDrawing = canvas->m_isDrawing; } void StrokeCanvasRenderer::paint(QCanvasPainter* painter) { updateBrushHole(); for (int id : m_pendingGroupRemovals) painter->removePathGroup(id); m_pendingGroupRemovals.clear(); painter->clearRect(0, 0, width(), height()); for (const auto& stroke : m_strokes) { painter->setStrokeStyle(brushFor(stroke.color)); painter->setFillStyle(brushFor(stroke.color)); painter->setLineWidth(stroke.width); painter->setLineCap(QCanvasPainter::LineCap::Round); painter->setLineJoin(QCanvasPainter::LineJoin::Round); if (stroke.isSinglePoint) { painter->fill(stroke.path, stroke.groupId); } else { painter->stroke(stroke.path, stroke.groupId); } } drawStroke( painter, m_currentStroke.points, m_currentStroke.color, m_currentStroke.width, m_currentStroke.isPolyline); if (m_hoverVisible) drawHoverCursor( painter, m_hoverPoint, m_penWidth, m_penColor, m_isDrawing); } }; // namespace ZShell::internal