diff options
Diffstat (limited to 'src/display/drawing-shape.cpp')
-rw-r--r-- | src/display/drawing-shape.cpp | 444 |
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 : |