summaryrefslogtreecommitdiffstats
path: root/src/display/drawing-shape.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/display/drawing-shape.cpp')
-rw-r--r--src/display/drawing-shape.cpp444
1 files changed, 444 insertions, 0 deletions
diff --git a/src/display/drawing-shape.cpp b/src/display/drawing-shape.cpp
new file mode 100644
index 0000000..d99e99f
--- /dev/null
+++ b/src/display/drawing-shape.cpp
@@ -0,0 +1,444 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Shape (styled path) belonging to an SVG drawing.
+ *//*
+ * Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2011 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm.h>
+#include <2geom/curves.h>
+#include <2geom/pathvector.h>
+#include <2geom/path-sink.h>
+#include <2geom/svg-path-parser.h>
+
+#include "dither-lock.h"
+#include "style.h"
+
+#include "curve.h"
+#include "drawing.h"
+#include "drawing-context.h"
+#include "drawing-shape.h"
+#include "control/canvas-item-drawing.h"
+
+#include "helper/geom.h"
+
+#include "ui/widget/canvas.h" // Canvas area
+
+namespace Inkscape {
+
+DrawingShape::DrawingShape(Drawing &drawing)
+ : DrawingItem(drawing)
+ , style_vector_effect_stroke(false)
+ , style_stroke_extensions_hairline(false)
+ , style_clip_rule(SP_WIND_RULE_EVENODD)
+ , style_fill_rule(SP_WIND_RULE_EVENODD)
+ , style_opacity(SP_SCALE24_MAX)
+ , _last_pick(nullptr)
+ , _repick_after(0)
+{
+}
+
+void DrawingShape::setPath(std::shared_ptr<SPCurve const> curve)
+{
+ defer([this, curve = std::move(curve)] () mutable {
+ _markForRendering();
+ _curve = std::move(curve);
+ _markForUpdate(STATE_ALL, false);
+ });
+}
+
+void DrawingShape::setStyle(SPStyle const *style, SPStyle const *context_style)
+{
+ DrawingItem::setStyle(style, context_style);
+
+ auto vector_effect_stroke = false;
+ auto stroke_extensions_hairline = false;
+ auto clip_rule = SP_WIND_RULE_EVENODD;
+ auto fill_rule = SP_WIND_RULE_EVENODD;
+ auto opacity = SP_SCALE24_MAX;
+ if (style) {
+ vector_effect_stroke = style->vector_effect.stroke;
+ stroke_extensions_hairline = style->stroke_extensions.hairline;
+ clip_rule = style->clip_rule.value;
+ fill_rule = style->fill_rule.value;
+ opacity = style->opacity.value;
+ }
+
+ defer([=, nrstyle = NRStyleData(_style)] () mutable {
+ _nrstyle.set(std::move(nrstyle));
+ style_vector_effect_stroke = vector_effect_stroke;
+ style_stroke_extensions_hairline = stroke_extensions_hairline;
+ style_clip_rule = clip_rule;
+ style_fill_rule = fill_rule;
+ style_opacity = opacity;
+ });
+}
+
+void DrawingShape::setChildrenStyle(SPStyle const *context_style)
+{
+ DrawingItem::setChildrenStyle(context_style);
+
+ defer([this, nrstyle = NRStyleData(_style, _context_style)] () mutable {
+ _nrstyle.set(std::move(nrstyle));
+ });
+}
+
+unsigned DrawingShape::_updateItem(Geom::IntRect const &area, UpdateContext const &ctx, unsigned flags, unsigned reset)
+{
+ // update markers
+ for (auto &c : _children) {
+ c.update(area, ctx, flags, reset);
+ }
+
+ // clear Cairo data to force update
+ if (flags & STATE_RENDER) {
+ _nrstyle.invalidate();
+ }
+
+ auto calc_curve_bbox = [&, this] () -> Geom::OptIntRect {
+ if (!_curve) {
+ return {};
+ }
+
+ auto rect = bounds_exact_transformed(_curve->get_pathvector(), ctx.ctm);
+ if (!rect) {
+ return {};
+ }
+
+ float stroke_max = 0.0f;
+
+ // Get the normal stroke.
+ if (_drawing.renderMode() != RenderMode::OUTLINE && _nrstyle.data.stroke.type != NRStyleData::PaintType::NONE) {
+ // Expand by stroke width.
+ stroke_max = _nrstyle.data.stroke_width * 0.5f;
+
+ // Scale by view transformation, unless vector effect stroke.
+ if (!style_vector_effect_stroke) {
+ stroke_max *= max_expansion(ctx.ctm);
+ }
+
+ // Cap minimum line width if asked.
+ if (_drawing.renderMode() == RenderMode::VISIBLE_HAIRLINES || style_stroke_extensions_hairline) {
+ stroke_max = std::max(stroke_max, 0.5f);
+ }
+ }
+
+ // Get the outline stroke.
+ if (_drawing.renderMode() == RenderMode::OUTLINE || _drawing.outlineOverlay()) {
+ stroke_max = std::max(stroke_max, 0.5f);
+ }
+
+ if (stroke_max > 0.0f) {
+ // Expand by mitres, if present.
+ if (_nrstyle.data.line_join == CAIRO_LINE_JOIN_MITER && _nrstyle.data.miter_limit >= 1.0f) {
+ stroke_max *= _nrstyle.data.miter_limit;
+ }
+
+ // Apply expansion if non-zero.
+ if (stroke_max > 0.01) {
+ rect->expandBy(stroke_max);
+ }
+ }
+
+ return rect->roundOutwards();
+ };
+
+ if (flags & STATE_BBOX) {
+ _bbox = calc_curve_bbox();
+
+ for (auto &c : _children) {
+ _bbox.unionWith(c.bbox());
+ }
+ }
+
+ return _state | flags;
+}
+
+void DrawingShape::_renderFill(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area) const
+{
+ Inkscape::DrawingContext::Save save(dc);
+ dc.transform(_ctm);
+
+ auto has_fill = _nrstyle.prepareFill(dc, rc, area, _item_bbox, _fill_pattern);
+
+ if (has_fill) {
+ dc.path(_curve->get_pathvector());
+ auto dl = DitherLock(dc, _nrstyle.data.fill.ditherable() && _drawing.useDithering());
+ _nrstyle.applyFill(dc, has_fill);
+ dc.fillPreserve();
+ dc.newPath(); // clear path
+ }
+}
+
+void DrawingShape::_renderStroke(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, unsigned flags) const
+{
+ Inkscape::DrawingContext::Save save(dc);
+ dc.transform(_ctm);
+
+ auto has_stroke = _nrstyle.prepareStroke(dc, rc, area, _item_bbox, _stroke_pattern);
+ if (!style_stroke_extensions_hairline && _nrstyle.data.stroke_width == 0) {
+ has_stroke.reset();
+ }
+
+ if (has_stroke) {
+ // TODO: remove segments outside of bbox when no dashes present
+ dc.path(_curve->get_pathvector());
+ if (style_vector_effect_stroke) {
+ dc.restore();
+ dc.save();
+ }
+ auto dl = DitherLock(dc, _nrstyle.data.stroke.ditherable() && _drawing.useDithering());
+ _nrstyle.applyStroke(dc, has_stroke);
+
+ // If the stroke is a hairline, set it to exactly 1px on screen.
+ // If visible hairline mode is on, make sure the line is at least 1px.
+ if (flags & RENDER_VISIBLE_HAIRLINES || style_stroke_extensions_hairline) {
+ double dx = 1.0, dy = 0.0;
+ dc.device_to_user_distance(dx, dy);
+ auto pixel_size = std::hypot(dx, dy);
+ if (style_stroke_extensions_hairline || _nrstyle.data.stroke_width < pixel_size) {
+ dc.setHairline();
+ }
+ }
+
+ dc.strokePreserve();
+ dc.newPath(); // clear path
+ }
+}
+
+void DrawingShape::_renderMarkers(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, unsigned flags, DrawingItem const *stop_at) const
+{
+ // marker rendering
+ for (auto &i : _children) {
+ i.render(dc, rc, area, flags, stop_at);
+ }
+}
+
+unsigned DrawingShape::_renderItem(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, unsigned flags, DrawingItem const *stop_at) const
+{
+ if (!_curve) return RENDER_OK;
+
+ auto visible = area & _bbox;
+ if (!visible) return RENDER_OK; // skip if not within bounding box
+
+ bool outline = flags & RENDER_OUTLINE;
+
+ if (outline) {
+ auto rgba = rc.outline_color;
+
+ // paint-order doesn't matter
+ {
+ Inkscape::DrawingContext::Save save(dc);
+ dc.transform(_ctm);
+ dc.path(_curve->get_pathvector());
+ }
+ {
+ Inkscape::DrawingContext::Save save(dc);
+ dc.setSource(rgba);
+ dc.setLineWidth(0.5);
+ dc.setTolerance(0.5);
+ dc.stroke();
+ }
+
+ _renderMarkers(dc, rc, area, flags, stop_at);
+ return RENDER_OK;
+ }
+
+ if (_nrstyle.data.paint_order_layer[0] == NRStyleData::PAINT_ORDER_NORMAL) {
+ // This is the most common case, special case so we don't call get_pathvector(), etc. twice
+
+ {
+ // we assume the context has no path
+ Inkscape::DrawingContext::Save save(dc);
+ dc.transform(_ctm);
+
+ // update fill and stroke paints.
+ // this cannot be done during nr_arena_shape_update, because we need a Cairo context
+ // to render svg:pattern
+ auto has_fill = _nrstyle.prepareFill(dc, rc, *visible, _item_bbox, _fill_pattern);
+ auto has_stroke = _nrstyle.prepareStroke(dc, rc, *visible, _item_bbox, _stroke_pattern);
+ if (!_nrstyle.data.hairline && _nrstyle.data.stroke_width == 0) {
+ has_stroke.reset();
+ }
+ if (has_fill || has_stroke) {
+ dc.path(_curve->get_pathvector());
+ // TODO: remove segments outside of bbox when no dashes present
+ if (has_fill) {
+ auto dl = DitherLock(dc, _nrstyle.data.fill.ditherable() && _drawing.useDithering());
+ _nrstyle.applyFill(dc, has_fill);
+ dc.fillPreserve();
+ }
+ if (style_vector_effect_stroke) {
+ dc.restore();
+ dc.save();
+ }
+ if (has_stroke) {
+ auto dl = DitherLock(dc, _nrstyle.data.stroke.ditherable() && _drawing.useDithering());
+ _nrstyle.applyStroke(dc, has_stroke);
+
+ // If the draw mode is set to visible hairlines, don't let anything get smaller
+ // than half a pixel.
+ if (flags & RENDER_VISIBLE_HAIRLINES) {
+ double dx = 1.0, dy = 0.0;
+ dc.device_to_user_distance(dx, dy);
+ auto half_pixel_size = std::hypot(dx, dy) * 0.5;
+ if (_nrstyle.data.stroke_width < half_pixel_size) {
+ dc.setLineWidth(half_pixel_size);
+ }
+ }
+
+ dc.strokePreserve();
+ }
+ dc.newPath(); // clear path
+ } // has fill or stroke pattern
+ }
+ _renderMarkers(dc, rc, area, flags, stop_at);
+ return RENDER_OK;
+
+ }
+
+ // Handle different paint orders
+ for (auto &i : _nrstyle.data.paint_order_layer) {
+ switch (i) {
+ case NRStyleData::PAINT_ORDER_FILL:
+ _renderFill(dc, rc, *visible);
+ break;
+ case NRStyleData::PAINT_ORDER_STROKE:
+ _renderStroke(dc, rc, *visible, flags);
+ break;
+ case NRStyleData::PAINT_ORDER_MARKER:
+ _renderMarkers(dc, rc, area, flags, stop_at);
+ break;
+ default:
+ // PAINT_ORDER_AUTO Should not happen
+ break;
+ }
+ }
+
+ return RENDER_OK;
+}
+
+void DrawingShape::_clipItem(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &/*area*/) const
+{
+ if (!_curve) return;
+
+ Inkscape::DrawingContext::Save save(dc);
+ if (style_clip_rule == SP_WIND_RULE_EVENODD) {
+ dc.setFillRule(CAIRO_FILL_RULE_EVEN_ODD);
+ } else {
+ dc.setFillRule(CAIRO_FILL_RULE_WINDING);
+ }
+ dc.transform(_ctm);
+ dc.path(_curve->get_pathvector());
+ dc.fill();
+}
+
+DrawingItem *DrawingShape::_pickItem(Geom::Point const &p, double delta, unsigned flags)
+{
+ if (_repick_after > 0)
+ --_repick_after;
+
+ if (_repick_after > 0) { // we are a slow, huge path
+ return _last_pick; // skip this pick, returning what was returned last time
+ }
+
+ if (!_curve) return nullptr;
+ bool outline = flags & PICK_OUTLINE;
+ bool pick_as_clip = flags & PICK_AS_CLIP;
+
+ if (SP_SCALE24_TO_FLOAT(style_opacity) == 0 && !outline && !pick_as_clip && !_drawing.selectZeroOpacity()) {
+ // fully transparent, no pick unless outline mode
+ return nullptr;
+ }
+
+ gint64 tstart = g_get_monotonic_time();
+
+ double width;
+ if (pick_as_clip) {
+ width = 0; // no width should be applied to clip picking
+ // this overrides display mode and stroke style considerations
+ } else if (outline) {
+ width = 0.5; // in outline mode, everything is stroked with the same 0.5px line width
+ } else if (_nrstyle.data.stroke.type != NRStyleData::PaintType::NONE && (_nrstyle.data.stroke.opacity > 1e-3 || _drawing.selectZeroOpacity())) {
+ // for normal picking calculate the distance corresponding top the stroke width
+ float scale = max_expansion(_ctm);
+ width = std::max(0.125f, _nrstyle.data.stroke_width * scale) / 2;
+ } else {
+ width = 0;
+ }
+
+ double dist = Geom::infinity();
+ int wind = 0;
+ bool needfill = pick_as_clip || (_nrstyle.data.fill.type != NRStyleData::PaintType::NONE && (_nrstyle.data.fill.opacity > 1e-3 || _drawing.selectZeroOpacity()) && !outline);
+ bool wind_evenodd = (pick_as_clip ? style_clip_rule : style_fill_rule) == SP_WIND_RULE_EVENODD;
+
+ // actual shape picking
+ if (_drawing.getCanvasItemDrawing()) {
+ Geom::Rect viewbox = _drawing.getCanvasItemDrawing()->get_canvas()->get_area_world();
+ viewbox.expandBy (width);
+ pathv_matrix_point_bbox_wind_distance(_curve->get_pathvector(), _ctm, p, nullptr, needfill? &wind : nullptr, &dist, 0.5, &viewbox);
+ } else {
+ pathv_matrix_point_bbox_wind_distance(_curve->get_pathvector(), _ctm, p, nullptr, needfill? &wind : nullptr, &dist, 0.5, nullptr);
+ }
+
+ gint64 tfinish = g_get_monotonic_time();
+ gint64 this_pick = tfinish - tstart;
+ //g_print ("pick time %lu\n", this_pick);
+ if (this_pick > 10000) { // slow picking, remember to skip several new picks
+ _repick_after = this_pick / 5000;
+ }
+
+ // covered by fill?
+ if (needfill) {
+ if (wind_evenodd) {
+ if (wind & 0x1) {
+ _last_pick = this;
+ return this;
+ }
+ } else {
+ if (wind != 0) {
+ _last_pick = this;
+ return this;
+ }
+ }
+ }
+
+ // close to the edge, as defined by strokewidth and delta?
+ // this ignores dashing (as if the stroke is solid) and always works as if caps are round
+ if (needfill || width > 0) { // if either fill or stroke visible,
+ if ((dist - width) < delta) {
+ _last_pick = this;
+ return this;
+ }
+ }
+
+ // if not picked on the shape itself, try its markers
+ for (auto &i : _children) {
+ DrawingItem *ret = i.pick(p, delta, flags & ~PICK_STICKY);
+ if (ret) {
+ _last_pick = this;
+ return this;
+ }
+ }
+
+ _last_pick = nullptr;
+ return nullptr;
+}
+
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :