mirror of
https://github.com/penpot/penpot.git
synced 2026-08-28 15:48:52 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
2eb9423963
@ -78,6 +78,11 @@
|
|||||||
text-transform-attrs
|
text-transform-attrs
|
||||||
text-fills))
|
text-fills))
|
||||||
|
|
||||||
|
(def text-span-attrs
|
||||||
|
"Inline text span attrs. Line-height is paragraph-level in the DOM editor;
|
||||||
|
it may still be stored redundantly on span nodes."
|
||||||
|
(vec (remove #{:line-height} text-node-attrs)))
|
||||||
|
|
||||||
(defn text-node-attr?
|
(defn text-node-attr?
|
||||||
[attr]
|
[attr]
|
||||||
(d/index-of text-node-attrs attr))
|
(d/index-of text-node-attrs attr))
|
||||||
@ -317,9 +322,16 @@
|
|||||||
"Given two content text structures, conformed by maps and vectors,
|
"Given two content text structures, conformed by maps and vectors,
|
||||||
compare them, and returns a set with the attributes that have changed.
|
compare them, and returns a set with the attributes that have changed.
|
||||||
This is independent of the text structure, so if the structure changes
|
This is independent of the text structure, so if the structure changes
|
||||||
but the attributes are the same, it will return an empty set."
|
but the attributes are the same, it will return an empty set.
|
||||||
|
|
||||||
|
Line-height on text nodes is ignored: it is a paragraph-level attribute
|
||||||
|
and may be stored redundantly on spans (e.g. after token apply)."
|
||||||
[a b]
|
[a b]
|
||||||
(let [diff-attrs (compare-text-content a b
|
(let [strip-span-line-height
|
||||||
|
#(transform-nodes is-text-node? (fn [node] (dissoc node :line-height)) %)
|
||||||
|
a (strip-span-line-height a)
|
||||||
|
b (strip-span-line-height b)
|
||||||
|
diff-attrs (compare-text-content a b
|
||||||
{:text-cb identity
|
{:text-cb identity
|
||||||
:attribute-cb (fn [acc attr] (conj acc attr))})]
|
:attribute-cb (fn [acc attr] (conj acc attr))})]
|
||||||
(if-not (contains? diff-attrs :text-content-structure)
|
(if-not (contains? diff-attrs :text-content-structure)
|
||||||
|
|||||||
@ -76,8 +76,31 @@
|
|||||||
(assoc-in content-base [:children 0 :children 0 :children 0 :font-family] "Arial"))
|
(assoc-in content-base [:children 0 :children 0 :children 0 :font-family] "Arial"))
|
||||||
|
|
||||||
(def content-changed-line-height
|
(def content-changed-line-height
|
||||||
|
(assoc-in content-base [:children 0 :children 0 :line-height] "1.5"))
|
||||||
|
|
||||||
|
(def content-redundant-span-line-height
|
||||||
(assoc-in content-base [:children 0 :children 0 :children 0 :line-height] "1.5"))
|
(assoc-in content-base [:children 0 :children 0 :children 0 :line-height] "1.5"))
|
||||||
|
|
||||||
|
;; Token apply may store line-height on paragraph and spans; after a DOM
|
||||||
|
;; round-trip spans no longer carry it (paragraph-level in the editor).
|
||||||
|
(def content-token-like-line-height
|
||||||
|
(-> content-base
|
||||||
|
(assoc-in [:children 0 :children 0 :line-height] 1.4)
|
||||||
|
(assoc-in [:children 0 :children 0 :children 0 :line-height] 1.4)))
|
||||||
|
|
||||||
|
(def content-after-editor-roundtrip
|
||||||
|
(update-in content-token-like-line-height
|
||||||
|
[:children 0 :children 0 :children 0]
|
||||||
|
dissoc :line-height))
|
||||||
|
|
||||||
|
;; from_dom used to merge default nil typography refs on import.
|
||||||
|
(def content-explicit-nil-typography-refs
|
||||||
|
(-> content-base
|
||||||
|
(assoc-in [:children 0 :children 0 :typography-ref-id] nil)
|
||||||
|
(assoc-in [:children 0 :children 0 :typography-ref-file] nil)
|
||||||
|
(assoc-in [:children 0 :children 0 :children 0 :typography-ref-id] nil)
|
||||||
|
(assoc-in [:children 0 :children 0 :children 0 :typography-ref-file] nil)))
|
||||||
|
|
||||||
(def content-changed-letter-spacing
|
(def content-changed-letter-spacing
|
||||||
(assoc-in content-base [:children 0 :children 0 :children 0 :letter-spacing] "2"))
|
(assoc-in content-base [:children 0 :children 0 :children 0 :letter-spacing] "2"))
|
||||||
|
|
||||||
@ -185,6 +208,10 @@
|
|||||||
;; Other text-node-attr categories
|
;; Other text-node-attr categories
|
||||||
attrs-font-family (cttx/get-diff-attrs content-base content-changed-font-family)
|
attrs-font-family (cttx/get-diff-attrs content-base content-changed-font-family)
|
||||||
attrs-line-height (cttx/get-diff-attrs content-base content-changed-line-height)
|
attrs-line-height (cttx/get-diff-attrs content-base content-changed-line-height)
|
||||||
|
attrs-span-line-height (cttx/get-diff-attrs content-base content-redundant-span-line-height)
|
||||||
|
attrs-roundtrip-line-height (cttx/get-diff-attrs content-token-like-line-height
|
||||||
|
content-after-editor-roundtrip)
|
||||||
|
attrs-nil-typography-refs (cttx/get-diff-attrs content-base content-explicit-nil-typography-refs)
|
||||||
attrs-letter-spacing (cttx/get-diff-attrs content-base content-changed-letter-spacing)
|
attrs-letter-spacing (cttx/get-diff-attrs content-base content-changed-letter-spacing)
|
||||||
attrs-text-decoration (cttx/get-diff-attrs content-base content-changed-text-decoration)
|
attrs-text-decoration (cttx/get-diff-attrs content-base content-changed-text-decoration)
|
||||||
attrs-text-transform (cttx/get-diff-attrs content-base content-changed-text-transform)
|
attrs-text-transform (cttx/get-diff-attrs content-base content-changed-text-transform)
|
||||||
@ -215,6 +242,9 @@
|
|||||||
;; Each text-node-attr category reports correct attr key
|
;; Each text-node-attr category reports correct attr key
|
||||||
(t/is (= #{:font-family} attrs-font-family))
|
(t/is (= #{:font-family} attrs-font-family))
|
||||||
(t/is (= #{:line-height} attrs-line-height))
|
(t/is (= #{:line-height} attrs-line-height))
|
||||||
|
(t/is (= #{} attrs-span-line-height))
|
||||||
|
(t/is (= #{} attrs-roundtrip-line-height))
|
||||||
|
(t/is (= #{} attrs-nil-typography-refs))
|
||||||
(t/is (= #{:letter-spacing} attrs-letter-spacing))
|
(t/is (= #{:letter-spacing} attrs-letter-spacing))
|
||||||
(t/is (= #{:text-decoration} attrs-text-decoration))
|
(t/is (= #{:text-decoration} attrs-text-decoration))
|
||||||
(t/is (= #{:text-transform} attrs-text-transform))
|
(t/is (= #{:text-transform} attrs-text-transform))
|
||||||
|
|||||||
@ -51,13 +51,19 @@
|
|||||||
[_ style-decode] (get styles/mapping key)]
|
[_ style-decode] (get styles/mapping key)]
|
||||||
(style-decode (.getPropertyValue style style-name)))
|
(style-decode (.getPropertyValue style style-name)))
|
||||||
(let [style-name (styles/get-style-name key)]
|
(let [style-name (styles/get-style-name key)]
|
||||||
(styles/normalize-attr-value key (.getPropertyValue style style-name))))]
|
(styles/normalize-attr-value key (.getPropertyValue style style-name))))
|
||||||
(assoc acc key (if (value-empty? value) (get defaults key) value))))
|
default (get defaults key)
|
||||||
|
final-value (if (value-empty? value) default value)]
|
||||||
|
;; Omit attrs with no CSS value when the default is nil (e.g.
|
||||||
|
;; typography-ref-id). Avoids polluting round-tripped content.
|
||||||
|
(if (and (value-empty? value) (nil? default))
|
||||||
|
acc
|
||||||
|
(assoc acc key final-value))))
|
||||||
{} attrs)))
|
{} attrs)))
|
||||||
|
|
||||||
(defn get-text-span-styles
|
(defn get-text-span-styles
|
||||||
[element]
|
[element]
|
||||||
(get-attrs-from-styles element txt/text-node-attrs (txt/get-default-text-attrs)))
|
(get-attrs-from-styles element txt/text-span-attrs (txt/get-default-text-attrs)))
|
||||||
|
|
||||||
(defn get-paragraph-styles
|
(defn get-paragraph-styles
|
||||||
[element]
|
[element]
|
||||||
|
|||||||
@ -94,9 +94,8 @@
|
|||||||
|
|
||||||
(defn get-text-span-styles
|
(defn get-text-span-styles
|
||||||
[inline paragraph]
|
[inline paragraph]
|
||||||
(let [node (if (= "" (:text inline)) paragraph inline)
|
(let [node (if (= "" (:text inline)) paragraph inline)]
|
||||||
styles (get-styles-from-attrs node txt/text-node-attrs txt/default-text-attrs)]
|
(get-styles-from-attrs node txt/text-span-attrs txt/default-text-attrs)))
|
||||||
(dissoc styles :line-height)))
|
|
||||||
|
|
||||||
(defn normalize-spaces
|
(defn normalize-spaces
|
||||||
"Add zero-width spaces after forward slashes to enable word breaking"
|
"Add zero-width spaces after forward slashes to enable word breaking"
|
||||||
|
|||||||
@ -2003,89 +2003,12 @@ impl RenderState {
|
|||||||
pub fn render_from_cache(&mut self, shapes: ShapesPoolRef) {
|
pub fn render_from_cache(&mut self, shapes: ShapesPoolRef) {
|
||||||
let _start = performance::begin_timed_log!("render_from_cache");
|
let _start = performance::begin_timed_log!("render_from_cache");
|
||||||
performance::begin_measure!("render_from_cache");
|
performance::begin_measure!("render_from_cache");
|
||||||
let bg_color = self.background_color;
|
self.surfaces.draw_combined_atlas_to_backbuffer(
|
||||||
|
&self.viewbox,
|
||||||
// During fast mode (pan/zoom), if a previous full-quality render still has pending tiles,
|
&self.tile_viewbox,
|
||||||
// always prefer the persistent atlas. The atlas is incrementally updated as tiles finish,
|
self.background_color,
|
||||||
// and drawing from it avoids mixing a partially-updated Cache surface with missing tiles.
|
);
|
||||||
if self.options.is_fast_mode() && !self.surfaces.atlas.is_empty() {
|
self.present_frame(shapes);
|
||||||
self.surfaces
|
|
||||||
.draw_atlas_to_backbuffer(self.viewbox, bg_color);
|
|
||||||
|
|
||||||
self.present_frame(shapes);
|
|
||||||
performance::end_measure!("render_from_cache");
|
|
||||||
performance::end_timed_log!("render_from_cache", _start);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have a valid cached viewbox (non-zero dimensions indicate valid cache)
|
|
||||||
if self.cached_viewbox.area.width() > 0.0 {
|
|
||||||
// Scale and translate the target according to the cached data
|
|
||||||
let navigate_zoom = self.viewbox.zoom / self.cached_viewbox.zoom;
|
|
||||||
|
|
||||||
let interest = self.options.dpr_viewport_interest_area_threshold;
|
|
||||||
let TileRect(start_tile_x, start_tile_y, _, _) =
|
|
||||||
tiles::get_tiles_for_viewbox_with_interest(&self.cached_viewbox, interest);
|
|
||||||
let offset_x = self.viewbox.area.left * self.cached_viewbox.zoom * self.options.dpr;
|
|
||||||
let offset_y = self.viewbox.area.top * self.cached_viewbox.zoom * self.options.dpr;
|
|
||||||
let translate_x = (start_tile_x as f32 * tiles::TILE_SIZE) - offset_x;
|
|
||||||
let translate_y = (start_tile_y as f32 * tiles::TILE_SIZE) - offset_y;
|
|
||||||
|
|
||||||
// For zoom-out, prefer cache only if it fully covers the viewport.
|
|
||||||
// Otherwise, atlas will provide a more correct full-viewport preview.
|
|
||||||
let zooming_out = self.viewbox.zoom < self.cached_viewbox.zoom;
|
|
||||||
if zooming_out {
|
|
||||||
let cache_dim = self.surfaces.cache_dimensions();
|
|
||||||
let cache_w = cache_dim.width as f32;
|
|
||||||
let cache_h = cache_dim.height as f32;
|
|
||||||
|
|
||||||
// Viewport in target pixels.
|
|
||||||
let vw = self.viewbox.dpr_width().max(1.0);
|
|
||||||
let vh = self.viewbox.dpr_height().max(1.0);
|
|
||||||
|
|
||||||
// Inverse-map viewport corners into cache coordinates.
|
|
||||||
// target = (cache * navigate_zoom) translated by (translate_x, translate_y) (in cache coords).
|
|
||||||
// => cache = (target / navigate_zoom) - translate
|
|
||||||
let inv = if navigate_zoom.abs() > f32::EPSILON {
|
|
||||||
1.0 / navigate_zoom
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
|
|
||||||
// let cx0 = (0.0 * inv) - translate_x;
|
|
||||||
// let cy0 = (0.0 * inv) - translate_y;
|
|
||||||
// NOTA: 0.0 * inv => siempre 0
|
|
||||||
let cx0 = -translate_x;
|
|
||||||
let cy0 = -translate_y;
|
|
||||||
let cx1 = (vw * inv) - translate_x;
|
|
||||||
let cy1 = (vh * inv) - translate_y;
|
|
||||||
|
|
||||||
let min_x = cx0.min(cx1);
|
|
||||||
let min_y = cy0.min(cy1);
|
|
||||||
let max_x = cx0.max(cx1);
|
|
||||||
let max_y = cy0.max(cy1);
|
|
||||||
|
|
||||||
let cache_covers =
|
|
||||||
min_x >= 0.0 && min_y >= 0.0 && max_x <= cache_w && max_y <= cache_h;
|
|
||||||
if !cache_covers {
|
|
||||||
// Early return only if atlas exists; otherwise keep cache path.
|
|
||||||
if !self.surfaces.atlas.is_empty() {
|
|
||||||
self.surfaces
|
|
||||||
.draw_atlas_to_backbuffer(self.viewbox, bg_color);
|
|
||||||
|
|
||||||
self.present_frame(shapes);
|
|
||||||
performance::end_measure!("render_from_cache");
|
|
||||||
performance::end_timed_log!("render_from_cache", _start);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw directly from cache surface, avoiding snapshot overhead
|
|
||||||
self.surfaces.draw_cache_to_backbuffer();
|
|
||||||
|
|
||||||
self.present_frame(shapes);
|
|
||||||
}
|
|
||||||
|
|
||||||
performance::end_measure!("render_from_cache");
|
performance::end_measure!("render_from_cache");
|
||||||
performance::end_timed_log!("render_from_cache", _start);
|
performance::end_timed_log!("render_from_cache", _start);
|
||||||
|
|||||||
@ -28,13 +28,17 @@ const CANVAS_BORDER_RADIUS: f32 = 12.0;
|
|||||||
// distinct offsets for the two and we mirror that.
|
// distinct offsets for the two and we mirror that.
|
||||||
const SELECTION_LABEL_BASELINE: f32 = 13.6;
|
const SELECTION_LABEL_BASELINE: f32 = 13.6;
|
||||||
|
|
||||||
// Selection-label gradient mask: matches the SVG `selection-gradient-start`
|
// Selection-label gradient mask: fades the tick labels behind the selection
|
||||||
// and `selection-gradient-end` defs. The mask is `OVER_NUMBER_SIZE` screen
|
// band. The mask is `OVER_NUMBER_SIZE` screen pixels long. `OVER_NUMBER_PERCENT`
|
||||||
// pixels long, with the opaque part starting `OVER_NUMBER_PERCENT` of the
|
// is the fraction of the mask that sits *outside* the band; at 1.0 the mask's
|
||||||
// way through the rect (40% from the outside edge, 60% from the inside).
|
// inner edge lands exactly on the band edge, so the dark shadow begins where
|
||||||
|
// the green selection area ends and fades outward (it never bleeds under the
|
||||||
|
// band). `GRADIENT_FADE_FRACTION` is the share of the mask spent on the
|
||||||
|
// transparent→opaque ramp at the outer end.
|
||||||
const OVER_NUMBER_SIZE: f32 = 100.0;
|
const OVER_NUMBER_SIZE: f32 = 100.0;
|
||||||
const OVER_NUMBER_PERCENT: f32 = 0.75;
|
const OVER_NUMBER_PERCENT: f32 = 1.0;
|
||||||
const GRADIENT_FADE_FRACTION: f32 = 0.4;
|
const GRADIENT_FADE_FRACTION: f32 = 0.4;
|
||||||
|
const OVER_NUMBER_OPACITY: f32 = 0.8;
|
||||||
|
|
||||||
fn calculate_step_size(zoom: f32) -> f32 {
|
fn calculate_step_size(zoom: f32) -> f32 {
|
||||||
if zoom <= 0.0 {
|
if zoom <= 0.0 {
|
||||||
@ -308,23 +312,27 @@ fn draw_selection_x(ctx: &RenderCtx, sel: Rect, offset: f32) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let text_y = ctx.vy + SELECTION_LABEL_BASELINE * zi;
|
let text_y = ctx.vy + SELECTION_LABEL_BASELINE * zi;
|
||||||
let pad_x = 4.0 * zi;
|
// Both labels use the same half-bar gap from the band so the start (left)
|
||||||
|
// and end (right) are spaced symmetrically.
|
||||||
|
let gap = (RULER_AREA_SIZE / 2.0) * zi;
|
||||||
let left_label = format_label(sel.left - offset);
|
let left_label = format_label(sel.left - offset);
|
||||||
let right_label = format_label(sel.right - offset);
|
let right_label = format_label(sel.right - offset);
|
||||||
let (lw_font, _) = ctx.font.measure_str(&left_label, None);
|
let (lw_font, _) = ctx.font.measure_str(&left_label, None);
|
||||||
// The right label is anchored at its left edge, so we don't need its
|
let lx = sel.left - gap - lw_font * zi;
|
||||||
// measured width.
|
let rx = sel.right + gap;
|
||||||
let lx = sel.left - pad_x - lw_font * zi;
|
|
||||||
let rx = sel.right + pad_x;
|
|
||||||
|
|
||||||
let mut text_paint = Paint::default();
|
let mut text_paint = Paint::default();
|
||||||
text_paint.set_color(ctx.state.accent_color);
|
text_paint.set_color(ctx.state.accent_color);
|
||||||
text_paint.set_anti_alias(true);
|
text_paint.set_anti_alias(true);
|
||||||
|
|
||||||
|
// 1. Left label
|
||||||
canvas.save();
|
canvas.save();
|
||||||
canvas.translate((lx, text_y));
|
canvas.translate((lx, text_y));
|
||||||
canvas.scale((zi, zi));
|
canvas.scale((zi, zi));
|
||||||
canvas.draw_str(&left_label, Point::new(0.0, 0.0), ctx.font, &text_paint);
|
canvas.draw_str(&left_label, Point::new(0.0, 0.0), ctx.font, &text_paint);
|
||||||
canvas.restore();
|
canvas.restore();
|
||||||
|
|
||||||
|
// 2. Right label
|
||||||
canvas.save();
|
canvas.save();
|
||||||
canvas.translate((rx, text_y));
|
canvas.translate((rx, text_y));
|
||||||
canvas.scale((zi, zi));
|
canvas.scale((zi, zi));
|
||||||
@ -341,7 +349,7 @@ enum MaskAxis {
|
|||||||
/// labels behind the selection band. `fade_to_end` flips it from
|
/// labels behind the selection band. `fade_to_end` flips it from
|
||||||
/// transparent→opaque (before the band) to opaque→transparent (after).
|
/// transparent→opaque (before the band) to opaque→transparent (after).
|
||||||
fn draw_mask(ctx: &RenderCtx, rect: Rect, axis: MaskAxis, fade_to_end: bool) {
|
fn draw_mask(ctx: &RenderCtx, rect: Rect, axis: MaskAxis, fade_to_end: bool) {
|
||||||
let opaque = ctx.state.bg_color;
|
let opaque = with_alpha(ctx.state.bg_color, OVER_NUMBER_OPACITY);
|
||||||
let transparent = with_alpha(ctx.state.bg_color, 0.0);
|
let transparent = with_alpha(ctx.state.bg_color, 0.0);
|
||||||
let (colors, offsets): (&[skia::Color; 3], &[f32; 3]) = if fade_to_end {
|
let (colors, offsets): (&[skia::Color; 3], &[f32; 3]) = if fade_to_end {
|
||||||
(
|
(
|
||||||
@ -377,11 +385,10 @@ fn draw_selection_y(ctx: &RenderCtx, sel: Rect, offset: f32) {
|
|||||||
let canvas = ctx.canvas;
|
let canvas = ctx.canvas;
|
||||||
let zi = ctx.zi;
|
let zi = ctx.zi;
|
||||||
|
|
||||||
let pad_y = 4.0 * zi;
|
|
||||||
let top_label = format_label(sel.top - offset);
|
let top_label = format_label(sel.top - offset);
|
||||||
let bottom_label = format_label(sel.bottom - offset);
|
let bottom_label = format_label(sel.bottom - offset);
|
||||||
// Top label's draw position doesn't depend on its own width (LX is just
|
// Both labels sit a half-bar gap from the band; only the bottom label's
|
||||||
// pad_y/zi), so we only need bw_font for the bottom label's right-anchor.
|
// origin depends on its own width (it reads toward the band).
|
||||||
let (bw_font, _) = ctx.font.measure_str(&bottom_label, None);
|
let (bw_font, _) = ctx.font.measure_str(&bottom_label, None);
|
||||||
|
|
||||||
// Mask first (gradient bg over tick labels behind), then band, then
|
// Mask first (gradient bg over tick labels behind), then band, then
|
||||||
@ -416,33 +423,27 @@ fn draw_selection_y(ctx: &RenderCtx, sel: Rect, offset: f32) {
|
|||||||
let mut text_paint = Paint::default();
|
let mut text_paint = Paint::default();
|
||||||
text_paint.set_color(ctx.state.accent_color);
|
text_paint.set_color(ctx.state.accent_color);
|
||||||
text_paint.set_anti_alias(true);
|
text_paint.set_anti_alias(true);
|
||||||
// Both labels read bottom-to-top on screen (after the -90° rotation
|
// Both labels read bottom-to-top on screen
|
||||||
// local +x points upward). With the transform stack
|
// 1. Top label
|
||||||
// (translate→rotate→scale) and a draw at code-(LX, 0), the actual
|
|
||||||
// origin in document coords is (text_x, pivot_y − LX·zi).
|
|
||||||
//
|
|
||||||
// Top label: want origin just above sel.top, reading upward from
|
|
||||||
// there, so pivot_y − LX·zi = sel.top − pad_y ⇒ LX = pad_y/zi.
|
|
||||||
canvas.save();
|
canvas.save();
|
||||||
canvas.translate((text_x, sel.top));
|
canvas.translate((text_x, sel.top));
|
||||||
canvas.rotate(-90.0, None);
|
canvas.rotate(-90.0, None);
|
||||||
canvas.scale((zi, zi));
|
canvas.scale((zi, zi));
|
||||||
canvas.draw_str(
|
canvas.draw_str(
|
||||||
&top_label,
|
&top_label,
|
||||||
Point::new(pad_y / zi, 0.0),
|
Point::new(RULER_AREA_SIZE / 2.0, 0.0),
|
||||||
ctx.font,
|
ctx.font,
|
||||||
&text_paint,
|
&text_paint,
|
||||||
);
|
);
|
||||||
canvas.restore();
|
canvas.restore();
|
||||||
// Bottom label: want the text END at sel.bottom + pad_y and origin
|
// 2. Bottom label
|
||||||
// at sel.bottom + pad_y + bw so it reads upward toward the band.
|
|
||||||
canvas.save();
|
canvas.save();
|
||||||
canvas.translate((text_x, sel.bottom));
|
canvas.translate((text_x, sel.bottom));
|
||||||
canvas.rotate(-90.0, None);
|
canvas.rotate(-90.0, None);
|
||||||
canvas.scale((zi, zi));
|
canvas.scale((zi, zi));
|
||||||
canvas.draw_str(
|
canvas.draw_str(
|
||||||
&bottom_label,
|
&bottom_label,
|
||||||
Point::new(-bw_font - pad_y / zi, 0.0),
|
Point::new(-bw_font - RULER_AREA_SIZE / 2.0, 0.0),
|
||||||
ctx.font,
|
ctx.font,
|
||||||
&text_paint,
|
&text_paint,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -590,6 +590,57 @@ impl Surfaces {
|
|||||||
canvas.restore();
|
canvas.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fast pan/zoom preview: draw the doc atlas as backdrop, then overlay HQ
|
||||||
|
/// cached tile textures placed via their stored document rects (pan + scale).
|
||||||
|
pub fn draw_combined_atlas_to_backbuffer(
|
||||||
|
&mut self,
|
||||||
|
viewbox: &Viewbox,
|
||||||
|
tile_viewbox: &TileViewbox,
|
||||||
|
background: skia::Color,
|
||||||
|
) {
|
||||||
|
self.draw_atlas_to_backbuffer(*viewbox, background);
|
||||||
|
|
||||||
|
// Tile textures are keyed by grid index but positioned in document space.
|
||||||
|
// Without `tile_doc_rects` we cannot displace/scale them correctly (e.g.
|
||||||
|
// right after zoom invalidation); the atlas backdrop alone is enough.
|
||||||
|
if self.atlas.tile_doc_rects.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let batch = self.tiles.build_atlas_draw_batch_for_doc_rects(
|
||||||
|
viewbox,
|
||||||
|
tile_viewbox,
|
||||||
|
&self.atlas.tile_doc_rects,
|
||||||
|
);
|
||||||
|
if batch.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.tiles.needs_snapshot() || self.tile_atlas_image.is_none() {
|
||||||
|
self.tile_atlas_image = Some(self.tile_atlas.image_snapshot());
|
||||||
|
self.tiles.snapshot();
|
||||||
|
}
|
||||||
|
let Some(atlas_image) = self.tile_atlas_image.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let canvas = self.backbuffer.canvas();
|
||||||
|
canvas.save();
|
||||||
|
canvas.reset_matrix();
|
||||||
|
|
||||||
|
canvas.draw_atlas(
|
||||||
|
atlas_image,
|
||||||
|
&batch.transforms,
|
||||||
|
&batch.textures,
|
||||||
|
None,
|
||||||
|
skia::BlendMode::SrcOver,
|
||||||
|
self.atlas_sampling_options,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn margins(&self) -> skia::ISize {
|
pub fn margins(&self) -> skia::ISize {
|
||||||
self.margins
|
self.margins
|
||||||
}
|
}
|
||||||
@ -716,18 +767,6 @@ impl Surfaces {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws the cache surface directly to the backbuffer canvas.
|
|
||||||
/// This avoids creating an intermediate snapshot, reducing GPU stalls.
|
|
||||||
pub fn draw_cache_to_backbuffer(&mut self) {
|
|
||||||
let sampling_options = self.sampling_options;
|
|
||||||
self.cache.draw(
|
|
||||||
self.backbuffer.canvas(),
|
|
||||||
(0.0, 0.0),
|
|
||||||
sampling_options,
|
|
||||||
Some(&skia::Paint::default()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cache_dimensions(&self) -> skia::ISize {
|
pub fn cache_dimensions(&self) -> skia::ISize {
|
||||||
skia::ISize::new(self.cache.width(), self.cache.height())
|
skia::ISize::new(self.cache.width(), self.cache.height())
|
||||||
}
|
}
|
||||||
@ -1516,6 +1555,17 @@ pub struct TileTextureCache {
|
|||||||
removed: HashSet<Tile>,
|
removed: HashSet<Tile>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AtlasDrawBatch {
|
||||||
|
pub transforms: Vec<skia::RSXform>,
|
||||||
|
pub textures: Vec<skia::Rect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AtlasDrawBatch {
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.transforms.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TileTextureCache {
|
impl TileTextureCache {
|
||||||
pub fn new(texture_size: i32, capacity: usize) -> Self {
|
pub fn new(texture_size: i32, capacity: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -1615,6 +1665,76 @@ impl TileTextureCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn build_atlas_draw_batch_for_doc_rects(
|
||||||
|
&self,
|
||||||
|
viewbox: &Viewbox,
|
||||||
|
tile_viewbox: &TileViewbox,
|
||||||
|
tile_doc_rects: &HashMap<Tile, skia::Rect>,
|
||||||
|
) -> AtlasDrawBatch {
|
||||||
|
let mut transforms = Vec::new();
|
||||||
|
let mut textures = Vec::new();
|
||||||
|
|
||||||
|
let s = viewbox.get_scale();
|
||||||
|
let view_doc = viewbox.area;
|
||||||
|
|
||||||
|
for y in tile_viewbox.visible_rect.top()..=tile_viewbox.visible_rect.bottom() {
|
||||||
|
for x in tile_viewbox.visible_rect.left()..=tile_viewbox.visible_rect.right() {
|
||||||
|
let tile = Tile(x, y);
|
||||||
|
|
||||||
|
let Some(tile_ref) = self.grid.get(&tile) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.removed.contains(&tile) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let doc_rect = tile_doc_rects
|
||||||
|
.get(&tile)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_else(|| tiles::get_tile_rect(tile, s));
|
||||||
|
if doc_rect.is_empty() || !doc_rect.intersects(view_doc) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scos = doc_rect.width() * s / self.tile_size;
|
||||||
|
let tx = (doc_rect.left + viewbox.pan.x) * s;
|
||||||
|
let ty = (doc_rect.top + viewbox.pan.y) * s;
|
||||||
|
|
||||||
|
transforms.push(skia::RSXform::new(scos, 0.0, (tx, ty)));
|
||||||
|
textures.push(tile_ref.rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cached tiles from a previous zoom level use indices outside visible_rect;
|
||||||
|
// place them via their stored document rect, not the current grid walk above.
|
||||||
|
for (&tile, tile_ref) in &self.grid {
|
||||||
|
if tile_viewbox.is_visible(&tile) || self.removed.contains(&tile) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let doc_rect = tile_doc_rects
|
||||||
|
.get(&tile)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_else(|| tiles::get_tile_rect(tile, s));
|
||||||
|
if doc_rect.is_empty() || !doc_rect.intersects(view_doc) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tx = (doc_rect.left + viewbox.pan.x) * s;
|
||||||
|
let ty = (doc_rect.top + viewbox.pan.y) * s;
|
||||||
|
let scos = doc_rect.width() * s / self.tile_size;
|
||||||
|
|
||||||
|
transforms.push(skia::RSXform::new(scos, 0.0, (tx, ty)));
|
||||||
|
textures.push(tile_ref.rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
AtlasDrawBatch {
|
||||||
|
transforms,
|
||||||
|
textures,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn has(&self, tile: Tile) -> bool {
|
pub fn has(&self, tile: Tile) -> bool {
|
||||||
self.grid.contains_key(&tile) && !self.removed.contains(&tile)
|
self.grid.contains_key(&tile) && !self.removed.contains(&tile)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user