mirror of
https://github.com/penpot/penpot.git
synced 2026-01-20 04:10:41 -05:00
Compare commits
14 Commits
eva-fix-sh
...
alotor-exp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da4920a079 | ||
|
|
6fd8ef5574 | ||
|
|
cb29100f93 | ||
|
|
a3119bef5e | ||
|
|
c60d74df62 | ||
|
|
d593e299e3 | ||
|
|
4a8e02987f | ||
|
|
ee766e85a0 | ||
|
|
35e3b7f19a | ||
|
|
1810df232b | ||
|
|
3e99ad036c | ||
|
|
042a3a4080 | ||
|
|
f0687fd1f7 | ||
|
|
2c9159288f |
@@ -29,7 +29,7 @@
|
||||
- Fix missing text color token from selected shapes in selected colors list [Taiga #12956](https://tree.taiga.io/project/penpot/issue/12956)
|
||||
- Fix dropdown option width in Guides columns dropdown [Taiga #12959](https://tree.taiga.io/project/penpot/issue/12959)
|
||||
- Fix typos on download modal [Taiga #12865](https://tree.taiga.io/project/penpot/issue/12865)
|
||||
|
||||
- Fix problem with text editor maintaining previous styles [Taiga #12835](https://tree.taiga.io/project/penpot/issue/12835)
|
||||
|
||||
## 2.12.1
|
||||
|
||||
|
||||
@@ -58,7 +58,8 @@
|
||||
:share-id share-id
|
||||
:object-id (mapv :id objects)
|
||||
:route "objects"
|
||||
:skip-children skip-children}
|
||||
:skip-children skip-children
|
||||
:wasm "true"}
|
||||
uri (-> (cf/get :public-uri)
|
||||
(assoc :path "/render.html")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
:page-id page-id
|
||||
:share-id share-id
|
||||
:object-id object-id
|
||||
:route "objects"}]
|
||||
:route "objects"
|
||||
;;:wasm "true"
|
||||
}]
|
||||
(-> base-uri
|
||||
(assoc :path "/render.html")
|
||||
(assoc :query (u/map->query-string params)))))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 25 KiB |
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import {
|
||||
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
|
||||
getRandomFloat, useShape, setShapeChildren, setupInteraction, addShapeSolidStrokeFill,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import {
|
||||
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
|
||||
getRandomFloat, useShape, setShapeChildren, setupInteraction, set_parent
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import {
|
||||
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
|
||||
getRandomFloat, useShape, setShapeChildren, setupInteraction, set_parent, draw_star,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import {
|
||||
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
|
||||
getRandomFloat, useShape, setShapeChildren, setupInteraction, set_parent, allocBytes,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import {
|
||||
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
|
||||
getRandomFloat, useShape, setShapeChildren, setupInteraction, addShapeSolidStrokeFill
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import {
|
||||
init, assignCanvas, setupInteraction, useShape, setShapeChildren, addTextShape, hexToU32ARGB,getRandomInt, getRandomColor, getRandomFloat, addShapeSolidFill, addShapeSolidStrokeFill
|
||||
} from './js/lib.js';
|
||||
@@ -102,4 +102,4 @@
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -45,7 +45,9 @@
|
||||
[app.main.ui.shapes.svg-raw :as svg-raw]
|
||||
[app.main.ui.shapes.text :as text]
|
||||
[app.main.ui.shapes.text.fontfaces :as ff]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as g]
|
||||
[app.util.http :as http]
|
||||
[app.util.strings :as ust]
|
||||
[app.util.thumbnails :as th]
|
||||
@@ -53,6 +55,7 @@
|
||||
[beicon.v2.core :as rx]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:const viewbox-decimal-precision 3)
|
||||
@@ -171,6 +174,8 @@
|
||||
;; Don't wrap svg elements inside a <g> otherwise some can break
|
||||
[:> svg-raw-wrapper {:shape shape :frame frame}]))))))
|
||||
|
||||
(set! wasm.api/shape-wrapper-factory shape-wrapper-factory)
|
||||
|
||||
(defn format-viewbox
|
||||
"Format a viewbox given a rectangle"
|
||||
[{:keys [x y width height] :or {x 0 y 0 width 100 height 100}}]
|
||||
@@ -480,6 +485,48 @@
|
||||
[:& ff/fontfaces-style {:fonts fonts}]
|
||||
[:& shape-wrapper {:shape object}]]]]))
|
||||
|
||||
(mf/defc object-wasm
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [objects object-id embed skip-children]
|
||||
:or {embed false}
|
||||
:as props}]
|
||||
(let [object (get objects object-id)
|
||||
object (cond-> object
|
||||
(:hide-fill-on-export object)
|
||||
(assoc :fills [])
|
||||
|
||||
skip-children
|
||||
(assoc :shapes []))
|
||||
|
||||
{:keys [width height] :as bounds}
|
||||
(gsb/get-object-bounds objects object {:ignore-margin? false})
|
||||
|
||||
vbox (format-viewbox bounds)
|
||||
zoom 1
|
||||
canvas-ref (mf/use-ref nil)]
|
||||
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
(let [canvas (mf/ref-val canvas-ref)]
|
||||
(->> @wasm.api/module
|
||||
(p/fmap
|
||||
(fn [ready?]
|
||||
(when ready?
|
||||
(try
|
||||
(when (wasm.api/init-canvas-context canvas)
|
||||
(wasm.api/initialize-viewport
|
||||
objects zoom vbox "transparent"
|
||||
(fn []
|
||||
(wasm.api/render-sync-shape object-id)
|
||||
(dom/set-attribute! canvas "id" (dm/str "screenshot-" object-id)))))
|
||||
(catch :default e
|
||||
(js/console.error "Error initializing canvas context:" e)
|
||||
false)))))))))
|
||||
[:canvas {:ref canvas-ref
|
||||
:width width
|
||||
:height height
|
||||
:style {:background "red"}}]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SPRITES (DEBUG)
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -10,6 +10,7 @@ $z-index-200: 200;
|
||||
$z-index-300: 300;
|
||||
$z-index-400: 400;
|
||||
$z-index-500: 500;
|
||||
$z-index-600: 600;
|
||||
|
||||
:global(:root) {
|
||||
--z-index-auto: #{$z-index-auto}; // Index for elements such as workspace, rulers ...
|
||||
@@ -18,4 +19,5 @@ $z-index-500: 500;
|
||||
--z-index-set: #{$z-index-300}; // Index for configuration elements like modals, color picker, grid edition elements
|
||||
--z-index-dropdown: #{$z-index-400}; // Index for dropdown like elements, selects, menus, dropdowns
|
||||
--z-index-notifications: #{$z-index-500}; // Index for notification
|
||||
--z-index-loaders: #{$z-index-600}; // Index for loaders
|
||||
}
|
||||
|
||||
@@ -308,6 +308,16 @@
|
||||
[:div {:class (stl/css :sign-info)}
|
||||
[:button {:on-click on-click} (tr "labels.retry")]]]))
|
||||
|
||||
(mf/defc webgl-context-lost*
|
||||
[]
|
||||
(let [on-reload (mf/use-fn #(js/location.reload))]
|
||||
[:> error-container* {}
|
||||
[:div {:class (stl/css :main-message)} (tr "labels.webgl-context-lost.main-message")]
|
||||
[:div {:class (stl/css :desc-message)} (tr "labels.webgl-context-lost.desc-message")]
|
||||
[:div {:class (stl/css :buttons-container)}
|
||||
[:> button* {:variant "primary" :on-click on-reload}
|
||||
(tr "labels.reload-page")]]]))
|
||||
|
||||
(defn- generate-report
|
||||
[data]
|
||||
(try
|
||||
@@ -437,6 +447,7 @@
|
||||
(rx/of default)
|
||||
(rx/throw cause)))))))
|
||||
|
||||
|
||||
(mf/defc exception-section*
|
||||
{::mf/private true}
|
||||
[{:keys [data route] :as props}]
|
||||
@@ -469,6 +480,9 @@
|
||||
:service-unavailable
|
||||
[:> service-unavailable*]
|
||||
|
||||
:webgl-context-lost
|
||||
[:> webgl-context-lost*]
|
||||
|
||||
[:> internal-error* props])))
|
||||
|
||||
(mf/defc context-wrapper*
|
||||
|
||||
@@ -217,6 +217,10 @@
|
||||
|
||||
design-tokens? (features/use-feature "design-tokens/v1")
|
||||
|
||||
wasm-renderer-enabled? (features/use-feature "render-wasm/v1")
|
||||
|
||||
first-frame-rendered? (mf/use-state false)
|
||||
|
||||
background-color (:background-color wglobal)]
|
||||
|
||||
(mf/with-effect []
|
||||
@@ -241,6 +245,17 @@
|
||||
(when (and file-loaded? (not page-id))
|
||||
(st/emit! (dcm/go-to-workspace :file-id file-id ::rt/replace true))))
|
||||
|
||||
(mf/with-effect [file-id page-id]
|
||||
(reset! first-frame-rendered? false))
|
||||
|
||||
(mf/with-effect []
|
||||
(let [handle-wasm-render
|
||||
(fn [_]
|
||||
(reset! first-frame-rendered? true))
|
||||
listener-key (events/listen globals/document "penpot:wasm:render" handle-wasm-render)]
|
||||
(fn []
|
||||
(events/unlistenByKey listener-key))))
|
||||
|
||||
[:> (mf/provider ctx/current-project-id) {:value project-id}
|
||||
[:> (mf/provider ctx/current-file-id) {:value file-id}
|
||||
[:> (mf/provider ctx/current-page-id) {:value page-id}
|
||||
@@ -249,15 +264,18 @@
|
||||
[:> modal-container*]
|
||||
[:section {:class (stl/css :workspace)
|
||||
:style {:background-color background-color
|
||||
:touch-action "none"}}
|
||||
:touch-action "none"
|
||||
:position "relative"}}
|
||||
[:> context-menu*]
|
||||
(if (and file-loaded? page-id)
|
||||
(when (and file-loaded? page-id)
|
||||
[:> workspace-inner*
|
||||
{:page-id page-id
|
||||
:file-id file-id
|
||||
:file file
|
||||
:wglobal wglobal
|
||||
:layout layout}]
|
||||
:layout layout}])
|
||||
(when (or (not (and file-loaded? page-id))
|
||||
(and wasm-renderer-enabled? (not @first-frame-rendered?)))
|
||||
[:> workspace-loader*])]]]]]]))
|
||||
|
||||
(mf/defc workspace-page*
|
||||
|
||||
@@ -20,7 +20,13 @@
|
||||
}
|
||||
|
||||
.workspace-loader {
|
||||
grid-area: viewport;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: var(--z-index-loaders);
|
||||
background-color: var(--color-background-primary);
|
||||
}
|
||||
|
||||
.workspace-content {
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
|
||||
(mf/defc object-svg
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [object-id embed skip-children]}]
|
||||
[{:keys [object-id embed skip-children wasm]}]
|
||||
(let [objects (mf/deref ref:objects)]
|
||||
|
||||
;; Set the globa CSS to assign the page size, needed for PDF
|
||||
@@ -77,27 +77,42 @@
|
||||
(mth/ceil height) "px")}))))
|
||||
|
||||
(when objects
|
||||
[:& (mf/provider ctx/is-render?) {:value true}
|
||||
[:& render/object-svg
|
||||
{:objects objects
|
||||
:object-id object-id
|
||||
:embed embed
|
||||
:skip-children skip-children}]])))
|
||||
(if wasm
|
||||
[:& render/object-wasm
|
||||
{:objects objects
|
||||
:object-id object-id
|
||||
:embed embed
|
||||
:skip-children skip-children}]
|
||||
|
||||
(mf/defc objects-svg
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [object-ids embed skip-children]}]
|
||||
(when-let [objects (mf/deref ref:objects)]
|
||||
(for [object-id object-ids]
|
||||
(let [objects (render/adapt-objects-for-shape objects object-id)]
|
||||
[:& (mf/provider ctx/is-render?) {:value true}
|
||||
[:& render/object-svg
|
||||
{:objects objects
|
||||
:key (str object-id)
|
||||
:object-id object-id
|
||||
:embed embed
|
||||
:skip-children skip-children}]]))))
|
||||
|
||||
(mf/defc objects-svg
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [object-ids embed skip-children wasm]}]
|
||||
(when-let [objects (mf/deref ref:objects)]
|
||||
(for [object-id object-ids]
|
||||
(let [objects (render/adapt-objects-for-shape objects object-id)]
|
||||
(if wasm
|
||||
[:& render/object-wasm
|
||||
{:objects objects
|
||||
:key (str object-id)
|
||||
:object-id object-id
|
||||
:embed embed
|
||||
:skip-children skip-children}]
|
||||
|
||||
[:& (mf/provider ctx/is-render?) {:value true}
|
||||
[:& render/object-svg
|
||||
{:objects objects
|
||||
:key (str object-id)
|
||||
:object-id object-id
|
||||
:embed embed
|
||||
:skip-children skip-children}]])))))
|
||||
|
||||
(defn- fetch-objects-bundle
|
||||
[& {:keys [file-id page-id share-id object-id] :as options}]
|
||||
(ptk/reify ::fetch-objects-bundle
|
||||
@@ -136,7 +151,7 @@
|
||||
(defn- render-objects
|
||||
[params]
|
||||
(try
|
||||
(let [{:keys [file-id page-id embed share-id object-id skip-children] :as params}
|
||||
(let [{:keys [file-id page-id embed share-id object-id skip-children wasm] :as params}
|
||||
(coerce-render-objects-params params)]
|
||||
(st/emit! (fetch-objects-bundle :file-id file-id :page-id page-id :share-id share-id :object-id object-id))
|
||||
(if (uuid? object-id)
|
||||
@@ -147,7 +162,8 @@
|
||||
:share-id share-id
|
||||
:object-id object-id
|
||||
:embed embed
|
||||
:skip-children skip-children}])
|
||||
:skip-children skip-children
|
||||
:wasm wasm}])
|
||||
|
||||
(mf/html
|
||||
[:& objects-svg
|
||||
@@ -156,7 +172,8 @@
|
||||
:share-id share-id
|
||||
:object-ids (into #{} object-id)
|
||||
:embed embed
|
||||
:skip-children skip-children}])))
|
||||
:skip-children skip-children
|
||||
:wasm wasm}])))
|
||||
(catch :default cause
|
||||
(when-let [explain (-> cause ex-data ::sm/explain)]
|
||||
(js/console.log "Unexpected error")
|
||||
@@ -307,6 +324,3 @@
|
||||
(defn ^:dev/after-load after-load
|
||||
[]
|
||||
(reinit))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
["react-dom/server" :as rds]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.logging :as log]
|
||||
[app.common.math :as mth]
|
||||
@@ -21,9 +22,7 @@
|
||||
[app.common.types.text :as txt]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.render-wasm :as drw]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.render :as render]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.shapes.text]
|
||||
[app.main.worker :as mw]
|
||||
@@ -74,6 +73,9 @@
|
||||
(def noop-fn
|
||||
(constantly nil))
|
||||
|
||||
;;
|
||||
(def shape-wrapper-factory nil)
|
||||
|
||||
;; Based on app.main.render/object-svg
|
||||
(mf/defc object-svg
|
||||
{::mf/props :obj}
|
||||
@@ -81,7 +83,7 @@
|
||||
(let [objects (mf/deref refs/workspace-page-objects)
|
||||
shape-wrapper
|
||||
(mf/with-memo [shape]
|
||||
(render/shape-wrapper-factory objects))]
|
||||
(shape-wrapper-factory objects))]
|
||||
|
||||
[:svg {:version "1.1"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
@@ -915,84 +917,85 @@
|
||||
(defn set-object
|
||||
[shape]
|
||||
(perf/begin-measure "set-object")
|
||||
(let [shape (svg-filters/apply-svg-derived shape)
|
||||
id (dm/get-prop shape :id)
|
||||
type (dm/get-prop shape :type)
|
||||
(when shape
|
||||
(let [shape (svg-filters/apply-svg-derived shape)
|
||||
id (dm/get-prop shape :id)
|
||||
type (dm/get-prop shape :type)
|
||||
|
||||
parent-id (get shape :parent-id)
|
||||
masked (get shape :masked-group)
|
||||
selrect (get shape :selrect)
|
||||
constraint-h (get shape :constraints-h)
|
||||
constraint-v (get shape :constraints-v)
|
||||
clip-content (if (= type :frame)
|
||||
(not (get shape :show-content))
|
||||
false)
|
||||
rotation (get shape :rotation)
|
||||
transform (get shape :transform)
|
||||
parent-id (get shape :parent-id)
|
||||
masked (get shape :masked-group)
|
||||
selrect (get shape :selrect)
|
||||
constraint-h (get shape :constraints-h)
|
||||
constraint-v (get shape :constraints-v)
|
||||
clip-content (if (= type :frame)
|
||||
(not (get shape :show-content))
|
||||
false)
|
||||
rotation (get shape :rotation)
|
||||
transform (get shape :transform)
|
||||
|
||||
fills (get shape :fills)
|
||||
strokes (if (= type :group)
|
||||
[] (get shape :strokes))
|
||||
children (get shape :shapes)
|
||||
blend-mode (get shape :blend-mode)
|
||||
opacity (get shape :opacity)
|
||||
hidden (get shape :hidden)
|
||||
content (let [content (get shape :content)]
|
||||
(if (= type :text)
|
||||
(ensure-text-content content)
|
||||
content))
|
||||
bool-type (get shape :bool-type)
|
||||
grow-type (get shape :grow-type)
|
||||
blur (get shape :blur)
|
||||
svg-attrs (get shape :svg-attrs)
|
||||
shadows (get shape :shadow)
|
||||
corners (map #(get shape %) [:r1 :r2 :r3 :r4])]
|
||||
fills (get shape :fills)
|
||||
strokes (if (= type :group)
|
||||
[] (get shape :strokes))
|
||||
children (get shape :shapes)
|
||||
blend-mode (get shape :blend-mode)
|
||||
opacity (get shape :opacity)
|
||||
hidden (get shape :hidden)
|
||||
content (let [content (get shape :content)]
|
||||
(if (= type :text)
|
||||
(ensure-text-content content)
|
||||
content))
|
||||
bool-type (get shape :bool-type)
|
||||
grow-type (get shape :grow-type)
|
||||
blur (get shape :blur)
|
||||
svg-attrs (get shape :svg-attrs)
|
||||
shadows (get shape :shadow)
|
||||
corners (map #(get shape %) [:r1 :r2 :r3 :r4])]
|
||||
|
||||
(use-shape id)
|
||||
(set-parent-id parent-id)
|
||||
(set-shape-type type)
|
||||
(set-shape-clip-content clip-content)
|
||||
(set-shape-constraints constraint-h constraint-v)
|
||||
(use-shape id)
|
||||
(set-parent-id parent-id)
|
||||
(set-shape-type type)
|
||||
(set-shape-clip-content clip-content)
|
||||
(set-shape-constraints constraint-h constraint-v)
|
||||
|
||||
(set-shape-rotation rotation)
|
||||
(set-shape-transform transform)
|
||||
(set-shape-blend-mode blend-mode)
|
||||
(set-shape-opacity opacity)
|
||||
(set-shape-hidden hidden)
|
||||
(set-shape-children children)
|
||||
(set-shape-corners corners)
|
||||
(set-shape-blur blur)
|
||||
(when (= type :group)
|
||||
(set-masked (boolean masked)))
|
||||
(when (= type :bool)
|
||||
(set-shape-bool-type bool-type))
|
||||
(when (and (some? content)
|
||||
(or (= type :path)
|
||||
(= type :bool)))
|
||||
(set-shape-path-content content))
|
||||
(when (some? svg-attrs)
|
||||
(set-shape-svg-attrs svg-attrs))
|
||||
(when (and (some? content) (= type :svg-raw))
|
||||
(set-shape-svg-raw-content (get-static-markup shape)))
|
||||
(set-shape-shadows shadows)
|
||||
(when (= type :text)
|
||||
(set-shape-grow-type grow-type))
|
||||
(set-shape-rotation rotation)
|
||||
(set-shape-transform transform)
|
||||
(set-shape-blend-mode blend-mode)
|
||||
(set-shape-opacity opacity)
|
||||
(set-shape-hidden hidden)
|
||||
(set-shape-children children)
|
||||
(set-shape-corners corners)
|
||||
(set-shape-blur blur)
|
||||
(when (= type :group)
|
||||
(set-masked (boolean masked)))
|
||||
(when (= type :bool)
|
||||
(set-shape-bool-type bool-type))
|
||||
(when (and (some? content)
|
||||
(or (= type :path)
|
||||
(= type :bool)))
|
||||
(set-shape-path-content content))
|
||||
(when (some? svg-attrs)
|
||||
(set-shape-svg-attrs svg-attrs))
|
||||
(when (and (some? content) (= type :svg-raw))
|
||||
(set-shape-svg-raw-content (get-static-markup shape)))
|
||||
(set-shape-shadows shadows)
|
||||
(when (= type :text)
|
||||
(set-shape-grow-type grow-type))
|
||||
|
||||
(set-shape-layout shape)
|
||||
(set-shape-selrect selrect)
|
||||
(set-shape-layout shape)
|
||||
(set-shape-selrect selrect)
|
||||
|
||||
(let [pending_thumbnails (into [] (concat
|
||||
(set-shape-text-content id content)
|
||||
(set-shape-text-images id content true)
|
||||
(set-shape-fills id fills true)
|
||||
(set-shape-strokes id strokes true)))
|
||||
pending_full (into [] (concat
|
||||
(set-shape-text-images id content false)
|
||||
(set-shape-fills id fills false)
|
||||
(set-shape-strokes id strokes false)))]
|
||||
(perf/end-measure "set-object")
|
||||
{:thumbnails pending_thumbnails
|
||||
:full pending_full})))
|
||||
(let [pending_thumbnails (into [] (concat
|
||||
(set-shape-text-content id content)
|
||||
(set-shape-text-images id content true)
|
||||
(set-shape-fills id fills true)
|
||||
(set-shape-strokes id strokes true)))
|
||||
pending_full (into [] (concat
|
||||
(set-shape-text-images id content false)
|
||||
(set-shape-fills id fills false)
|
||||
(set-shape-strokes id strokes false)))]
|
||||
(perf/end-measure "set-object")
|
||||
{:thumbnails pending_thumbnails
|
||||
:full pending_full}))))
|
||||
|
||||
(defn update-text-layouts
|
||||
[shapes]
|
||||
@@ -1055,8 +1058,9 @@
|
||||
(perf/end-measure "set-objects")
|
||||
(process-pending shapes thumbnails full noop-fn
|
||||
(fn []
|
||||
(when render-callback (render-callback))
|
||||
(render-finish)
|
||||
(if render-callback
|
||||
(render-callback)
|
||||
(render-finish))
|
||||
(ug/dispatch! (ug/event "penpot:wasm:set-objects")))))))
|
||||
|
||||
(defn clear-focus-mode
|
||||
@@ -1236,7 +1240,8 @@
|
||||
(dom/prevent-default event)
|
||||
(reset! wasm/context-lost? true)
|
||||
(log/warn :hint "WebGL context lost")
|
||||
(st/emit! (drw/context-lost)))
|
||||
(ex/raise :type :webgl-context-lost
|
||||
:hint "WebGL context lost"))
|
||||
|
||||
(defn init-canvas-context
|
||||
[canvas]
|
||||
@@ -1447,6 +1452,35 @@
|
||||
|
||||
result)))
|
||||
|
||||
(defn render-shape-pixels
|
||||
[shape-id]
|
||||
(let [buffer (uuid/get-u32 shape-id)
|
||||
|
||||
offset
|
||||
(h/call wasm/internal-module "_render_shape_pixels"
|
||||
(aget buffer 0)
|
||||
(aget buffer 1)
|
||||
(aget buffer 2)
|
||||
(aget buffer 3))
|
||||
|
||||
offset-32
|
||||
(mem/->offset-32 offset)
|
||||
|
||||
heap (mem/get-heap-u8)
|
||||
heapu32 (mem/get-heap-u32)
|
||||
|
||||
length (aget heapu32 (mem/->offset-32 offset))
|
||||
width (aget heapu32 (+ (mem/->offset-32 offset) 1))
|
||||
height (aget heapu32 (+ (mem/->offset-32 offset) 2))
|
||||
|
||||
result
|
||||
(dr/read-image-bytes heap (+ offset 12) length)
|
||||
]
|
||||
|
||||
(mem/free)
|
||||
result
|
||||
))
|
||||
|
||||
(defn init-wasm-module
|
||||
[module]
|
||||
(let [default-fn (unchecked-get module "default")
|
||||
|
||||
@@ -45,6 +45,12 @@
|
||||
:center (gpt/point cx cy)
|
||||
:transform (gmt/matrix a b c d e f)}))
|
||||
|
||||
(defn read-image-bytes
|
||||
[heap offset length]
|
||||
|
||||
(.slice ^js heap offset (+ offset length))
|
||||
)
|
||||
|
||||
(defn read-position-data-entry
|
||||
[heapu32 heapf32 offset]
|
||||
(let [paragraph (aget heapu32 (+ offset 0))
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
(ns debug
|
||||
(:require
|
||||
[app.render-wasm.wasm :as wasm]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.repair :as cfr]
|
||||
@@ -456,3 +458,46 @@
|
||||
(defn ^:export network-averages
|
||||
[]
|
||||
(.log js/console (clj->js @http/network-averages)))
|
||||
|
||||
(defn ^:export export-image
|
||||
[]
|
||||
|
||||
#_(let [texture (.createTexture ^js wasm/gl-context)]
|
||||
(.bindTexture ^js wasm/gl-context (.-TEXTURE_2D ^js wasm/gl-context) texture)
|
||||
|
||||
(.texImage2D
|
||||
^js wasm/gl-context
|
||||
(.-TEXTURE_2D ^js wasm/gl-context)
|
||||
0
|
||||
(.-RGBA ^js wasm/gl-context)
|
||||
800
|
||||
600
|
||||
0
|
||||
(.-RGBA ^js wasm/gl-context)
|
||||
(.-UNSIGNED_BYTE ^js wasm/gl-context)
|
||||
nil)
|
||||
|
||||
;;(.log js/console )
|
||||
(let [texture-id (wasm.api/get-texture-id-for-gl-object texture)
|
||||
objects (dsh/lookup-page-objects @st/state)
|
||||
shape-id (->> (get-selected @st/state) first)]
|
||||
(wasm.api/render-to-texture shape-id texture-id))
|
||||
)
|
||||
|
||||
(let [objects (dsh/lookup-page-objects @st/state)
|
||||
shape-id (->> (get-selected @st/state) first)
|
||||
bytes (wasm.api/render-shape-pixels shape-id)
|
||||
|
||||
_ (.log js/console (clj->js bytes))
|
||||
|
||||
blob (js/Blob. #js [bytes] #js {:type "image/png"})
|
||||
url (.createObjectURL js/URL blob)
|
||||
|
||||
a (.createElement js/document "a")]
|
||||
(set! (.-href a) url)
|
||||
(set! (.-download a) "export.png")
|
||||
(.click a)
|
||||
(.revokeObjectURL js/URL url)
|
||||
|
||||
(.log js/console bytes)
|
||||
))
|
||||
|
||||
@@ -242,7 +242,6 @@ export class SelectionController extends EventTarget {
|
||||
continue;
|
||||
}
|
||||
let styleValue = element.style.getPropertyValue(styleName);
|
||||
|
||||
if (styleName === "font-family") {
|
||||
styleValue = sanitizeFontFamily(styleValue);
|
||||
}
|
||||
@@ -277,22 +276,29 @@ export class SelectionController extends EventTarget {
|
||||
this.#applyDefaultStylesToCurrentStyle();
|
||||
const root = startNode.parentElement.parentElement.parentElement;
|
||||
this.#applyStylesFromElementToCurrentStyle(root);
|
||||
// FIXME: I don't like this approximation. Having to iterate nodes twice
|
||||
// is bad for performance. I think we need another way of "computing"
|
||||
// the cascade.
|
||||
for (const textNode of this.#textNodeIterator.iterateFrom(
|
||||
startNode,
|
||||
endNode,
|
||||
)) {
|
||||
const paragraph = textNode.parentElement.parentElement;
|
||||
if (startNode === endNode) {
|
||||
const paragraph = startNode.parentElement.parentElement;
|
||||
this.#applyStylesFromElementToCurrentStyle(paragraph);
|
||||
}
|
||||
for (const textNode of this.#textNodeIterator.iterateFrom(
|
||||
startNode,
|
||||
endNode,
|
||||
)) {
|
||||
const textSpan = textNode.parentElement;
|
||||
this.#mergeStylesFromElementToCurrentStyle(textSpan);
|
||||
const textSpan = startNode.parentElement;
|
||||
this.#applyStylesFromElementToCurrentStyle(textSpan);
|
||||
} else {
|
||||
// FIXME: I don't like this approximation. Having to iterate nodes twice
|
||||
// is bad for performance. I think we need another way of "computing"
|
||||
// the cascade.
|
||||
for (const textNode of this.#textNodeIterator.iterateFrom(
|
||||
startNode,
|
||||
endNode,
|
||||
)) {
|
||||
const paragraph = textNode.parentElement.parentElement;
|
||||
this.#applyStylesFromElementToCurrentStyle(paragraph);
|
||||
}
|
||||
for (const textNode of this.#textNodeIterator.iterateFrom(
|
||||
startNode,
|
||||
endNode,
|
||||
)) {
|
||||
const textSpan = textNode.parentElement;
|
||||
this.#mergeStylesFromElementToCurrentStyle(textSpan);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -2521,6 +2521,9 @@ msgstr "Release notes"
|
||||
msgid "labels.reload-file"
|
||||
msgstr "Reload file"
|
||||
|
||||
msgid "labels.reload-page"
|
||||
msgstr "Reload page"
|
||||
|
||||
#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs
|
||||
#, unused
|
||||
msgid "labels.remove"
|
||||
@@ -8475,6 +8478,12 @@ msgstr "Recent"
|
||||
msgid "labels.deleted"
|
||||
msgstr "Deleted"
|
||||
|
||||
msgid "labels.webgl-context-lost.main-message"
|
||||
msgstr "Oops! The canvas context was lost"
|
||||
|
||||
msgid "labels.webgl-context-lost.desc-message"
|
||||
msgstr "WebGL has stopped working. Please reload the page to reset it"
|
||||
|
||||
msgid "dashboard.restore-all-deleted-button"
|
||||
msgstr "Restore All"
|
||||
|
||||
|
||||
@@ -2502,6 +2502,9 @@ msgstr "Notas de versión"
|
||||
msgid "labels.reload-file"
|
||||
msgstr "Recargar archivo"
|
||||
|
||||
msgid "labels.reload-page"
|
||||
msgstr "Recargar página"
|
||||
|
||||
#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs
|
||||
#, unused
|
||||
msgid "labels.remove"
|
||||
@@ -8328,6 +8331,12 @@ msgstr "Recientes"
|
||||
msgid "labels.deleted"
|
||||
msgstr "Eliminados"
|
||||
|
||||
msgid "labels.webgl-context-lost.main-message"
|
||||
msgstr "Ups! Se ha perdido el contexto del canvas"
|
||||
|
||||
msgid "labels.webgl-context-lost.desc-message"
|
||||
msgstr "WebGL ha dejado de funcionar. Por favor, recarga la página para restaurarlo"
|
||||
|
||||
msgid "dashboard.restore-all-deleted-button"
|
||||
msgstr "Restaurar todo"
|
||||
|
||||
|
||||
@@ -738,6 +738,28 @@ pub extern "C" fn end_temp_objects() {
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn render_shape_pixels(a: u32, b: u32, c: u32, d: u32) -> *mut u8 {
|
||||
let id = uuid_from_u32_quartet(a, b, c, d);
|
||||
|
||||
with_state_mut!(state, {
|
||||
let (data, width, height) = state.render_shape_pixels(&id, performance::get_time())
|
||||
.expect("Cannot render into texture");
|
||||
|
||||
let len = data.len() as u32;
|
||||
println!(">{len} {width} {height}");
|
||||
let mut buf = Vec::with_capacity(4 + data.len());
|
||||
buf.extend_from_slice(&len.to_le_bytes());
|
||||
buf.extend_from_slice(&width.to_le_bytes());
|
||||
buf.extend_from_slice(&height.to_le_bytes());
|
||||
buf.extend_from_slice(&data);
|
||||
|
||||
println!("{len:?}");
|
||||
mem::write_bytes(buf)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
init_gl!();
|
||||
|
||||
@@ -10,7 +10,6 @@ mod shadows;
|
||||
mod strokes;
|
||||
mod surfaces;
|
||||
pub mod text;
|
||||
|
||||
mod ui;
|
||||
|
||||
use skia_safe::{self as skia, Matrix, RRect, Rect};
|
||||
@@ -18,6 +17,7 @@ use std::borrow::Cow;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use gpu_state::GpuState;
|
||||
|
||||
use options::RenderOptions;
|
||||
pub use surfaces::{SurfaceId, Surfaces};
|
||||
|
||||
@@ -41,6 +41,7 @@ const NODE_BATCH_THRESHOLD: i32 = 3;
|
||||
|
||||
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NodeRenderState {
|
||||
pub id: Uuid,
|
||||
// We use this bool to keep that we've traversed all the children inside this node.
|
||||
@@ -53,6 +54,25 @@ pub struct NodeRenderState {
|
||||
mask: bool,
|
||||
}
|
||||
|
||||
/// Get simplified children of a container, flattening nested flattened containers
|
||||
fn get_simplified_children<'a>(tree: ShapesPoolRef<'a>, shape: &'a Shape) -> Vec<Uuid> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
for child_id in shape.children_ids_iter(false) {
|
||||
if let Some(child) = tree.get(child_id) {
|
||||
if child.can_flatten() {
|
||||
// Child is flattened: recursively get its simplified children
|
||||
result.extend(get_simplified_children(tree, child));
|
||||
} else {
|
||||
// Child is not flattened: add it directly
|
||||
result.push(*child_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
impl NodeRenderState {
|
||||
pub fn is_root(&self) -> bool {
|
||||
self.id.is_nil()
|
||||
@@ -276,6 +296,10 @@ pub(crate) struct RenderState {
|
||||
/// where we must render shapes without inheriting ancestor layer blurs. Toggle it through
|
||||
/// `with_nested_blurs_suppressed` to ensure it's always restored.
|
||||
pub ignore_nested_blurs: bool,
|
||||
/// Cached root_ids and root_ids_map to avoid recalculating them every frame
|
||||
/// These are invalidated when the tree structure changes
|
||||
cached_root_ids: Option<Vec<Uuid>>,
|
||||
cached_root_ids_map: Option<std::collections::HashMap<Uuid, usize>>,
|
||||
}
|
||||
|
||||
pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
|
||||
@@ -348,6 +372,8 @@ impl RenderState {
|
||||
focus_mode: FocusMode::new(),
|
||||
touched_ids: HashSet::default(),
|
||||
ignore_nested_blurs: false,
|
||||
cached_root_ids: None,
|
||||
cached_root_ids_map: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,12 +424,7 @@ impl RenderState {
|
||||
}
|
||||
|
||||
fn frame_clip_layer_blur(shape: &Shape) -> Option<Blur> {
|
||||
match shape.shape_type {
|
||||
Type::Frame(_) if shape.clip() => shape.blur.filter(|blur| {
|
||||
!blur.hidden && blur.blur_type == BlurType::LayerBlur && blur.value > 0.
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
shape.frame_clip_layer_blur()
|
||||
}
|
||||
|
||||
/// Runs `f` with `ignore_nested_blurs` temporarily forced to `true`.
|
||||
@@ -516,43 +537,64 @@ impl RenderState {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>) {
|
||||
pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>, target: SurfaceId) {
|
||||
performance::begin_measure!("apply_drawing_to_render_canvas");
|
||||
|
||||
let paint = skia::Paint::default();
|
||||
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::TextDropShadows, SurfaceId::Current, Some(&paint));
|
||||
// Only draw surfaces that have content (dirty flag optimization)
|
||||
if self.surfaces.is_dirty(SurfaceId::TextDropShadows) {
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::TextDropShadows, target, Some(&paint));
|
||||
}
|
||||
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint));
|
||||
if self.surfaces.is_dirty(SurfaceId::Fills) {
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::Fills, target, Some(&paint));
|
||||
}
|
||||
|
||||
let mut render_overlay_below_strokes = false;
|
||||
if let Some(shape) = shape {
|
||||
render_overlay_below_strokes = shape.has_fills();
|
||||
}
|
||||
|
||||
if render_overlay_below_strokes {
|
||||
if render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) {
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
|
||||
.draw_into(SurfaceId::InnerShadows, target, Some(&paint));
|
||||
}
|
||||
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::Strokes, SurfaceId::Current, Some(&paint));
|
||||
|
||||
if !render_overlay_below_strokes {
|
||||
if self.surfaces.is_dirty(SurfaceId::Strokes) {
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
|
||||
.draw_into(SurfaceId::Strokes, target, Some(&paint));
|
||||
}
|
||||
|
||||
let surface_ids = SurfaceId::Strokes as u32
|
||||
| SurfaceId::Fills as u32
|
||||
| SurfaceId::InnerShadows as u32
|
||||
| SurfaceId::TextDropShadows as u32;
|
||||
if !render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) {
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::InnerShadows, target, Some(&paint));
|
||||
}
|
||||
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().clear(skia::Color::TRANSPARENT);
|
||||
});
|
||||
// Build mask of dirty surfaces that need clearing
|
||||
let mut dirty_surfaces_to_clear = 0u32;
|
||||
if self.surfaces.is_dirty(SurfaceId::Strokes) {
|
||||
dirty_surfaces_to_clear |= SurfaceId::Strokes as u32;
|
||||
}
|
||||
if self.surfaces.is_dirty(SurfaceId::Fills) {
|
||||
dirty_surfaces_to_clear |= SurfaceId::Fills as u32;
|
||||
}
|
||||
if self.surfaces.is_dirty(SurfaceId::InnerShadows) {
|
||||
dirty_surfaces_to_clear |= SurfaceId::InnerShadows as u32;
|
||||
}
|
||||
if self.surfaces.is_dirty(SurfaceId::TextDropShadows) {
|
||||
dirty_surfaces_to_clear |= SurfaceId::TextDropShadows as u32;
|
||||
}
|
||||
|
||||
if dirty_surfaces_to_clear != 0 {
|
||||
self.surfaces.apply_mut(dirty_surfaces_to_clear, |s| {
|
||||
s.canvas().clear(skia::Color::TRANSPARENT);
|
||||
});
|
||||
// Clear dirty flags for surfaces we just cleared
|
||||
self.surfaces.clear_dirty(dirty_surfaces_to_clear);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_focus_mode(&mut self) {
|
||||
@@ -601,13 +643,22 @@ impl RenderState {
|
||||
offset: Option<(f32, f32)>,
|
||||
parent_shadows: Option<Vec<skia_safe::Paint>>,
|
||||
) {
|
||||
println!(" >Shape:{:?}", shape.id);
|
||||
let surface_ids = fills_surface_id as u32
|
||||
| strokes_surface_id as u32
|
||||
| innershadows_surface_id as u32
|
||||
| text_drop_shadows_surface_id as u32;
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().save();
|
||||
});
|
||||
|
||||
// Only save canvas state if we have clipping or transforms
|
||||
// For simple shapes without clipping, skip expensive save/restore
|
||||
let needs_save =
|
||||
clip_bounds.is_some() || offset.is_some() || !shape.transform.is_identity();
|
||||
|
||||
if needs_save {
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().save();
|
||||
});
|
||||
}
|
||||
|
||||
let antialias = shape.should_use_antialias(self.get_scale());
|
||||
|
||||
@@ -683,16 +734,18 @@ impl RenderState {
|
||||
matrix.pre_concat(&svg_transform);
|
||||
}
|
||||
|
||||
self.surfaces.canvas(fills_surface_id).concat(&matrix);
|
||||
self.surfaces
|
||||
.canvas_and_mark_dirty(fills_surface_id)
|
||||
.concat(&matrix);
|
||||
|
||||
if let Some(svg) = shape.svg.as_ref() {
|
||||
svg.render(self.surfaces.canvas(fills_surface_id))
|
||||
svg.render(self.surfaces.canvas_and_mark_dirty(fills_surface_id));
|
||||
} else {
|
||||
let font_manager = skia::FontMgr::from(self.fonts().font_provider().clone());
|
||||
let dom_result = skia::svg::Dom::from_str(&sr.content, font_manager);
|
||||
match dom_result {
|
||||
Ok(dom) => {
|
||||
dom.render(self.surfaces.canvas(fills_surface_id));
|
||||
dom.render(self.surfaces.canvas_and_mark_dirty(fills_surface_id));
|
||||
shape.to_mut().set_svg(dom);
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -906,11 +959,15 @@ impl RenderState {
|
||||
}
|
||||
|
||||
if apply_to_current_surface {
|
||||
self.apply_drawing_to_render_canvas(Some(&shape));
|
||||
self.apply_drawing_to_render_canvas(Some(&shape), SurfaceId::Current);
|
||||
}
|
||||
|
||||
// Only restore if we saved (optimization for simple shapes)
|
||||
if needs_save {
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().restore();
|
||||
});
|
||||
}
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().restore();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn update_render_context(&mut self, tile: tiles::Tile) {
|
||||
@@ -1031,7 +1088,8 @@ impl RenderState {
|
||||
// reorder by distance to the center.
|
||||
self.current_tile = None;
|
||||
self.render_in_progress = true;
|
||||
self.apply_drawing_to_render_canvas(None);
|
||||
|
||||
self.apply_drawing_to_render_canvas(None, SurfaceId::Current);
|
||||
|
||||
if sync_render {
|
||||
self.render_shape_tree_sync(base_object, tree, timestamp)?;
|
||||
@@ -1086,6 +1144,48 @@ impl RenderState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render_shape_pixels(
|
||||
&mut self,
|
||||
id: &Uuid,
|
||||
tree: ShapesPoolRef,
|
||||
timestamp: i32,
|
||||
) -> Result<(Vec<u8>, i32, i32), String> {
|
||||
|
||||
// let width = 800;
|
||||
// let height = 600;
|
||||
|
||||
if tree.len() != 0 {
|
||||
// self.render_shape_tree_partial(Some(id), tree, timestamp, false)?;
|
||||
|
||||
let shape = tree.get(id).unwrap();
|
||||
let scale = 1.0;
|
||||
self.surfaces.update_render_context(shape.extrect(tree, scale), scale);
|
||||
|
||||
self.pending_nodes.push(NodeRenderState {
|
||||
id: *id,
|
||||
visited_children: false,
|
||||
clip_bounds: None,
|
||||
visited_mask: false,
|
||||
mask: false,
|
||||
});
|
||||
self.render_shape_tree_partial_uncached(tree, timestamp, false, true)?;
|
||||
} else {
|
||||
println!("Empty tree");
|
||||
}
|
||||
|
||||
self.surfaces.flush_and_submit(&mut self.gpu_state, SurfaceId::Export);
|
||||
|
||||
let image = self.surfaces.snapshot(SurfaceId::Export);
|
||||
let data = image.encode(
|
||||
&mut self.gpu_state.context,
|
||||
skia::EncodedImageFormat::PNG,
|
||||
100
|
||||
).expect("PNG encode failed");
|
||||
let skia::ISize { width, height } = image.dimensions();
|
||||
|
||||
Ok((data.as_bytes().to_vec(), width, height))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn should_stop_rendering(&self, iteration: i32, timestamp: i32) -> bool {
|
||||
iteration % NODE_BATCH_THRESHOLD == 0
|
||||
@@ -1117,18 +1217,6 @@ impl RenderState {
|
||||
self.nested_fills.push(Vec::new());
|
||||
}
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_blend_mode(element.blend_mode().into());
|
||||
paint.set_alpha_f(element.opacity());
|
||||
|
||||
if let Some(frame_blur) = Self::frame_clip_layer_blur(element) {
|
||||
let scale = self.get_scale();
|
||||
let sigma = frame_blur.value * scale;
|
||||
if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) {
|
||||
paint.set_image_filter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
// When we're rendering the mask shape we need to set a special blend mode
|
||||
// called 'destination-in' that keeps the drawn content within the mask.
|
||||
// @see https://skia.org/docs/user/api/skblendmode_overview/
|
||||
@@ -1141,16 +1229,40 @@ impl RenderState {
|
||||
.save_layer(&mask_rec);
|
||||
}
|
||||
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::Current)
|
||||
.save_layer(&layer_rec);
|
||||
// Only create save_layer if actually needed
|
||||
// For simple shapes with default opacity and blend mode, skip expensive save_layer
|
||||
// Groups with masks need a layer to properly handle the mask rendering
|
||||
let needs_layer = element.needs_layer();
|
||||
|
||||
if needs_layer {
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_blend_mode(element.blend_mode().into());
|
||||
paint.set_alpha_f(element.opacity());
|
||||
|
||||
if let Some(frame_blur) = Self::frame_clip_layer_blur(element) {
|
||||
let scale = self.get_scale();
|
||||
let sigma = frame_blur.value * scale;
|
||||
if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) {
|
||||
paint.set_image_filter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::Current)
|
||||
.save_layer(&layer_rec);
|
||||
}
|
||||
|
||||
self.focus_mode.enter(&element.id);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn render_shape_exit(&mut self, element: &Shape, visited_mask: bool) {
|
||||
pub fn render_shape_exit(
|
||||
&mut self,
|
||||
element: &Shape,
|
||||
visited_mask: bool,
|
||||
clip_bounds: Option<ClipStack>,
|
||||
) {
|
||||
if visited_mask {
|
||||
// Because masked groups needs two rendering passes (first drawing
|
||||
// the content and then drawing the mask), we need to do an
|
||||
@@ -1204,9 +1316,10 @@ impl RenderState {
|
||||
element_strokes.to_mut().clear_fills();
|
||||
element_strokes.to_mut().clear_shadows();
|
||||
element_strokes.to_mut().clip_content = false;
|
||||
println!(">in clip render");
|
||||
self.render_shape(
|
||||
&element_strokes,
|
||||
None,
|
||||
clip_bounds,
|
||||
SurfaceId::Fills,
|
||||
SurfaceId::Strokes,
|
||||
SurfaceId::InnerShadows,
|
||||
@@ -1217,7 +1330,14 @@ impl RenderState {
|
||||
);
|
||||
}
|
||||
|
||||
self.surfaces.canvas(SurfaceId::Current).restore();
|
||||
// Only restore if we created a layer (optimization for simple shapes)
|
||||
// Groups with masks need restore to properly handle the mask rendering
|
||||
let needs_layer = element.needs_layer();
|
||||
|
||||
if needs_layer {
|
||||
self.surfaces.canvas(SurfaceId::Current).restore();
|
||||
}
|
||||
|
||||
self.focus_mode.exit(&element.id);
|
||||
}
|
||||
|
||||
@@ -1366,6 +1486,7 @@ impl RenderState {
|
||||
}
|
||||
|
||||
state.with_nested_blurs_suppressed(|state| {
|
||||
println!(">render drop black shadow");
|
||||
state.render_shape(
|
||||
&plain_shape,
|
||||
clip_bounds,
|
||||
@@ -1433,11 +1554,18 @@ impl RenderState {
|
||||
tree: ShapesPoolRef,
|
||||
timestamp: i32,
|
||||
allow_stop: bool,
|
||||
export: bool,
|
||||
) -> Result<(bool, bool), String> {
|
||||
let mut iteration = 0;
|
||||
let mut is_empty = true;
|
||||
|
||||
let mut target_surface = SurfaceId::Current;
|
||||
if export {
|
||||
target_surface = SurfaceId::Export;
|
||||
}
|
||||
|
||||
while let Some(node_render_state) = self.pending_nodes.pop() {
|
||||
println!("Node: {node_render_state:?}");
|
||||
let node_id = node_render_state.id;
|
||||
let visited_children = node_render_state.visited_children;
|
||||
let visited_mask = node_render_state.visited_mask;
|
||||
@@ -1457,18 +1585,48 @@ impl RenderState {
|
||||
}
|
||||
|
||||
if visited_children {
|
||||
self.render_shape_exit(element, visited_mask);
|
||||
// Skip render_shape_exit for flattened containers
|
||||
if !element.can_flatten() {
|
||||
self.render_shape_exit(element, visited_mask, clip_bounds);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if !node_render_state.is_root() {
|
||||
let transformed_element: Cow<Shape> = Cow::Borrowed(element);
|
||||
let scale = self.get_scale();
|
||||
let extrect = transformed_element.extrect(tree, scale);
|
||||
|
||||
let is_visible = extrect.intersects(self.render_area)
|
||||
&& !transformed_element.hidden
|
||||
&& !transformed_element.visually_insignificant(scale, tree);
|
||||
// Aggressive early exit: check hidden first (fastest check)
|
||||
if transformed_element.hidden {
|
||||
continue;
|
||||
}
|
||||
|
||||
// For frames and groups, we must use extrect because they can have nested content
|
||||
// that extends beyond their selrect. Using selrect for early exit would incorrectly
|
||||
// skip frames/groups that have nested content in the current tile.
|
||||
let is_container = matches!(
|
||||
transformed_element.shape_type,
|
||||
crate::shapes::Type::Frame(_) | crate::shapes::Type::Group(_)
|
||||
);
|
||||
|
||||
let scale = self.get_scale();
|
||||
let has_effects = transformed_element.has_effects_that_extend_bounds();
|
||||
|
||||
let is_visible = export || if is_container {
|
||||
// Containers (frames/groups) must always use extrect to include nested content
|
||||
let extrect = transformed_element.extrect(tree, scale);
|
||||
extrect.intersects(self.render_area)
|
||||
&& !transformed_element.visually_insignificant(scale, tree)
|
||||
} else if !has_effects {
|
||||
// Simple shape: selrect check is sufficient, skip expensive extrect
|
||||
let selrect = transformed_element.selrect();
|
||||
selrect.intersects(self.render_area)
|
||||
&& !transformed_element.visually_insignificant(scale, tree)
|
||||
} else {
|
||||
// Shape with effects: need extrect for accurate bounds
|
||||
let extrect = transformed_element.extrect(tree, scale);
|
||||
extrect.intersects(self.render_area)
|
||||
&& !transformed_element.visually_insignificant(scale, tree)
|
||||
};
|
||||
|
||||
if self.options.is_debug_visible() {
|
||||
let shape_extrect_bounds =
|
||||
@@ -1477,11 +1635,17 @@ impl RenderState {
|
||||
}
|
||||
|
||||
if !is_visible {
|
||||
println!(">NOT VISIBLE");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
self.render_shape_enter(element, mask);
|
||||
// Skip render_shape_enter/exit for flattened containers
|
||||
// If a container was flattened, it doesn't affect children visually, so we skip
|
||||
// the expensive enter/exit operations and process children directly
|
||||
if !element.can_flatten() {
|
||||
self.render_shape_enter(element, mask);
|
||||
}
|
||||
|
||||
if !node_render_state.is_root() && self.focus_mode.is_active() {
|
||||
let scale: f32 = self.get_scale();
|
||||
@@ -1515,6 +1679,8 @@ impl RenderState {
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let element_extrect = element.extrect(tree, scale);
|
||||
|
||||
for shadow in element.drop_shadows_visible() {
|
||||
let paint = skia::Paint::default();
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
@@ -1526,7 +1692,7 @@ impl RenderState {
|
||||
// First pass: Render shadow in black to establish alpha mask
|
||||
self.render_drop_black_shadow(
|
||||
element,
|
||||
&element.extrect(tree, scale),
|
||||
&element_extrect,
|
||||
shadow,
|
||||
clip_bounds.clone(),
|
||||
scale,
|
||||
@@ -1546,9 +1712,10 @@ impl RenderState {
|
||||
.get_nested_shadow_clip_bounds(element, shadow);
|
||||
|
||||
if !matches!(shadow_shape.shape_type, Type::Text(_)) {
|
||||
let shadow_extrect = shadow_shape.extrect(tree, scale);
|
||||
self.render_drop_black_shadow(
|
||||
shadow_shape,
|
||||
&shadow_shape.extrect(tree, scale),
|
||||
&shadow_extrect,
|
||||
shadow,
|
||||
clip_bounds,
|
||||
scale,
|
||||
@@ -1585,6 +1752,7 @@ impl RenderState {
|
||||
new_shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
|
||||
|
||||
self.with_nested_blurs_suppressed(|state| {
|
||||
println!(">render_shape in if focus");
|
||||
state.render_shape(
|
||||
shadow_shape,
|
||||
clip_bounds,
|
||||
@@ -1619,7 +1787,7 @@ impl RenderState {
|
||||
if let Some(clips) = clip_bounds.as_ref() {
|
||||
let antialias = element.should_use_antialias(scale);
|
||||
|
||||
self.surfaces.canvas(SurfaceId::Current).save();
|
||||
self.surfaces.canvas(target_surface).save();
|
||||
for (bounds, corners, transform) in clips.iter() {
|
||||
let mut total_matrix = Matrix::new_identity();
|
||||
total_matrix.pre_scale((scale, scale), None);
|
||||
@@ -1627,18 +1795,18 @@ impl RenderState {
|
||||
total_matrix.pre_concat(transform);
|
||||
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::Current)
|
||||
.canvas(target_surface)
|
||||
.concat(&total_matrix);
|
||||
|
||||
if let Some(corners) = corners {
|
||||
let rrect = RRect::new_rect_radii(*bounds, corners);
|
||||
self.surfaces.canvas(SurfaceId::Current).clip_rrect(
|
||||
self.surfaces.canvas(target_surface).clip_rrect(
|
||||
rrect,
|
||||
skia::ClipOp::Intersect,
|
||||
antialias,
|
||||
);
|
||||
} else {
|
||||
self.surfaces.canvas(SurfaceId::Current).clip_rect(
|
||||
self.surfaces.canvas(target_surface).clip_rect(
|
||||
*bounds,
|
||||
skia::ClipOp::Intersect,
|
||||
antialias,
|
||||
@@ -1646,23 +1814,24 @@ impl RenderState {
|
||||
}
|
||||
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::Current)
|
||||
.canvas(target_surface)
|
||||
.concat(&total_matrix.invert().unwrap_or_default());
|
||||
}
|
||||
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::DropShadows, SurfaceId::Current, None);
|
||||
.draw_into(SurfaceId::DropShadows, target_surface, None);
|
||||
|
||||
self.surfaces.canvas(SurfaceId::Current).restore();
|
||||
self.surfaces.canvas(target_surface).restore();
|
||||
} else {
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::DropShadows, SurfaceId::Current, None);
|
||||
.draw_into(SurfaceId::DropShadows, target_surface, None);
|
||||
}
|
||||
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.clear(skia::Color::TRANSPARENT);
|
||||
|
||||
println!(">>>> render_shape segundo focus");
|
||||
self.render_shape(
|
||||
element,
|
||||
clip_bounds.clone(),
|
||||
@@ -1679,17 +1848,22 @@ impl RenderState {
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.clear(skia::Color::TRANSPARENT);
|
||||
} else if visited_children {
|
||||
self.apply_drawing_to_render_canvas(Some(element));
|
||||
println!(">>>>apply_drawing_to_render_canvas");
|
||||
self.apply_drawing_to_render_canvas(Some(element), target_surface);
|
||||
}
|
||||
|
||||
match element.shape_type {
|
||||
Type::Frame(_) if Self::frame_clip_layer_blur(element).is_some() => {
|
||||
self.nested_blurs.push(None);
|
||||
// Skip nested state updates for flattened containers
|
||||
// Flattened containers don't affect children, so we don't need to track their state
|
||||
if !element.can_flatten() {
|
||||
match element.shape_type {
|
||||
Type::Frame(_) if Self::frame_clip_layer_blur(element).is_some() => {
|
||||
self.nested_blurs.push(None);
|
||||
}
|
||||
Type::Frame(_) | Type::Group(_) => {
|
||||
self.nested_blurs.push(element.blur);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Type::Frame(_) | Type::Group(_) => {
|
||||
self.nested_blurs.push(element.blur);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Set the node as visited_children before processing children
|
||||
@@ -1702,26 +1876,38 @@ impl RenderState {
|
||||
});
|
||||
|
||||
if element.is_recursive() {
|
||||
println!("!adding children");
|
||||
let children_clip_bounds =
|
||||
node_render_state.get_children_clip_bounds(element, None);
|
||||
let mut children_ids: Vec<_> = element.children_ids_iter(false).collect();
|
||||
|
||||
let children_ids: Vec<_> = if element.can_flatten() {
|
||||
// Container was flattened: get simplified children (which skip this level)
|
||||
get_simplified_children(tree, element)
|
||||
} else {
|
||||
// Container not flattened: use original children
|
||||
element.children_ids_iter(false).copied().collect()
|
||||
};
|
||||
|
||||
// Z-index ordering on Layouts
|
||||
if element.has_layout() {
|
||||
let children_ids = if element.has_layout() {
|
||||
let mut ids = children_ids;
|
||||
if element.is_flex() && !element.is_flex_reverse() {
|
||||
children_ids.reverse();
|
||||
ids.reverse();
|
||||
}
|
||||
|
||||
children_ids.sort_by(|id1, id2| {
|
||||
ids.sort_by(|id1, id2| {
|
||||
let z1 = tree.get(id1).map(|s| s.z_index()).unwrap_or(0);
|
||||
let z2 = tree.get(id2).map(|s| s.z_index()).unwrap_or(0);
|
||||
z2.cmp(&z1)
|
||||
});
|
||||
}
|
||||
ids
|
||||
} else {
|
||||
children_ids
|
||||
};
|
||||
|
||||
for child_id in children_ids.iter() {
|
||||
self.pending_nodes.push(NodeRenderState {
|
||||
id: **child_id,
|
||||
id: *child_id,
|
||||
visited_children: false,
|
||||
clip_bounds: children_clip_bounds.clone(),
|
||||
visited_mask: false,
|
||||
@@ -1732,6 +1918,7 @@ impl RenderState {
|
||||
|
||||
// We try to avoid doing too many calls to get_time
|
||||
if allow_stop && self.should_stop_rendering(iteration, timestamp) {
|
||||
println!("STOP");
|
||||
return Ok((is_empty, true));
|
||||
}
|
||||
iteration += 1;
|
||||
@@ -1771,7 +1958,7 @@ impl RenderState {
|
||||
} else {
|
||||
performance::begin_measure!("render_shape_tree::uncached");
|
||||
let (is_empty, early_return) =
|
||||
self.render_shape_tree_partial_uncached(tree, timestamp, allow_stop)?;
|
||||
self.render_shape_tree_partial_uncached(tree, timestamp, allow_stop, false)?;
|
||||
|
||||
if early_return {
|
||||
return Ok(());
|
||||
@@ -1803,14 +1990,39 @@ impl RenderState {
|
||||
.canvas(SurfaceId::Current)
|
||||
.clear(self.background_color);
|
||||
|
||||
let root_ids = {
|
||||
if let Some(shape_id) = base_object {
|
||||
// Get or compute root_ids and root_ids_map (cached to avoid recalculation every frame)
|
||||
let root_ids_map = {
|
||||
let root_ids = if let Some(shape_id) = base_object {
|
||||
vec![*shape_id]
|
||||
} else {
|
||||
let Some(root) = tree.get(&Uuid::nil()) else {
|
||||
return Err(String::from("Root shape not found"));
|
||||
};
|
||||
root.children_ids(false)
|
||||
};
|
||||
|
||||
// Check if cache is valid (same root_ids)
|
||||
let cache_valid = self
|
||||
.cached_root_ids
|
||||
.as_ref()
|
||||
.map(|cached| cached.as_slice() == root_ids.as_slice())
|
||||
.unwrap_or(false);
|
||||
|
||||
if cache_valid {
|
||||
// Use cached map
|
||||
self.cached_root_ids_map.as_ref().unwrap().clone()
|
||||
} else {
|
||||
// Recompute and cache
|
||||
let root_ids_map: std::collections::HashMap<Uuid, usize> = root_ids
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, id)| (*id, i))
|
||||
.collect();
|
||||
|
||||
self.cached_root_ids = Some(root_ids.clone());
|
||||
self.cached_root_ids_map = Some(root_ids_map.clone());
|
||||
|
||||
root_ids_map
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1821,12 +2033,6 @@ impl RenderState {
|
||||
|
||||
if !self.surfaces.has_cached_tile_surface(next_tile) {
|
||||
if let Some(ids) = self.tiles.get_shapes_at(next_tile) {
|
||||
let root_ids_map: std::collections::HashMap<Uuid, usize> = root_ids
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, id)| (*id, i))
|
||||
.collect();
|
||||
|
||||
// We only need first level shapes
|
||||
let mut valid_ids: Vec<Uuid> = ids
|
||||
.iter()
|
||||
|
||||
@@ -18,7 +18,7 @@ fn draw_image_fill(
|
||||
}
|
||||
|
||||
let size = image.unwrap().dimensions();
|
||||
let canvas = render_state.surfaces.canvas(surface_id);
|
||||
let canvas = render_state.surfaces.canvas_and_mark_dirty(surface_id);
|
||||
let container = &shape.selrect;
|
||||
let path_transform = shape.to_path_transform();
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ where
|
||||
F: FnOnce(&mut RenderState, SurfaceId),
|
||||
{
|
||||
if let Some((image, scale)) = render_into_filter_surface(render_state, bounds, draw_fn) {
|
||||
let canvas = render_state.surfaces.canvas(target_surface);
|
||||
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
|
||||
|
||||
// If we scaled down, we need to scale the source rect and adjust the destination
|
||||
if scale < 1.0 {
|
||||
|
||||
@@ -104,4 +104,38 @@ impl GpuState {
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn create_surface_from_texture(
|
||||
&mut self,
|
||||
width: i32,
|
||||
height: i32,
|
||||
texture_id: u32
|
||||
) -> skia::Surface {
|
||||
let texture_info = TextureInfo {
|
||||
target: gl::TEXTURE_2D,
|
||||
id: texture_id,
|
||||
format: gl::RGBA8,
|
||||
protected: skia::gpu::Protected::No,
|
||||
};
|
||||
|
||||
let backend_texture = unsafe{
|
||||
gpu::backend_textures::make_gl(
|
||||
(width, height),
|
||||
gpu::Mipmapped::No,
|
||||
texture_info,
|
||||
String::from("export_texture"))
|
||||
};
|
||||
|
||||
gpu::surfaces::wrap_backend_texture(
|
||||
&mut self.context,
|
||||
&backend_texture,
|
||||
gpu::SurfaceOrigin::BottomLeft,
|
||||
None,
|
||||
skia::ColorType::RGBA8888,
|
||||
None,
|
||||
None,
|
||||
).unwrap()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ pub fn render_text_shadows(
|
||||
|
||||
let canvas = render_state
|
||||
.surfaces
|
||||
.canvas(surface_id.unwrap_or(SurfaceId::TextDropShadows));
|
||||
.canvas_and_mark_dirty(surface_id.unwrap_or(SurfaceId::TextDropShadows));
|
||||
|
||||
for shadow in shadows {
|
||||
let shadow_layer = SaveLayerRec::default().paint(shadow);
|
||||
|
||||
@@ -387,7 +387,7 @@ fn draw_image_stroke_in_container(
|
||||
}
|
||||
|
||||
let size = image.unwrap().dimensions();
|
||||
let canvas = render_state.surfaces.canvas(surface_id);
|
||||
let canvas = render_state.surfaces.canvas_and_mark_dirty(surface_id);
|
||||
let container = &shape.selrect;
|
||||
let path_transform = shape.to_path_transform();
|
||||
let svg_attrs = shape.svg_attrs.as_ref();
|
||||
@@ -606,7 +606,7 @@ fn render_internal(
|
||||
|
||||
let scale = render_state.get_scale();
|
||||
let target_surface = surface_id.unwrap_or(SurfaceId::Strokes);
|
||||
let canvas = render_state.surfaces.canvas(target_surface);
|
||||
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
|
||||
let selrect = shape.selrect;
|
||||
let path_transform = shape.to_path_transform();
|
||||
let svg_attrs = shape.svg_attrs.as_ref();
|
||||
@@ -688,7 +688,7 @@ pub fn render_text_paths(
|
||||
let scale = render_state.get_scale();
|
||||
let canvas = render_state
|
||||
.surfaces
|
||||
.canvas(surface_id.unwrap_or(SurfaceId::Strokes));
|
||||
.canvas_and_mark_dirty(surface_id.unwrap_or(SurfaceId::Strokes));
|
||||
let selrect = &shape.selrect;
|
||||
let svg_attrs = shape.svg_attrs.as_ref();
|
||||
let mut paint: skia_safe::Handle<_> =
|
||||
|
||||
@@ -17,17 +17,18 @@ const TILE_SIZE_MULTIPLIER: i32 = 2;
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub enum SurfaceId {
|
||||
Target = 0b00_0000_0001,
|
||||
Filter = 0b00_0000_0010,
|
||||
Cache = 0b00_0000_0100,
|
||||
Current = 0b00_0000_1000,
|
||||
Fills = 0b00_0001_0000,
|
||||
Strokes = 0b00_0010_0000,
|
||||
DropShadows = 0b00_0100_0000,
|
||||
InnerShadows = 0b00_1000_0000,
|
||||
TextDropShadows = 0b01_0000_0000,
|
||||
UI = 0b10_0000_0000,
|
||||
Debug = 0b10_0000_0001,
|
||||
Target = 0b000_0000_0001,
|
||||
Filter = 0b000_0000_0010,
|
||||
Cache = 0b000_0000_0100,
|
||||
Current = 0b000_0000_1000,
|
||||
Fills = 0b000_0001_0000,
|
||||
Strokes = 0b000_0010_0000,
|
||||
DropShadows = 0b000_0100_0000,
|
||||
InnerShadows = 0b000_1000_0000,
|
||||
TextDropShadows = 0b001_0000_0000,
|
||||
UI = 0b010_0000_0000,
|
||||
Debug = 0b010_0000_0001,
|
||||
Export = 0b100_0000_0001,
|
||||
}
|
||||
|
||||
pub struct Surfaces {
|
||||
@@ -52,9 +53,13 @@ pub struct Surfaces {
|
||||
// for drawing debug info.
|
||||
debug: skia::Surface,
|
||||
// for drawing tiles.
|
||||
export: skia::Surface,
|
||||
|
||||
tiles: TileTextureCache,
|
||||
sampling_options: skia::SamplingOptions,
|
||||
margins: skia::ISize,
|
||||
// Tracks which surfaces have content (dirty flag bitmask)
|
||||
dirty_surfaces: u32,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -88,6 +93,7 @@ impl Surfaces {
|
||||
|
||||
let ui = gpu_state.create_surface_with_dimensions("ui".to_string(), width, height);
|
||||
let debug = gpu_state.create_surface_with_dimensions("debug".to_string(), width, height);
|
||||
let export = gpu_state.create_target_surface(width, height);
|
||||
|
||||
let tiles = TileTextureCache::new();
|
||||
Surfaces {
|
||||
@@ -102,9 +108,11 @@ impl Surfaces {
|
||||
shape_strokes,
|
||||
ui,
|
||||
debug,
|
||||
export,
|
||||
tiles,
|
||||
sampling_options,
|
||||
margins,
|
||||
dirty_surfaces: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,10 +155,51 @@ impl Surfaces {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the canvas and automatically marks
|
||||
/// render surfaces as dirty when accessed. This tracks which surfaces
|
||||
/// have content for optimization purposes.
|
||||
pub fn canvas_and_mark_dirty(&mut self, id: SurfaceId) -> &skia::Canvas {
|
||||
// Automatically mark render surfaces as dirty when accessed
|
||||
// This tracks which surfaces have content for optimization
|
||||
match id {
|
||||
SurfaceId::Fills
|
||||
| SurfaceId::Strokes
|
||||
| SurfaceId::InnerShadows
|
||||
| SurfaceId::TextDropShadows => {
|
||||
self.mark_dirty(id);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self.canvas(id)
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the canvas without any side effects.
|
||||
/// Use this when you only need to read or manipulate the canvas state
|
||||
/// without marking the surface as dirty.
|
||||
pub fn canvas(&mut self, id: SurfaceId) -> &skia::Canvas {
|
||||
self.get_mut(id).canvas()
|
||||
}
|
||||
|
||||
/// Marks a surface as having content (dirty)
|
||||
pub fn mark_dirty(&mut self, id: SurfaceId) {
|
||||
self.dirty_surfaces |= id as u32;
|
||||
}
|
||||
|
||||
/// Checks if a surface has content
|
||||
pub fn is_dirty(&self, id: SurfaceId) -> bool {
|
||||
(self.dirty_surfaces & id as u32) != 0
|
||||
}
|
||||
|
||||
/// Clears the dirty flag for a surface or set of surfaces
|
||||
pub fn clear_dirty(&mut self, ids: u32) {
|
||||
self.dirty_surfaces &= !ids;
|
||||
}
|
||||
|
||||
/// Clears all dirty flags
|
||||
pub fn clear_all_dirty(&mut self) {
|
||||
self.dirty_surfaces = 0;
|
||||
}
|
||||
|
||||
pub fn flush_and_submit(&mut self, gpu_state: &mut GpuState, id: SurfaceId) {
|
||||
let surface = self.get_mut(id);
|
||||
gpu_state.context.flush_and_submit_surface(surface, None);
|
||||
@@ -159,9 +208,12 @@ impl Surfaces {
|
||||
pub fn draw_into(&mut self, from: SurfaceId, to: SurfaceId, paint: Option<&skia::Paint>) {
|
||||
let sampling_options = self.sampling_options;
|
||||
|
||||
self.get_mut(from)
|
||||
.clone()
|
||||
.draw(self.canvas(to), (0.0, 0.0), sampling_options, paint);
|
||||
self.get_mut(from).clone().draw(
|
||||
self.canvas_and_mark_dirty(to),
|
||||
(0.0, 0.0),
|
||||
sampling_options,
|
||||
paint,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn apply_mut(&mut self, ids: u32, mut f: impl FnMut(&mut skia::Surface)) {
|
||||
@@ -196,6 +248,9 @@ impl Surfaces {
|
||||
if ids & SurfaceId::Debug as u32 != 0 {
|
||||
f(self.get_mut(SurfaceId::Debug));
|
||||
}
|
||||
if ids & SurfaceId::Export as u32 != 0 {
|
||||
f(self.get_mut(SurfaceId::Export));
|
||||
}
|
||||
performance::begin_measure!("apply_mut::flags");
|
||||
}
|
||||
|
||||
@@ -212,22 +267,40 @@ impl Surfaces {
|
||||
|
||||
pub fn update_render_context(&mut self, render_area: skia::Rect, scale: f32) {
|
||||
let translation = self.get_render_context_translation(render_area, scale);
|
||||
self.apply_mut(
|
||||
SurfaceId::Fills as u32
|
||||
| SurfaceId::Strokes as u32
|
||||
| SurfaceId::InnerShadows as u32
|
||||
| SurfaceId::TextDropShadows as u32,
|
||||
|s| {
|
||||
let canvas = s.canvas();
|
||||
canvas.reset_matrix();
|
||||
canvas.scale((scale, scale));
|
||||
canvas.translate(translation);
|
||||
},
|
||||
);
|
||||
|
||||
// When context changes (zoom/pan/tile), clear all render surfaces first
|
||||
// to remove any residual content from previous tiles, then mark as dirty
|
||||
// so they get redrawn with new transformations
|
||||
let surface_ids = SurfaceId::Fills as u32
|
||||
| SurfaceId::Strokes as u32
|
||||
| SurfaceId::InnerShadows as u32
|
||||
| SurfaceId::TextDropShadows as u32
|
||||
// | SurfaceId::Export as u32
|
||||
;
|
||||
|
||||
// Clear surfaces before updating transformations to remove residual content
|
||||
self.apply_mut(surface_ids, |s| {
|
||||
s.canvas().clear(skia::Color::TRANSPARENT);
|
||||
});
|
||||
|
||||
// Mark all render surfaces as dirty so they get redrawn
|
||||
self.mark_dirty(SurfaceId::Fills);
|
||||
self.mark_dirty(SurfaceId::Strokes);
|
||||
self.mark_dirty(SurfaceId::InnerShadows);
|
||||
self.mark_dirty(SurfaceId::TextDropShadows);
|
||||
self.mark_dirty(SurfaceId::Export);
|
||||
|
||||
// Update transformations
|
||||
self.apply_mut(surface_ids, |s| {
|
||||
let canvas = s.canvas();
|
||||
canvas.reset_matrix();
|
||||
canvas.scale((scale, scale));
|
||||
canvas.translate(translation);
|
||||
});
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface {
|
||||
pub fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface {
|
||||
match id {
|
||||
SurfaceId::Target => &mut self.target,
|
||||
SurfaceId::Filter => &mut self.filter,
|
||||
@@ -240,6 +313,7 @@ impl Surfaces {
|
||||
SurfaceId::Strokes => &mut self.shape_strokes,
|
||||
SurfaceId::Debug => &mut self.debug,
|
||||
SurfaceId::UI => &mut self.ui,
|
||||
SurfaceId::Export => &mut self.export
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,19 +338,21 @@ impl Surfaces {
|
||||
pub fn draw_rect_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) {
|
||||
if let Some(corners) = shape.shape_type.corners() {
|
||||
let rrect = RRect::new_rect_radii(shape.selrect, &corners);
|
||||
self.canvas(id).draw_rrect(rrect, paint);
|
||||
self.canvas_and_mark_dirty(id).draw_rrect(rrect, paint);
|
||||
} else {
|
||||
self.canvas(id).draw_rect(shape.selrect, paint);
|
||||
self.canvas_and_mark_dirty(id)
|
||||
.draw_rect(shape.selrect, paint);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_circle_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) {
|
||||
self.canvas(id).draw_oval(shape.selrect, paint);
|
||||
self.canvas_and_mark_dirty(id)
|
||||
.draw_oval(shape.selrect, paint);
|
||||
}
|
||||
|
||||
pub fn draw_path_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) {
|
||||
if let Some(path) = shape.get_skia_path() {
|
||||
self.canvas(id).draw_path(&path, paint);
|
||||
self.canvas_and_mark_dirty(id).draw_path(&path, paint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +380,9 @@ impl Surfaces {
|
||||
self.canvas(SurfaceId::UI)
|
||||
.clear(skia::Color::TRANSPARENT)
|
||||
.reset_matrix();
|
||||
|
||||
// Clear all dirty flags after reset
|
||||
self.clear_all_dirty();
|
||||
}
|
||||
|
||||
pub fn cache_current_tile_texture(
|
||||
|
||||
@@ -192,7 +192,7 @@ pub fn render(
|
||||
}
|
||||
}
|
||||
|
||||
let canvas = render_state.surfaces.canvas(target_surface);
|
||||
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
|
||||
render_text_on_canvas(canvas, shape, paragraph_builders, shadow, blur);
|
||||
return;
|
||||
}
|
||||
@@ -371,7 +371,7 @@ pub fn render_as_path(
|
||||
) {
|
||||
let canvas = render_state
|
||||
.surfaces
|
||||
.canvas(surface_id.unwrap_or(SurfaceId::Fills));
|
||||
.canvas_and_mark_dirty(surface_id.unwrap_or(SurfaceId::Fills));
|
||||
|
||||
for (path, paint) in paths {
|
||||
// Note: path can be empty
|
||||
@@ -397,7 +397,7 @@ pub fn render_position_data(
|
||||
let rect = Rect::from_xywh(pd.x, pd.y, pd.width, pd.height);
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas(surface_id)
|
||||
.canvas_and_mark_dirty(surface_id)
|
||||
.draw_rect(rect, &paint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -920,8 +920,13 @@ impl Shape {
|
||||
}
|
||||
|
||||
Type::Group(_) | Type::Frame(_) if !self.clip_content => {
|
||||
// For frames and groups, we must always calculate extrect for all children
|
||||
// to ensure accurate bounds that include nested content across all tiles.
|
||||
// Using selrect for children can cause frames to be incorrectly omitted from
|
||||
// tiles where they have nested content.
|
||||
for child_id in self.children_ids_iter(false) {
|
||||
if let Some(child_shape) = shapes_pool.get(child_id) {
|
||||
// Always calculate full extrect for children to ensure accurate bounds
|
||||
let child_extrect = child_shape.calculate_extrect(shapes_pool, scale);
|
||||
rect.join(child_extrect);
|
||||
}
|
||||
@@ -1419,6 +1424,97 @@ impl Shape {
|
||||
!self.fills.is_empty()
|
||||
}
|
||||
|
||||
/// Determines if this frame or group can be flattened (doesn't affect children visually)
|
||||
/// A container can be flattened if it has no visual effects that affect its children
|
||||
/// and doesn't render its own content (no fills/strokes)
|
||||
pub fn can_flatten(&self) -> bool {
|
||||
// Only frames and groups can be flattened
|
||||
if !matches!(self.shape_type, Type::Frame(_) | Type::Group(_)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot flatten if it has visual effects that affect children:
|
||||
|
||||
if self.clip_content {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.transform.is_identity() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.opacity != 1.0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.blend_mode() != BlendMode::default() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.blur.is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.shadows.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Type::Group(group) = &self.shape_type {
|
||||
if group.masked {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if self.hidden {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the container itself has fills/strokes, it renders something visible
|
||||
// We cannot flatten containers that render their own background/border
|
||||
// because they need to be rendered even if they don't affect children
|
||||
if self.has_fills() || self.has_visible_strokes() {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Checks if this shape needs a layer for rendering due to visual effects
|
||||
/// (opacity < 1.0, non-default blend mode, or frame clip layer blur)
|
||||
pub fn needs_layer(&self) -> bool {
|
||||
self.opacity() < 1.0
|
||||
|| self.blend_mode().0 != skia::BlendMode::SrcOver
|
||||
|| self.has_frame_clip_layer_blur()
|
||||
|| (matches!(self.shape_type, Type::Group(g) if g.masked))
|
||||
}
|
||||
|
||||
/// Checks if this frame has clip layer blur (affects children)
|
||||
/// A frame has clip layer blur if it clips content and has layer blur
|
||||
pub fn has_frame_clip_layer_blur(&self) -> bool {
|
||||
self.frame_clip_layer_blur().is_some()
|
||||
}
|
||||
|
||||
/// Returns the frame clip layer blur if this frame has one
|
||||
/// A frame has clip layer blur if it clips content and has layer blur
|
||||
pub fn frame_clip_layer_blur(&self) -> Option<Blur> {
|
||||
use crate::shapes::BlurType;
|
||||
match self.shape_type {
|
||||
Type::Frame(_) if self.clip_content => self.blur.filter(|blur| {
|
||||
!blur.hidden && blur.blur_type == BlurType::LayerBlur && blur.value > 0.0
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if this shape has visual effects that might extend its bounds beyond selrect
|
||||
/// Shapes with these effects require expensive extrect calculation for accurate visibility checks
|
||||
pub fn has_effects_that_extend_bounds(&self) -> bool {
|
||||
!self.shadows.is_empty()
|
||||
|| self.blur.is_some()
|
||||
|| !self.strokes.is_empty()
|
||||
|| matches!(self.shape_type, Type::Group(_) | Type::Frame(_))
|
||||
}
|
||||
|
||||
pub fn count_visible_inner_strokes(&self) -> usize {
|
||||
self.visible_strokes()
|
||||
.filter(|s| s.kind == StrokeKind::Inner)
|
||||
|
||||
@@ -99,6 +99,11 @@ impl<'a> State<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render_shape_pixels(&mut self, id: &Uuid, timestamp: i32) -> Result<(Vec<u8>, i32, i32), String> {
|
||||
self.render_state
|
||||
.render_shape_pixels(id, &self.shapes, timestamp)
|
||||
}
|
||||
|
||||
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<(), String> {
|
||||
self.render_state
|
||||
.start_render_loop(None, &self.shapes, timestamp, false)?;
|
||||
|
||||
Reference in New Issue
Block a user