Merge remote-tracking branch 'origin/main' into develop

This commit is contained in:
alonso.torres 2022-05-27 09:25:22 +02:00
commit 3d8c41cd69
52 changed files with 625 additions and 568 deletions

View file

@ -107,6 +107,30 @@
(rx/of (update-indices page-id changes))))
(rx/empty))))))
(defn changed-frames
"Extracts the frame-ids changed in the given changes"
[changes objects]
(let [change->ids
(fn [change]
(case (:type change)
:add-obj
[(:parent-id change)]
(:mod-obj :del-obj)
[(:id change)]
:mov-objects
(d/concat-vec (:shapes change) [(:parent-id change)])
[]))]
(into #{}
(comp (mapcat change->ids)
(keep #(if (= :frame (get-in objects [% :type]))
%
(get-in objects [% :frame-id])))
(remove #(= uuid/zero %)))
changes)))
(defn commit-changes
[{:keys [redo-changes undo-changes
@ -115,15 +139,18 @@
(log/debug :msg "commit-changes"
:js/redo-changes redo-changes
:js/undo-changes undo-changes)
(let [error (volatile! nil)]
(let [error (volatile! nil)
page-id (:current-page-id @st/state)
frames (changed-frames redo-changes (wsh/lookup-page-objects @st/state))]
(ptk/reify ::commit-changes
cljs.core/IDeref
(-deref [_]
{:file-id file-id
:hint-events @st/last-events
:hint-origin (ptk/type origin)
:changes redo-changes})
:changes redo-changes
:page-id page-id
:frames frames})
ptk/UpdateEvent
(update [_ state]

View file

@ -279,7 +279,10 @@
:always
(d/without-nils))]
(assoc-in shape [:strokes index] new-attrs)))))))))
(-> shape
(cond-> (not (contains? shape :strokes))
(assoc :strokes []))
(assoc-in [:strokes index] new-attrs))))))))))
(defn change-shadow
[ids attrs index]

View file

@ -195,13 +195,26 @@
(ptk/reify ::handle-file-change
ptk/WatchEvent
(watch [_ _ _]
(let [changes-by-pages (group-by :page-id changes)
(let [position-data-operation?
(fn [{:keys [type attr]}]
(and (= :set type) (= attr :position-data)))
remove-update-position-data
(fn [change]
(cond-> change
(= :mod-obj (:type change))
(update :operations #(filterv (comp not position-data-operation?) %))))
process-page-changes
(fn [[page-id changes]]
(dch/update-indices page-id changes))]
(dch/update-indices page-id changes))
;; We remove `position-data` from the incomming message
changes (->> changes (mapv remove-update-position-data))
changes-by-pages (group-by :page-id changes)]
(rx/merge
(rx/of (dwp/shapes-changes-persisted file-id msg))
(rx/of (dwp/shapes-changes-persisted file-id (assoc msg :changes changes)))
(when-not (empty? changes-by-pages)
(rx/from (map process-page-changes changes-by-pages))))))))

View file

@ -6,6 +6,7 @@
(ns app.main.data.workspace.persistence
(:require
[app.common.data :as d]
[app.common.logging :as log]
[app.common.pages :as cp]
[app.common.spec :as us]
@ -17,6 +18,7 @@
[app.main.data.fonts :as df]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.thumbnails :as dwt]
[app.main.repo :as rp]
[app.main.store :as st]
[app.util.http :as http]
@ -138,16 +140,27 @@
:revn (:revn file)
:session-id sid
:changes-with-metadata (into [] changes)}]
(when (= file-id (:id params))
(->> (rp/mutation :update-file params)
(rx/mapcat (fn [lagged]
(log/debug :hint "changes persisted" :lagged (count lagged))
(let [lagged (cond->> lagged
(= #{sid} (into #{} (map :session-id) lagged))
(map #(assoc % :changes [])))]
(->> (rx/of lagged)
(rx/mapcat seq)
(rx/map #(shapes-changes-persisted file-id %))))))
(map #(assoc % :changes [])))
frame-updates
(-> (group-by :page-id changes)
(d/update-vals #(into #{} (mapcat :frames) %)))]
(rx/merge
(->> (rx/from frame-updates)
(rx/flat-map (fn [[page-id frames]]
(->> frames (map #(vector page-id %)))))
(rx/map (fn [[page-id frame-id]] (dwt/update-thumbnail page-id frame-id))))
(->> (rx/of lagged)
(rx/mapcat seq)
(rx/map #(shapes-changes-persisted file-id %)))))))
(rx/catch (fn [cause]
(rx/concat
(rx/of (rt/assign-exception cause))

View file

@ -63,13 +63,17 @@
(ted/get-editor-current-content))]
(if (ted/content-has-text? content)
(let [content (d/merge (ted/export-content content)
(dissoc (:content shape) :children))]
(dissoc (:content shape) :children))
modifiers (get-in state [:workspace-text-modifier id])]
(rx/merge
(rx/of (update-editor-state shape nil))
(when (and (not= content (:content shape))
(some? (:current-page-id state)))
(rx/of
(dch/update-shapes [id] #(assoc % :content content))
(dch/update-shapes [id] (fn [shape]
(-> shape
(assoc :content content)
(merge modifiers))))
(dwu/commit-undo-transaction)))))
(when (some? id)

View file

@ -6,7 +6,6 @@
(ns app.main.data.workspace.thumbnails
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.pages.helpers :as cph]
[app.common.uuid :as uuid]
@ -14,6 +13,8 @@
[app.main.refs :as refs]
[app.main.repo :as rp]
[app.main.store :as st]
[app.util.dom :as dom]
[app.util.webapi :as wapi]
[beicon.core :as rx]
[potok.core :as ptk]))
@ -26,45 +27,47 @@
(rx/filter #(= % id))
(rx/take 1)))
(defn thumbnail-stream
[object-id]
(rx/create
(fn [subs]
(let [node (dom/query (dm/fmt "canvas.thumbnail-canvas[data-object-id='%'" object-id))]
(if (some? node)
(-> node
(.toBlob (fn [blob]
(rx/push! subs blob)
(rx/end! subs))
"image/png"))
;; If we cannot find the node we send `nil` and the upsert will delete the thumbnail
(do (rx/push! subs nil)
(rx/end! subs)))))))
(defn update-thumbnail
"Updates the thumbnail information for the given frame `id`"
[page-id frame-id data]
(let [lock (uuid/next)
object-id (dm/str page-id frame-id)]
[page-id frame-id]
(ptk/reify ::update-thumbnail
ptk/WatchEvent
(watch [_ state _]
(let [object-id (dm/str page-id frame-id)
file-id (:current-file-id state)
blob-result (thumbnail-stream object-id)]
(ptk/reify ::update-thumbnail
IDeref
(-deref [_] {:object-id object-id :data data})
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-file :thumbnails object-id] data)
(cond-> (nil? (get-in state [::update-thumbnail-lock object-id]))
(assoc-in [::update-thumbnail-lock object-id] lock))))
(->> blob-result
(rx/merge-map
(fn [blob]
(if (some? blob)
(wapi/read-file-as-data-url blob)
(rx/of nil))))
ptk/WatchEvent
(watch [_ state stream]
(when (= lock (get-in state [::update-thumbnail-lock object-id]))
(let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize)))
params {:file-id (:current-file-id state)
:object-id object-id}]
;; Sends the first event and debounce the rest. Will only make one update once
;; the 2 second debounce is finished
(rx/merge
(->> stream
(rx/filter (ptk/type? ::update-thumbnail))
(rx/map deref)
(rx/filter #(= object-id (:object-id %)))
(rx/debounce 2000)
(rx/take 1)
(rx/map :data)
(rx/flat-map #(rp/mutation! :upsert-file-object-thumbnail (assoc params :data %)))
(rx/map #(fn [state] (d/dissoc-in state [::update-thumbnail-lock object-id])))
(rx/take-until stopper))
(->> (rx/of (update-thumbnail page-id frame-id data))
(rx/observe-on :async)))))))))
(rx/merge-map
(fn [data]
(let [params {:file-id file-id :object-id object-id :data data}]
(rx/merge
;; Update the local copy of the thumbnails so we don't need to request it again
(rx/of #(assoc-in % [:workspace-file :thumbnails object-id] data))
(->> (rp/mutation! :upsert-file-object-thumbnail params)
(rx/ignore)))))))))))
(defn- extract-frame-changes
"Process a changes set in a commit to extract the frames that are changing"
@ -156,5 +159,3 @@
(let [page-id (get state :current-page-id)
old-shape-thumbnail (get-in state [:workspace-file :thumbnails (dm/str page-id old-id)])]
(-> state (assoc-in [:workspace-file :thumbnails (dm/str page-id new-id)] old-shape-thumbnail))))))

View file

@ -82,6 +82,7 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defonce loaded (l/atom #{}))
(defonce loading (l/atom {}))
(defn- create-link-element
[uri]
@ -199,11 +200,34 @@
(p/create (fn [resolve]
(ensure-loaded! id resolve))))
([id on-loaded]
(if (contains? @loaded id)
(on-loaded id)
(when-let [font (get @fontsdb id)]
(load-font (assoc font ::on-loaded on-loaded))
(swap! loaded conj id)))))
(let [font (get @fontsdb id)]
(cond
;; Font already loaded, we just continue
(contains? @loaded id)
(on-loaded id)
;; Font is currently downloading. We attach the caller to the promise
(contains? @loading id)
(-> (get @loading id)
(p/then #(on-loaded id)))
;; First caller, we create the promise and then wait
:else
(let [on-load (fn [resolve]
(swap! loaded conj id)
(swap! loading dissoc id)
(on-loaded id)
(resolve id))
load-p (p/create
(fn [resolve _]
(-> font
(assoc ::on-loaded (partial on-load resolve))
(load-font))))]
(swap! loading assoc id load-p)
nil)))))
(defn ready
[cb]

View file

@ -75,7 +75,6 @@
on-succes
(fn [data]
(prn "SUCCESS" data)
(when-let [token (:invitation-token data)]
(st/emit! (rt/nav :auth-verify-token {} {:token token}))))

View file

@ -81,13 +81,12 @@
[:hr]
[:h2 (tr "feedback.discussions-title")]
[:p (tr "feedback.discussions-subtitle1")]
[:p (tr "feedback.discussions-subtitle2")]
[:h2 (tr "feedback.twitter-title")]
[:p (tr "feedback.twitter-subtitle1")]
[:a.btn-secondary.btn-large
{:href "https://github.com/penpot/penpot/discussions" :target "_blank"}
(tr "feedback.discussions-go-to")]
{:href "https://twitter.com/PenpotSupport" :target "_blank"}
(tr "feedback.twitter-go-to")]
[:hr]

View file

@ -360,7 +360,7 @@
(-> props
(obj/set! "style" style)))
(some? svg-attrs)
(and (some? svg-attrs) (empty? (:fills shape)))
(let [style
(-> (obj/get props "style")
(obj/clone))

View file

@ -73,8 +73,12 @@
;; Creates a style tag by replacing the urls with the data uri
style (replace-embeds fonts-css fonts-urls fonts-embed)]
(when (d/not-empty? style)
[:style {:data-loading loading?} style])))
(cond
(d/not-empty? style)
[:style {:data-loading loading?} style]
(d/not-empty? fonts)
[:style {:data-loading true}])))
(defn shape->fonts
[shape objects]

View file

@ -110,7 +110,10 @@
(let [font-variant (d/seek #(= font-variant-id (:id %)) (:variants font))]
[(str/quote (or (:family font) (:font-family data)))
(or (:style font-variant) (:font-style data))
(or (:weight font-variant) (:font-weight data))]))]
(or (:weight font-variant) (:font-weight data))]))
base (-> base
(obj/set! "--font-id" font-id))]
(cond-> base
(some? fills)

View file

@ -71,11 +71,6 @@
frame-id (:id shape)
page-id (mf/use-ctx ctx/current-page-id)
thumbnail-data-ref (mf/use-memo (mf/deps page-id frame-id) #(refs/thumbnail-frame-data page-id frame-id))
thumbnail-data (mf/deref thumbnail-data-ref)
thumbnail? (and thumbnail? (some? thumbnail-data))
;; References to the current rendered node and the its parentn
node-ref (mf/use-var nil)
@ -88,11 +83,11 @@
disable-thumbnail? (d/not-empty? (dm/get-in modifiers [(:id shape) :modifiers]))
[on-load-frame-dom thumb-renderer]
(ftr/use-render-thumbnail page-id shape node-ref rendered? thumbnail-data-ref disable-thumbnail?)
[on-load-frame-dom render-frame? thumbnail-renderer]
(ftr/use-render-thumbnail page-id shape node-ref rendered? disable-thumbnail?)
on-frame-load
(fns/use-node-store thumbnail? node-ref rendered?)]
(fns/use-node-store thumbnail? node-ref rendered? render-frame?)]
(fdm/use-dynamic-modifiers objects @node-ref modifiers)
@ -115,9 +110,9 @@
(rx/dispose! sub)))))
(mf/use-effect
(mf/deps shape fonts thumbnail? on-load-frame-dom @force-render)
(mf/deps shape fonts thumbnail? on-load-frame-dom @force-render render-frame?)
(fn []
(when (and (some? @node-ref) (or @rendered? (not thumbnail?) @force-render))
(when (and (some? @node-ref) (or @rendered? (not thumbnail?) @force-render render-frame?))
(mf/mount
(mf/element frame-shape
#js {:ref on-load-frame-dom :shape shape :fonts fonts})
@ -128,12 +123,11 @@
[:& (mf/provider ctx/render-ctx) {:value render-id}
[:g.frame-container {:id (dm/str "frame-container-" (:id shape))
:key "frame-container"
:ref on-frame-load}
:ref on-frame-load
:opacity (when (:hidden shape) 0)}
[:& ff/fontfaces-style {:fonts fonts}]
[:g.frame-thumbnail-wrapper {:id (dm/str "thumbnail-container-" (:id shape))}
[:> frame/frame-thumbnail {:key (dm/str (:id shape))
:shape (cond-> shape
(some? thumbnail-data)
(assoc :thumbnail thumbnail-data))}]
thumb-renderer]]]))))
[:g.frame-thumbnail-wrapper
{:id (dm/str "thumbnail-container-" (:id shape))
;; Hide the thumbnail when not displaying
:opacity (when (and @rendered? (not thumbnail?)) 0)}
thumbnail-renderer]]]))))

View file

@ -12,7 +12,7 @@
(defn use-node-store
"Hook responsible of storing the rendered DOM node in memory while not being used"
[thumbnail? node-ref rendered?]
[thumbnail? node-ref rendered? render-frame?]
(let [;; when `true` the node is in memory
in-memory? (mf/use-var true)
@ -33,13 +33,13 @@
(swap! re-render inc)))))]
(mf/use-effect
(mf/deps thumbnail?)
(mf/deps thumbnail? render-frame?)
(fn []
(when (and (some? @parent-ref) (some? @node-ref) @rendered? thumbnail?)
(when (and (some? @parent-ref) (some? @node-ref) @rendered? (and thumbnail? (not render-frame?)))
(.removeChild @parent-ref @node-ref)
(reset! in-memory? true))
(when (and (some? @node-ref) @in-memory? (not thumbnail?))
(when (and (some? @node-ref) @in-memory? (or (not thumbnail?) render-frame?))
(.appendChild @parent-ref @node-ref)
(reset! in-memory? false))))

View file

@ -9,19 +9,20 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.math :as mth]
[app.main.data.workspace :as dw]
[app.main.data.workspace.thumbnails :as dwt]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.hooks :as hooks]
[app.main.ui.shapes.frame :as frame]
[app.util.dom :as dom]
[app.util.object :as obj]
[app.util.timers :as ts]
[beicon.core :as rx]
[cuerdas.core :as str]
[debug :refer [debug?]]
[rumext.alpha :as mf]))
;; (def thumbnail-scale-factor 2)
(defn- draw-thumbnail-canvas
(defn- draw-thumbnail-canvas!
[canvas-node img-node]
(try
(when (and (some? canvas-node) (some? img-node))
@ -29,19 +30,12 @@
canvas-width (.-width canvas-node)
canvas-height (.-height canvas-node)]
;; TODO: Expermient with different scale factors
;; (set! (.-width canvas-node) (* thumbnail-scale-factor canvas-width))
;; (set! (.-height canvas-node) (* thumbnail-scale-factor canvas-height))
;; (.setTransform canvas-context thumbnail-scale-factor 0 0 thumbnail-scale-factor 0 0)
;; (set! (.-imageSmoothingEnabled canvas-context) true)
;; (set! (.-imageSmoothingQuality canvas-context) "high")
(.clearRect canvas-context 0 0 canvas-width canvas-height)
(.drawImage canvas-context img-node 0 0 canvas-width canvas-height)
(.toDataURL canvas-node "image/png" 1.0)))
true))
(catch :default err
(.error js/console err)
nil)))
false)))
(defn- remove-image-loading
"Remove the changes related to change a url for its embed value. This is necessary
@ -59,7 +53,7 @@
(defn use-render-thumbnail
"Hook that will create the thumbnail thata"
[page-id {:keys [id x y width height] :as shape} node-ref rendered? thumbnail-data-ref disable?]
[page-id {:keys [id x y width height] :as shape} node-ref rendered? disable?]
(let [frame-canvas-ref (mf/use-ref nil)
frame-image-ref (mf/use-ref nil)
@ -78,16 +72,25 @@
updates-str (mf/use-memo #(rx/subject))
thumbnail-data-ref (mf/use-memo (mf/deps page-id id) #(refs/thumbnail-frame-data page-id id))
thumbnail-data (mf/deref thumbnail-data-ref)
render-frame? (mf/use-state (not thumbnail-data))
on-image-load
(mf/use-callback
(fn []
(ts/raf
#(let [canvas-node (mf/ref-val frame-canvas-ref)
img-node (mf/ref-val frame-image-ref)
thumb-data (draw-thumbnail-canvas canvas-node img-node)]
(when (some? thumb-data)
(st/emit! (dw/update-thumbnail page-id id thumb-data))
(reset! image-url nil))))))
img-node (mf/ref-val frame-image-ref)]
(when (draw-thumbnail-canvas! canvas-node img-node)
(reset! image-url nil)
(reset! render-frame? false))
;; If we don't have the thumbnail data saved (normaly the first load) we update the data
;; when available
(when (not @thumbnail-data-ref)
(st/emit! (dwt/update-thumbnail page-id id) ))))))
generate-thumbnail
(mf/use-callback
@ -115,7 +118,7 @@
(fn []
(when (and (some? @node-ref) @regenerate-thumbnail)
(let [loading-images? (some? (dom/query @node-ref "[data-loading='true']"))
loading-fonts? (some? (dom/query (dm/str "#frame-container-" (:id shape) " style[data-loading='true']")))]
loading-fonts? (some? (dom/query (dm/str "#frame-container-" (:id shape) " > style[data-loading='true']")))]
(when (and (not loading-images?) (not loading-fonts?))
(generate-thumbnail)
(reset! regenerate-thumbnail false))))))
@ -172,18 +175,28 @@
(reset! observer-ref nil)))))
[on-load-frame-dom
(when (some? @image-url)
(mf/html
[:g.thumbnail-rendering
[:foreignObject {:x x :y y :width width :height height}
[:canvas {:ref frame-canvas-ref
:width fixed-width
:height fixed-height}]]
@render-frame?
(mf/html
[:*
[:> frame/frame-thumbnail {:key (dm/str (:id shape))
:shape (cond-> shape
(some? thumbnail-data)
(assoc :thumbnail thumbnail-data))}]
[:foreignObject {:x x :y y :width width :height height}
[:canvas.thumbnail-canvas
{:ref frame-canvas-ref
:data-object-id (dm/str page-id (:id shape))
:width fixed-width
:height fixed-height
;; DEBUG
:style {:filter (when (debug? :thumbnails) "invert(1)")}}]]
(when (some? @image-url)
[:image {:ref frame-image-ref
:x (:x shape)
:y (:y shape)
:href @image-url
:width (:width shape)
:height (:height shape)
:on-load on-image-load}]]))]))
:on-load on-image-load}])])]))

View file

@ -23,6 +23,7 @@
[app.util.text-editor :as ted]
[app.util.text-svg-position :as utp]
[app.util.timers :as ts]
[promesa.core :as p]
[rumext.alpha :as mf]))
(defn strip-position-data [shape]
@ -61,37 +62,39 @@
(assoc :content (d/txt-merge content editor-content)))))
(defn- update-text-shape
[{:keys [grow-type id]} node]
[{:keys [grow-type id migrate]} node]
;; Check if we need to update the size because it's auto-width or auto-height
(when (contains? #{:auto-height :auto-width} grow-type)
(let [{:keys [width height]}
(-> (dom/query node ".paragraph-set")
(dom/get-client-size))
width (mth/ceil width)
height (mth/ceil height)]
(when (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height)))
(st/emit! (dwt/resize-text id width height)))))
;; Update the position-data of every text fragment
(let [position-data (utp/calc-position-data node)]
(st/emit! (dwt/update-position-data id position-data)))
(p/let [position-data (utp/calc-position-data node)]
(st/emit! (dwt/update-position-data id position-data))
(when (contains? #{:auto-height :auto-width} grow-type)
(let [{:keys [width height]}
(-> (dom/query node ".paragraph-set")
(dom/get-client-size))
width (mth/ceil width)
height (mth/ceil height)]
(when (and (not (mth/almost-zero? width))
(not (mth/almost-zero? height))
(not migrate))
(st/emit! (dwt/resize-text id width height))))))
(st/emit! (dwt/clean-text-modifier id)))
(defn- update-text-modifier
[{:keys [grow-type id]} node]
(let [position-data (utp/calc-position-data node)
props {:position-data position-data}
(p/let [position-data (utp/calc-position-data node)
props {:position-data position-data}
props
(if (contains? #{:auto-height :auto-width} grow-type)
(let [{:keys [width height]} (-> (dom/query node ".paragraph-set") (dom/get-client-size))
width (mth/ceil width)
height (mth/ceil height)]
(if (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height)))
(assoc props :width width :height height)
props))
props)]
props
(if (contains? #{:auto-height :auto-width} grow-type)
(let [{:keys [width height]} (-> (dom/query node ".paragraph-set") (dom/get-client-size))
width (mth/ceil width)
height (mth/ceil height)]
(if (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height)))
(assoc props :width width :height height)
props))
props)]
(st/emit! (dwt/update-text-modifier id props))))
@ -228,7 +231,14 @@
(mf/deps text-shapes modifiers)
#(d/update-vals text-shapes (partial process-shape modifiers)))
editing-shape (get text-shapes edition)]
editing-shape (get text-shapes edition)
;; This memo is necesary so the viewport-text-wrapper memoize its props correctly
text-shapes-wrapper
(mf/use-memo
(mf/deps text-shapes edition)
(fn []
(dissoc text-shapes edition)))]
;; We only need the effect to run on "mount" because the next fonts will be changed when the texts are
;; edited
@ -242,5 +252,5 @@
(when editing-shape
[:& viewport-text-editing {:shape editing-shape}])
[:& viewport-texts-wrapper {:text-shapes (dissoc text-shapes edition)
[:& viewport-texts-wrapper {:text-shapes text-shapes-wrapper
:modifiers modifiers}]]))

View file

@ -71,8 +71,8 @@
focus (mf/deref refs/workspace-focus-selected)
objects-ref (mf/use-memo #(refs/workspace-page-objects-by-id page-id))
base-objects (-> (mf/deref objects-ref)
(ui-hooks/with-focus-objects focus))
objects (mf/deref objects-ref)
base-objects (-> objects (ui-hooks/with-focus-objects focus))
modifiers (mf/deref refs/workspace-modifiers)
@ -245,7 +245,7 @@
[:g {:pointer-events "none" :opacity 0}
[:& stv/viewport-texts {:key (dm/str "texts-" page-id)
:page-id page-id
:objects base-objects
:objects objects
:modifiers modifiers
:edition edition}]]]

View file

@ -25,6 +25,7 @@
[app.util.globals :as globals]
[app.util.timers :as timers]
[beicon.core :as rx]
[debug :refer [debug?]]
[goog.events :as events]
[rumext.alpha :as mf])
(:import goog.events.EventType))
@ -269,7 +270,11 @@
;; We only allow active frames that are contained in the vbox
(filter (partial inside-vbox vbox objects)))
all-frames)]
all-frames)
;; Debug only: Disable the thumbnails
new-active-frames
(if (debug? :disable-frame-thumbnails) (into #{} all-frames) new-active-frames)]
(when (not= @active-frames new-active-frames)
(reset! active-frames new-active-frames)))))))

View file

@ -135,35 +135,36 @@
(on-frame-leave (:id frame))))
text-pos-x (if (:use-for-thumbnail? frame) 15 0)]
[:*
(when (:use-for-thumbnail? frame)
[:g {:transform (str (when (and selected? modifiers)
(str (:displacement modifiers) " "))
(text-transform label-pos zoom))}
[:svg {:x 0
:y -9
:width 12
:height 12
:class "workspace-frame-icon"
(when (not (:hidden frame))
[:*
(when (:use-for-thumbnail? frame)
[:g {:transform (str (when (and selected? modifiers)
(str (:displacement modifiers) " "))
(text-transform label-pos zoom))}
[:svg {:x 0
:y -9
:width 12
:height 12
:class "workspace-frame-icon"
:style {:fill (when selected? "var(--color-primary-dark)")}
:visibility (if show-artboard-names? "visible" "hidden")}
[:use {:href "#icon-set-thumbnail"}]]])
[:text {:x text-pos-x
:y 0
:width width
:height 20
:class "workspace-frame-label"
:transform (str (when (and selected? modifiers)
(str (:displacement modifiers) " "))
(text-transform label-pos zoom))
:style {:fill (when selected? "var(--color-primary-dark)")}
:visibility (if show-artboard-names? "visible" "hidden")}
[:use {:href "#icon-set-thumbnail"}]]])
[:text {:x text-pos-x
:y 0
:width width
:height 20
:class "workspace-frame-label"
:transform (str (when (and selected? modifiers)
(str (:displacement modifiers) " "))
(text-transform label-pos zoom))
:style {:fill (when selected? "var(--color-primary-dark)")}
:visibility (if show-artboard-names? "visible" "hidden")
:on-mouse-down on-mouse-down
:on-double-click on-double-click
:on-context-menu on-context-menu
:on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave}
(:name frame)]]))
:visibility (if show-artboard-names? "visible" "hidden")
:on-mouse-down on-mouse-down
:on-double-click on-double-click
:on-context-menu on-context-menu
:on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave}
(:name frame)]])))
(mf/defc frame-titles
{::mf/wrap-props false

View file

@ -10,17 +10,34 @@
[beicon.core :as rx]))
(defonce cache (atom {}))
(defonce pending (atom {}))
(defn with-cache
[{:keys [key max-age]} observable]
(let [entry (get @cache key)
pending-entry (get @pending key)
age (when entry
(dt/diff (dt/now)
(:created-at entry)))]
(if (and (some? entry) (< age max-age))
(cond
(and (some? entry) (< age max-age))
(rx/of (:data entry))
(->> observable
(rx/tap
(fn [data]
(let [entry {:created-at (dt/now) :data data}]
(swap! cache assoc key entry))))))))
(some? pending-entry)
pending-entry
:else
(let [subject (rx/subject)]
(swap! pending assoc key subject)
(->> observable
(rx/catch #(do (rx/error! subject %)
(swap! pending dissoc key)
(rx/throw %)))
(rx/tap
(fn [data]
(let [entry {:created-at (dt/now) :data data}]
(swap! cache assoc key entry))
(rx/push! subject data)
(rx/end! subject)
(swap! pending dissoc key))))))))

View file

@ -558,3 +558,11 @@
(seq (.-children node)))]
(->> root-node
(tree-seq branch? get-children))))
(defn check-font? [font]
(let [fonts (.-fonts globals/document)]
(.check fonts font)))
(defn load-font [font]
(let [fonts (.-fonts globals/document)]
(.load fonts font)))

View file

@ -173,32 +173,34 @@
(fetch-data-uri uri false))
([uri throw-err?]
(c/with-cache {:key uri :max-age (dt/duration {:hours 4})}
(let [request-stream
(send! {:method :get
:uri uri
:response-type :blob
:omit-default-headers true})
(let [request-str
(->> (send! {:method :get
:uri uri
:response-type :blob
:omit-default-headers true})
(rx/tap
(fn [resp]
(when (or (< (:status resp) 200) (>= (:status resp) 300))
(rx/throw (js/Error. "Error fetching data uri" #js {:cause (clj->js resp)})))))
request-stream
(if throw-err?
(rx/tap #(when-not (and (>= (:status %) 200) (< (:status %) 300))
;; HTTP ERRROR
(throw (js/Error. "Error fetching data uri" #js {:cause (clj->js %)})))
request-stream)
(rx/filter #(= 200 (:status %))
request-stream))]
(->> request-stream
(rx/map :body)
(rx/mapcat wapi/read-file-as-data-url)
(rx/map #(hash-map uri %)))))))
(rx/map :body)
(rx/mapcat wapi/read-file-as-data-url)
(rx/map #(hash-map uri %))
(c/with-cache {:key uri :max-age (dt/duration {:hours 4})}))]
;; We need to check `throw-err?` after the cache is resolved otherwise we cannot cache request
;; with different values of throw-err. By default we throw always the exception and then we just
;; ignore when `throw-err?` is `true`
(if (not throw-err?)
(->> request-str (rx/catch #(rx/empty)))
request-str))))
(defn fetch-text [url]
(c/with-cache {:key url :max-age (dt/duration {:hours 4})}
(->> (send!
{:method :get
:mode :cors
:omit-default-headers true
:uri url
:response-type :text})
(rx/map :body))))
(->> (send!
{:method :get
:mode :cors
:omit-default-headers true
:uri url
:response-type :text})
(rx/map :body)
(c/with-cache {:key url :max-age (dt/duration {:hours 4})})))

View file

@ -23,14 +23,18 @@
{:label "Français (community)" :value "fr"}
{:label "Deutsch (community)" :value "de"}
;; {:label "Italiano (community)" :value "it"}
{:label "Norsk - Bokmål (community)" :value "nb_no"}
{:label "Portuguese - Brazil (community)" :value "pt_br"}
{:label "Polski (community)" :value "pl"}
{:label "Русский (community)" :value "ru"}
{:label "Rumanian (community)" :value "ro"}
{:label "Türkçe (community)" :value "tr"}
{:label "Rumanian (communit)" :value "ro"}
{:label "Portuguese (Brazil, community)" :value "pt_br"}
{:label "Ελληνική γλώσσα (community)" :value "el"}
{:label "עִבְרִית (community)" :value "he"}
{:label "عربي/عربى (community)" :value "ar"}
{:label "简体中文 (community)" :value "zh_cn"}])
{:label "فارسی (community)" :value "fa"}
{:label "简体中文 (community)" :value "zh_cn"}
{:label "中国传统的 (community)" :value "zh_hant"}])
(defn- parse-locale
[locale]

View file

@ -7,6 +7,7 @@
(ns app.util.import.parser
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.spec.interactions :as cti]
@ -213,16 +214,29 @@
(reduce add-attrs node-attrs))
(= type :frame)
(let [;; The nodes with the "frame-background" class can have some anidation depending on the strokes they have
(let [;; Old .penpot files doesn't have "g" nodes. They have a clipPath reference as a node attribute
to-url #(dm/str "url(#" % ")")
frame-clip-rect-node (->> (find-all-nodes node :defs)
(mapcat #(find-all-nodes % :clipPath))
(filter #(= (to-url (:id (:attrs %))) (:clip-path node-attrs)))
(mapcat #(find-all-nodes % #{:rect :path}))
(first))
;; The nodes with the "frame-background" class can have some anidation depending on the strokes they have
g-nodes (find-all-nodes node :g)
defs-nodes (flatten (map #(find-all-nodes % :defs) g-nodes))
gg-nodes (flatten (map #(find-all-nodes % :g) g-nodes))
rect-nodes (flatten [[(find-all-nodes node :rect)]
(map #(find-all-nodes % #{:rect :path}) defs-nodes)
(map #(find-all-nodes % #{:rect :path}) g-nodes)
(map #(find-all-nodes % #{:rect :path}) gg-nodes)])
svg-node (d/seek #(= "frame-background" (get-in % [:attrs :class])) rect-nodes)]
(merge (add-attrs {} (:attrs svg-node)) node-attrs))
(merge
(add-attrs {} (:attrs frame-clip-rect-node))
(add-attrs {} (:attrs svg-node))
node-attrs))
(= type :svg-raw)
(let [svg-content (get-data node :penpot:svg-content)

View file

@ -9,9 +9,11 @@
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.common.transit :as transit]
[app.main.fonts :as fonts]
[app.main.store :as st]
[app.util.dom :as dom]
[app.util.text-position-data :as tpd]))
[app.util.text-position-data :as tpd]
[promesa.core :as p]))
(defn parse-text-nodes
"Given a text node retrieves the rectangles for everyone of its paragraphs and its text."
@ -27,6 +29,27 @@
(map parse-entry)
(tpd/parse-text-nodes parent-node text-node))))
(def load-promises (atom {}))
(defn load-font
[font]
(if (contains? @load-promises font)
(get @load-promises font)
(let [load-promise (dom/load-font font)]
(swap! load-promises assoc font load-promise)
load-promise)))
(defn resolve-font
[^js node]
(let [styles (js/getComputedStyle node)
font (.getPropertyValue styles "font")]
(if (dom/check-font? font)
(p/resolved font)
(let [font-id (.getPropertyValue styles "--font-id")]
(-> (fonts/ensure-loaded! font-id)
(p/then #(when (not (dom/check-font? font))
(load-font font))))))))
(defn calc-text-node-positions
[base-node viewport zoom]
@ -58,22 +81,25 @@
:width (- (:x p2) (:x p1))
:height (- (:y p2) (:y p1)))))
text-nodes (dom/query-all base-node ".text-node, span[data-text]")]
(->> text-nodes
(mapcat
(fn [parent-node]
(let [direction (.-direction (js/getComputedStyle parent-node))]
(->> (.-childNodes parent-node)
(mapcat #(parse-text-nodes parent-node direction %))))))
(mapv #(update % :position translate-rect))))))
text-nodes (dom/query-all base-node ".text-node, span[data-text]")
load-fonts (->> text-nodes (map resolve-font))]
(-> (p/all load-fonts)
(p/then
(fn []
(->> text-nodes
(mapcat
(fn [parent-node]
(let [direction (.-direction (js/getComputedStyle parent-node))]
(->> (.-childNodes parent-node)
(mapcat #(parse-text-nodes parent-node direction %))))))
(mapv #(update % :position translate-rect)))))))))
(defn calc-position-data
[base-node]
(let [viewport (dom/get-element "render")
zoom (or (get-in @st/state [:workspace-local :zoom]) 1)]
(when (and (some? base-node) (some? viewport))
(let [text-data (calc-text-node-positions base-node viewport zoom)]
(p/let [text-data (calc-text-node-positions base-node viewport zoom)]
(when (d/not-empty? text-data)
(->> text-data
(mapv (fn [{:keys [node position text direction]}]

View file

@ -125,7 +125,8 @@
match-criteria?
(fn [shape]
(and (not (:hidden shape))
(not (:blocked shape))
(or (= :frame (:type shape)) ;; We return frames even if blocked
(not (:blocked shape)))
(or (not frame-id) (= frame-id (:frame-id shape)))
(case (:type shape)
:frame include-frames?

View file

@ -64,6 +64,9 @@
;; Disable thumbnail cache
:disable-thumbnail-cache
;; Disable frame thumbnails
:disable-frame-thumbnails
})
;; These events are excluded when we activate the :events flag