0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-17 17:24:32 -05:00

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

This commit is contained in:
Andrey Antukh 2022-04-01 11:58:07 +02:00
commit c9ddc83eef
39 changed files with 678 additions and 450 deletions

View file

@ -22,11 +22,11 @@
- Constraints are not well assigned when default and multiselection [Taiga #3069](https://tree.taiga.io/project/penpot/issue/3069)
- Exporting big files flow [Taiga #2218](https://tree.taiga.io/project/penpot/us/2218)
- Multiexport from main menu [Taiga #520](https://tree.taiga.io/project/penpot/us/28541)
- Multipexport assets (aka bulk export) [Taiga #520](https://tree.taiga.io/project/penpot/us/520)
- Multiexport assets (aka bulk export) [Taiga #520](https://tree.taiga.io/project/penpot/us/520)
- Set the artboard layer fixed at the top side of the layers [Taiga #2636](https://tree.taiga.io/project/penpot/us/2636)
- Set an artboard as the file thumbnail [Taiga #1526](https://tree.taiga.io/project/penpot/us/1526)
- Social login redesign [Taiga #2974](https://tree.taiga.io/project/penpot/task/2974)
- Add border radius to our artboars [Taiga #2056](https://tree.taiga.io/project/penpot/us/2056)
- Add border radius to our artboards [Taiga #2056](https://tree.taiga.io/project/penpot/us/2056)
- Allow send multiple team invitations at once [Taiga #2798](https://tree.taiga.io/project/penpot/us/2798)
- Persist color palette and color picker across refresh [Taiga #1660](https://tree.taiga.io/project/penpot/issue/1660)
- Ability to add multiple strokes to a shape [Taiga #2778](https://tree.taiga.io/project/penpot/us/2778)
@ -42,9 +42,13 @@
- Allow registration with invitation token when registration is disabled
- Add the ability to disable standard, password login [Taiga #2999](https://tree.taiga.io/project/penpot/us/2999)
- Don't stop SVG import when an image cannot be imported [#1531](https://github.com/penpot/penpot/issues/1531)
- Fix paste shapes while editing text [Taiga #2396](https://tree.taiga.io/project/penpot/issue/2396)
### :bug: Bugs fixed
- Avoid numeric inputs to allow big numbers [Taiga #2858](https://tree.taiga.io/project/penpot/issue/2858)
- Fix component contex menu size [Taiga #2480](https://tree.taiga.io/project/penpot/issue/2480)
- Add shadow to artboard make it lose the fill [Taiga #3139](https://tree.taiga.io/project/penpot/issue/3139)
- Avoid numeric inputs to change its value without focusing them [Taiga #3140](https://tree.taiga.io/project/penpot/issue/3140)
- Fix comments modal when changing pages [Taiga #2597](https://tree.taiga.io/project/penpot/issue/2508)
- Copy paste inside a text layer leaves pasted text transparent [Taiga #3096](https://tree.taiga.io/project/penpot/issue/3096)
@ -71,10 +75,19 @@
- Fix shift+2 shortcut in MacOS with non-english keyboards [Taiga #3038](https://tree.taiga.io/project/penpot/issue/3038)
- Some fixes to SVG imports [Taiga #3122](https://tree.taiga.io/project/penpot/issue/3122) [#1720](https://github.com/penpot/penpot/issues/1720) [Taiga #2884](https://tree.taiga.io/project/penpot/issue/2884)
- Fix drag guides to delete target area [#1679](https://github.com/penpot/penpot/issues/1679)
- Fix undo when rotating groups [Taiga #3136](https://tree.taiga.io/project/penpot/issue/3136)
- Fix component name in sidebar widget [Taiga #3144](https://tree.taiga.io/project/penpot/issue/3144)
### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!)
## 1.12.3-beta
### :bug: Bugs fixed
- Fix issue with shift+select to deselect shapes [Taiga #3154](https://tree.taiga.io/project/penpot/issue/3154)
- Fix issue with drag-select shapes [Taiga #3165](https://tree.taiga.io/project/penpot/issue/3165)
## 1.12.2-beta
### :bug: Bugs fixed

View file

@ -1,5 +1,6 @@
{:deps
{penpot/common {:local/root "../common"}
org.clojure/clojure {:mvn/version "1.10.3"}
org.clojure/core.async {:mvn/version "1.5.648"}
;; Logging

View file

@ -17,10 +17,11 @@
[app.srepl.dev :as dev]
[app.util.blob :as blob]
[app.util.time :as dt]
[fipp.edn :refer [pprint]]
[clojure.spec.alpha :as s]
[clojure.walk :as walk]
[cuerdas.core :as str]
[expound.alpha :as expound]))
[expound.alpha :as expound]
[fipp.edn :refer [pprint]]))
(defn update-file
([system id f] (update-file system id f false))
@ -66,86 +67,48 @@
(db/insert! conn :file params)
(:id file))))))
(defn verify-files
[system {:keys [age sleep chunk-size max-chunks stop-on-error? verbose?]
:or {sleep 1000
age "72h"
chunk-size 10
verbose? false
stop-on-error? true
max-chunks ##Inf}}]
;; (defn check-image-shapes
;; [{:keys [data] :as file} stats]
;; (println "=> analizing file:" (:name file) (:id file))
;; (swap! stats update :total-files (fnil inc 0))
;; (let [affected? (atom false)]
;; (walk/prewalk (fn [obj]
;; (when (and (map? obj) (= :image (:type obj)))
;; (when-let [fcolor (some-> obj :fill-color str/upper)]
;; (when (or (= fcolor "#B1B2B5")
;; (= fcolor "#7B7D85"))
;; (reset! affected? true)
;; (swap! stats update :affected-shapes (fnil inc 0))
;; (println "--> image shape:" ((juxt :id :name :fill-color :fill-opacity) obj)))))
;; obj)
;; data)
;; (when @affected?
;; (swap! stats update :affected-files (fnil inc 0)))))
(letfn [(retrieve-chunk [conn cursor]
(let [sql (str "select id, name, modified_at, data from file "
" where modified_at > ? and deleted_at is null "
" order by modified_at asc limit ?")
age (if cursor
cursor
(-> (dt/now) (dt/minus age)))]
(seq (db/exec! conn [sql age chunk-size]))))
(defn analyze-files
[system {:keys [sleep chunk-size max-chunks on-file]
:or {sleep 1000 chunk-size 10 max-chunks ##Inf}}]
(let [stats (atom {})]
(letfn [(retrieve-chunk [conn cursor]
(let [sql (str "select id, name, modified_at, data from file "
" where modified_at < ? and deleted_at is null "
" order by modified_at desc limit ?")]
(->> (db/exec! conn [sql cursor chunk-size])
(map #(update % :data blob/decode)))))
(validate-item [{:keys [id data modified-at] :as file}]
(let [data (blob/decode data)
valid? (s/valid? ::spec.file/data data)]
(process-chunk [chunk]
(loop [items chunk]
(when-let [item (first items)]
(on-file item stats)
(recur (rest items)))))]
(l/debug :hint "validated file"
:file-id id
:age (-> (dt/diff modified-at (dt/now))
(dt/truncate :minutes)
(str)
(subs 2)
(str/lower))
:valid valid?)
(when (and (not valid?) verbose?)
(let [edata (-> (s/explain-data ::spec.file/data data)
(update ::s/problems #(take 5 %)))]
(binding [s/*explain-out* expound/printer]
(l/warn ::l/raw (with-out-str (s/explain-out edata))))))
(when (and (not valid?) stop-on-error?)
(throw (ex-info "penpot/abort" {})))
valid?))
(validate-chunk [chunk]
(loop [items chunk
success 0
errored 0]
(if-let [item (first items)]
(if (validate-item item)
(recur (rest items) (inc success) errored)
(recur (rest items) success (inc errored)))
[(:modified-at (last chunk))
success
errored])))
(fmt-result [ns ne]
{:total (+ ns ne)
:errors ne
:success ns})
]
(try
(db/with-atomic [conn (:app.db/pool system)]
(loop [cursor nil
chunks 0
success 0
errors 0]
(if (< chunks max-chunks)
(if-let [chunk (retrieve-chunk conn cursor)]
(let [[cursor success' errors'] (validate-chunk chunk)]
(loop [cursor (dt/now)
chunks 0]
(when (< chunks max-chunks)
(when-let [chunk (retrieve-chunk conn cursor)]
(let [cursor (-> chunk last :modified-at)]
(process-chunk chunk)
(Thread/sleep (inst-ms (dt/duration sleep)))
(recur cursor
(inc chunks)
(+ success success')
(+ errors errors')))
(fmt-result success errors))
(fmt-result success errors))))
(catch Throwable cause
(when (not= "penpot/abort" (ex-message cause))
(throw cause))
:error))))
(recur cursor (inc chunks))))))
@stats))))

View file

@ -6,7 +6,8 @@
(ns app.common.data
"Data manipulation and query helper functions."
(:refer-clojure :exclude [read-string hash-map merge name parse-double group-by iteration])
(:refer-clojure :exclude [read-string hash-map merge name update-vals
parse-double group-by iteration])
#?(:cljs
(:require-macros [app.common.data]))
(:require
@ -198,6 +199,23 @@
([mfn coll]
(into {} (mapm mfn) coll)))
;; TEMPORARY COPY of clojure.core/update-vals until we migrate to clojure 1.11
(defn update-vals
"m f => {k (f v) ...}
Given a map m and a function f of 1-argument, returns a new map where the keys of m
are mapped to result of applying f to the corresponding values of m."
[m f]
(with-meta
(persistent!
(reduce-kv (fn [acc k v] (assoc! acc k (f v)))
(if #?(:clj (instance? clojure.lang.IEditableCollection m)
:cljs (implements? core/IEditableCollection m))
(transient m)
(transient {}))
m))
(meta m)))
(defn removev
"Returns a vector of the items in coll for which (fn item) returns logical false"
[fn coll]
@ -653,3 +671,13 @@
(recur acc (step k))
acc)))
acc))))))
(defn toggle-selection
([set value]
(toggle-selection set value false))
([set value toggle?]
(if-not toggle?
(conj (ordered-set) value)
(if (contains? set value)
(disj set value)
(conj set value)))))

View file

@ -26,7 +26,7 @@
(toString [_]
(str "matrix(" a "," b "," c "," d "," e "," f ")")))
(defn ^boolean matrix?
(defn matrix?
"Return true if `v` is Matrix instance."
[v]
(instance? Matrix v))
@ -57,6 +57,15 @@
(map (comp d/parse-double first)))]
(apply matrix params)))
(defn close?
[m1 m2]
(and (mth/close? (.-a m1) (.-a m2))
(mth/close? (.-b m1) (.-b m2))
(mth/close? (.-c m1) (.-c m2))
(mth/close? (.-d m1) (.-d m2))
(mth/close? (.-e m1) (.-e m2))
(mth/close? (.-f m1) (.-f m2))))
(defn multiply
([^Matrix m1 ^Matrix m2]
(let [m1a (.-a m1)
@ -111,7 +120,7 @@
([{x :x y :y :as pt}]
(assert (gpt/point? pt))
(Matrix. 1 0 0 1 x y))
([x y]
(translate-matrix (gpt/point x y))))

View file

@ -21,7 +21,7 @@
(defn s [{:keys [x y]}] (str "(" x "," y ")"))
(defn ^boolean point?
(defn point?
"Return true if `v` is Point instance."
[v]
(or (instance? Point v)
@ -33,8 +33,7 @@
(s/def ::point
(s/and (s/keys :req-un [::x ::y]) point?))
(defn ^boolean point-like?
(defn point-like?
[{:keys [x y] :as v}]
(and (map? v)
(not (nil? x))
@ -61,6 +60,11 @@
([x y]
(Point. x y)))
(defn close?
[p1 p2]
(and (mth/close? (:x p1) (:x p2))
(mth/close? (:y p1) (:y p2))))
(defn angle->point [{:keys [x y]} angle distance]
(point
(+ x (* distance (mth/cos angle)))

View file

@ -34,6 +34,24 @@
:width width
:height height})))
(defn close-rect?
[rect1 rect2]
(and (mth/close? (:x rect1) (:x rect2))
(mth/close? (:y rect1) (:y rect2))
(mth/close? (:width rect1) (:width rect2))
(mth/close? (:height rect1) (:height rect2))))
(defn close-selrect?
[selrect1 selrect2]
(and (mth/close? (:x selrect1) (:x selrect2))
(mth/close? (:y selrect1) (:y selrect2))
(mth/close? (:x1 selrect1) (:x1 selrect2))
(mth/close? (:y1 selrect1) (:y1 selrect2))
(mth/close? (:x2 selrect1) (:x2 selrect2))
(mth/close? (:y2 selrect1) (:y2 selrect2))
(mth/close? (:width selrect1) (:width selrect2))
(mth/close? (:height selrect1) (:height selrect2))))
(defn rect->points [{:keys [x y width height]}]
(when (d/num? x y)
(let [width (max width 0.01)

View file

@ -8,8 +8,12 @@
(: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.geom.shapes :as gsh]
[app.common.geom.shapes.bool :as gshb]
[app.common.geom.shapes.rect :as gshr]
[app.common.math :as mth]
[app.common.pages :as cp]
[app.common.pages.helpers :as cph]
[app.common.uuid :as uuid]))
@ -379,8 +383,24 @@
generate-operation
(fn [operations attr old new]
(let [old-val (get old attr)
new-val (get new attr)]
(if (= old-val new-val)
new-val (get new attr)
equal? (cond
(and (number? old-val) (number? new-val))
(mth/close? old-val new-val)
(and (gmt/matrix? old-val) (gmt/matrix? new-val))
(gmt/close? old-val new-val)
(= attr :points)
(every? #(apply gpt/close? %) (d/zip old-val new-val))
(= attr :selrect)
(gshr/close-selrect? old-val new-val)
:else
(= old-val new-val))]
(if equal?
operations
(-> operations
(update :rops conj {:type :set :attr attr :val new-val :ignore-touched true})
@ -390,8 +410,8 @@
(fn [changes parent]
(let [children (->> parent :shapes (map (d/getf objects)))
resized-parent (cond
(empty? children)
changes
(empty? children) ;; a parent with no children will be deleted,
nil ;; so it does not need resize
(= (:type parent) :bool)
(gshb/update-bool-selrect parent children objects)
@ -399,21 +419,22 @@
(= (:type parent) :group)
(if (:masked-group? parent)
(gsh/update-mask-selrect parent children)
(gsh/update-group-selrect parent children)))
(gsh/update-group-selrect parent children)))]
(if resized-parent
(let [{rops :rops uops :uops}
(reduce #(generate-operation %1 %2 parent resized-parent)
{:rops [] :uops []}
(keys parent))
{rops :rops uops :uops}
(reduce #(generate-operation %1 %2 parent resized-parent)
{:rops [] :uops []}
(keys parent))
change {:type :mod-obj
:page-id page-id
:id (:id parent)}]
change {:type :mod-obj
:page-id page-id
:id (:id parent)}]
(if (seq rops)
(-> changes
(update :redo-changes conj (assoc change :operations rops))
(update :undo-changes conj (assoc change :operations uops)))
(if (seq rops)
(-> changes
(update :redo-changes conj (assoc change :operations rops))
(update :undo-changes d/preconj (assoc change :operations uops)))
changes))
changes)))]
(-> (reduce resize-parent changes all-parents)

View file

@ -9,7 +9,7 @@
[app.common.colors :as clr]
[app.common.uuid :as uuid]))
(def file-version 16)
(def file-version 17)
(def default-color clr/gray-20)
(def root uuid/zero)

View file

@ -17,24 +17,28 @@
;; GENERIC SHAPE SELECTORS AND PREDICATES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn ^boolean root-frame?
(defn root-frame?
[{:keys [id type]}]
(and (= type :frame)
(= id uuid/zero)))
(defn ^boolean frame-shape?
(defn frame-shape?
[{:keys [type]}]
(= type :frame))
(defn ^boolean group-shape?
(defn group-shape?
[{:keys [type]}]
(= type :group))
(defn ^boolean text-shape?
(defn text-shape?
[{:keys [type]}]
(= type :text))
(defn ^boolean unframed-shape?
(defn image-shape?
[{:keys [type]}]
(= type :image))
(defn unframed-shape?
"Checks if it's a non-frame shape in the top level."
[shape]
(and (not (frame-shape? shape))
@ -214,7 +218,7 @@
([libraries library-id component-id]
(get-in libraries [library-id :data :components component-id])))
(defn ^boolean is-main-of?
(defn is-main-of?
[shape-main shape-inst]
(and (:shape-ref shape-inst)
(or (= (:shape-ref shape-inst) (:id shape-main))

View file

@ -10,26 +10,26 @@
[app.common.geom.matrix :as gmt]
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.path :as gsp]
[app.common.logging :as l]
[app.common.math :as mth]
[app.common.pages :as cp]
[app.common.uuid :as uuid]))
[app.common.pages.helpers :as cph]
[app.common.uuid :as uuid]
[cuerdas.core :as str]))
;; TODO: revisit this and rename to file-migrations
(defmulti migrate :version)
(defn migrate-data
([data]
(if (= (:version data) cp/file-version)
([data] (migrate-data data cp/file-version))
([data to-version]
(if (= (:version data) to-version)
data
(reduce #(migrate-data %1 %2 (inc %2))
data
(range (:version data 0) cp/file-version))))
([data _ to-version]
(-> data
(assoc :version to-version)
(migrate))))
(let [migrate-fn #(do
(l/trace :hint "migrate file" :id (:id %) :version-from %2 :version-to (inc %2))
(migrate (assoc %1 :version (inc %2))))]
(reduce migrate-fn data (range (:version data 0) to-version))))))
(defn migrate-file
[file]
@ -45,17 +45,16 @@
;; Ensure that all :shape attributes on shapes are vectors.
(defmethod migrate 2
[data]
(letfn [(update-object [_ object]
(letfn [(update-object [object]
(d/update-when object :shapes
(fn [shapes]
(if (seq? shapes)
(into [] shapes)
shapes))))
(update-page [page]
(update page :objects d/update-vals update-object))]
(update-page [_ page]
(update page :objects #(d/mapm update-object %)))]
(update data :pages-index #(d/mapm update-page %))))
(update data :pages-index d/update-vals update-page)))
;; Changes paths formats
(defmethod migrate 3
@ -89,7 +88,7 @@
(empty? (:points shape))
(assoc :points (gsh/rect->points (:selrect shape))))))
(update-object [_ object]
(update-object [object]
(cond-> object
(= :curve (:type object))
(assoc :type :path)
@ -97,25 +96,22 @@
(#{:curve :path} (:type object))
(migrate-path)
(= :frame (:type object))
(cph/frame-shape? object)
(fix-frames-selrects)
(and (empty? (:points object)) (not= (:id object) uuid/zero))
(fix-empty-points)
;; Setup an empty transformation to re-calculate selrects
;; and points data
:always
(->
;; Setup an empty transformation to re-calculate selrects
;; and points data
(assoc :modifiers {:displacement (gmt/matrix)})
(gsh/transform-shape))
(-> (assoc :modifiers {:displacement (gmt/matrix)})
(gsh/transform-shape))))
))
(update-page [page]
(update page :objects d/update-vals update-object))]
(update-page [_ page]
(update page :objects #(d/mapm update-object %)))]
(update data :pages-index #(d/mapm update-page %))))
(update data :pages-index d/update-vals update-page)))
;; We did rollback version 4 migration.
;; Keep this in order to remember the next version to be 5
@ -124,61 +120,55 @@
;; Put the id of the local file in :component-file in instances of local components
(defmethod migrate 5
[data]
(letfn [(update-object [_ object]
(letfn [(update-object [object]
(if (and (some? (:component-id object))
(nil? (:component-file object)))
(assoc object :component-file (:id data))
object))
(update-page [_ page]
(update page :objects #(d/mapm update-object %)))]
(update data :pages-index #(d/mapm update-page %))))
(defn fix-line-paths
"Fixes issues with selrect/points for shapes with width/height = 0 (line-like paths)"
[_ shape]
(if (= (:type shape) :path)
(let [{:keys [width height]} (gsh/points->rect (:points shape))]
(if (or (mth/almost-zero? width) (mth/almost-zero? height))
(let [selrect (gsh/content->selrect (:content shape))
points (gsh/rect->points selrect)
transform (gmt/matrix)
transform-inv (gmt/matrix)]
(assoc shape
:selrect selrect
:points points
:transform transform
:transform-inverse transform-inv))
shape))
shape))
(update-page [page]
(update page :objects d/update-vals update-object))]
(update data :pages-index d/update-vals update-page)))
(defmethod migrate 6
[data]
(letfn [(update-container [_ container]
(-> container
(update :objects #(d/mapm fix-line-paths %))))]
;; Fixes issues with selrect/points for shapes with width/height = 0 (line-like paths)"
(letfn [(fix-line-paths [shape]
(if (= (:type shape) :path)
(let [{:keys [width height]} (gsh/points->rect (:points shape))]
(if (or (mth/almost-zero? width) (mth/almost-zero? height))
(let [selrect (gsh/content->selrect (:content shape))
points (gsh/rect->points selrect)
transform (gmt/matrix)
transform-inv (gmt/matrix)]
(assoc shape
:selrect selrect
:points points
:transform transform
:transform-inverse transform-inv))
shape))
shape))
(update-container [container]
(update container :objects d/update-vals fix-line-paths))]
(-> data
(update :components #(d/mapm update-container %))
(update :pages-index #(d/mapm update-container %)))))
(update :pages-index d/update-vals update-container)
(update :components d/update-vals update-container))))
;; Remove interactions pointing to deleted frames
(defmethod migrate 7
[data]
(letfn [(update-object [page _ object]
(letfn [(update-object [page object]
(d/update-when object :interactions
(fn [interactions]
(filterv #(get-in page [:objects (:destination %)])
interactions))))
(fn [interactions]
(filterv #(get-in page [:objects (:destination %)]) interactions))))
(update-page [_ page]
(update page :objects #(d/mapm (partial update-object page) %)))]
(update data :pages-index #(d/mapm update-page %))))
(update-page [page]
(update page :objects d/update-vals (partial update-object page)))]
(update data :pages-index d/update-vals update-page)))
;; Remove groups without any shape, both in pages and components
@ -210,7 +200,7 @@
[(count deleted)
(d/mapm #(clean-parents %2 deleted) result)]))))
(clean-container [_ container]
(clean-container [container]
(loop [n 0
objects (:objects container)]
(let [[deleted objects] (clean-objects objects)]
@ -219,8 +209,8 @@
(assoc container :objects objects)))))]
(-> data
(update :pages-index #(d/mapm clean-container %))
(d/update-when :components #(d/mapm clean-container %)))))
(update :pages-index d/update-vals clean-container)
(update :components d/update-vals clean-container))))
(defmethod migrate 9
[data]
@ -252,35 +242,35 @@
(defmethod migrate 10
[data]
(letfn [(update-page [_ page]
(letfn [(update-page [page]
(d/update-in-when page [:objects uuid/zero] dissoc :points :selrect))]
(update data :pages-index #(d/mapm update-page %))))
(update data :pages-index d/update-vals update-page)))
(defmethod migrate 11
[data]
(letfn [(update-object [objects _id shape]
(if (= :frame (:type shape))
(letfn [(update-object [objects shape]
(if (cph/frame-shape? shape)
(d/update-when shape :shapes (fn [shapes]
(filterv (fn [id] (contains? objects id)) shapes)))
shape))
(update-page [_ page]
(update page :objects #(d/mapm (partial update-object %) %)))]
(update data :pages-index #(d/mapm update-page %))))
(update-page [page]
(update page :objects (fn [objects]
(d/update-vals objects (partial update-object objects)))))]
(update data :pages-index d/update-vals update-page)))
(defmethod migrate 12
[data]
(letfn [(update-grid [_key grid]
(letfn [(update-grid [grid]
(cond-> grid
(= :auto (:size grid))
(assoc :size nil)))
(update-page [_id page]
(d/update-in-when page [:options :saved-grids] #(d/mapm update-grid %)))]
(update-page [page]
(d/update-in-when page [:options :saved-grids] d/update-vals update-grid))]
(update data :pages-index #(d/mapm update-page %))))
(update data :pages-index d/update-vals update-page)))
;; Add rx and ry to images
(defmethod migrate 13
@ -291,83 +281,124 @@
(assoc :rx 0)
(assoc :ry 0))
shape))
(update-object [_ object]
(update-object [object]
(cond-> object
(= :image (:type object))
(cph/image-shape? object)
(fix-radius)))
(update-page [_ page]
(update page :objects #(d/mapm update-object %)))]
(update-page [page]
(update page :objects d/update-vals update-object))]
(update data :pages-index #(d/mapm update-page %))))
(update data :pages-index d/update-vals update-page)))
(defn set-fills
[shape]
(let [attrs {:fill-color (:fill-color shape)
:fill-color-gradient (:fill-color-gradient shape)
:fill-color-ref-file (:fill-color-ref-file shape)
:fill-color-ref-id (:fill-color-ref-id shape)
:fill-opacity (:fill-opacity shape)}
clean-attrs (d/without-nils attrs)]
(cond-> shape
(d/not-empty? clean-attrs)
(assoc :fills [clean-attrs]))))
;; Add fills to shapes
(defmethod migrate 14
[data]
(letfn [(update-object [_ object]
(cond-> object
(and (not (= :text (:type object))) (nil? (:fills object)))
(set-fills)))
(letfn [(process-shape [shape]
(let [fill-color (str/upper (:fill-color shape))
fill-opacity (:fill-opacity shape)]
(cond-> shape
(and (= 1 fill-opacity)
(or (= "#B1B2B5" fill-color)
(= "#7B7D85" fill-color)))
(dissoc :fill-color :fill-opacity))))
(update-page [_ page]
(update page :objects #(d/mapm update-object %)))]
(update data :pages-index #(d/mapm update-page %))))
(update-container [{:keys [objects] :as container}]
(loop [objects objects
shapes (->> (vals objects)
(filter cph/image-shape?))]
(if-let [shape (first shapes)]
(let [{:keys [id frame-id] :as shape'} (process-shape shape)]
(if (identical? shape shape')
(recur objects (rest shapes))
(recur (-> objects
(assoc id shape')
(d/update-when frame-id dissoc :thumbnail))
(rest shapes))))
(assoc container :objects objects))))]
(defn set-strokes
[shape]
(let [attrs {:stroke-style (:stroke-style shape)
:stroke-alignment (:stroke-alignment shape)
:stroke-width (:stroke-width shape)
:stroke-color (:stroke-color shape)
:stroke-color-ref-id (:stroke-color-ref-id shape)
:stroke-color-ref-file (:stroke-color-ref-file shape)
:stroke-opacity (:stroke-opacity shape)
:stroke-color-gradient (:stroke-color-gradient shape)
:stroke-cap-start (:stroke-cap-start shape)
:stroke-cap-end (:stroke-cap-end shape)}
(-> data
(update :pages-index d/update-vals update-container)
(update :components d/update-vals update-container))))
clean-attrs (d/without-nils attrs)]
(cond-> shape
(d/not-empty? clean-attrs)
(assoc :strokes [clean-attrs]))))
;; Add strokes to shapes
(defmethod migrate 15
[data]
(letfn [(update-object [_ object]
(cond-> object
(and (not (= :text (:type object))) (nil? (:strokes object)))
(set-strokes)))
(update-page [_ page]
(update page :objects #(d/mapm update-object %)))]
(update data :pages-index #(d/mapm update-page %))))
;; Add fills and strokes to components
(defmethod migrate 15 [data] data)
;; Add fills and strokes
(defmethod migrate 16
[data]
(letfn [(update-object [_ object]
(cond-> object
(and (not (= :text (:type object))) (nil? (:strokes object)))
(set-strokes)
(letfn [(assign-fills [shape]
(let [attrs {:fill-color (:fill-color shape)
:fill-color-gradient (:fill-color-gradient shape)
:fill-color-ref-file (:fill-color-ref-file shape)
:fill-color-ref-id (:fill-color-ref-id shape)
:fill-opacity (:fill-opacity shape)}
clean-attrs (d/without-nils attrs)]
(cond-> shape
(d/not-empty? clean-attrs)
(assoc :fills [clean-attrs]))))
(assign-strokes [shape]
(let [attrs {:stroke-style (:stroke-style shape)
:stroke-alignment (:stroke-alignment shape)
:stroke-width (:stroke-width shape)
:stroke-color (:stroke-color shape)
:stroke-color-ref-id (:stroke-color-ref-id shape)
:stroke-color-ref-file (:stroke-color-ref-file shape)
:stroke-opacity (:stroke-opacity shape)
:stroke-color-gradient (:stroke-color-gradient shape)
:stroke-cap-start (:stroke-cap-start shape)
:stroke-cap-end (:stroke-cap-end shape)}
clean-attrs (d/without-nils attrs)]
(cond-> shape
(d/not-empty? clean-attrs)
(assoc :strokes [clean-attrs]))))
(update-object [object]
(cond-> object
(and (not (cph/text-shape? object))
(not (contains? object :strokes)))
(assign-strokes)
(and (not (cph/text-shape? object))
(not (contains? object :fills)))
(assign-fills)))
(update-container [container]
(update container :objects d/update-vals update-object))]
(and (not (= :text (:type object))) (nil? (:fills object)))
(set-fills)))
(update-container [_ container]
(update container :objects #(d/mapm update-object %)))]
(-> data
(update :components #(d/mapm update-container %)))))
(update :pages-index d/update-vals update-container)
(update :components d/update-vals update-container))))
(defmethod migrate 17
[data]
(letfn [(affected-object? [object]
(and (cph/image-shape? object)
(some? (:fills object))
(= 1 (count (:fills object)))
(some? (:fill-color object))
(some? (:fill-opacity object))
(let [color-old (str/upper (:fill-color object))
color-new (str/upper (get-in object [:fills 0 :fill-color]))
opacity-old (:fill-opacity object)
opacity-new (get-in object [:fills 0 :fill-opacity])]
(and (= color-old color-new)
(or (= "#B1B2B5" color-old)
(= "#7B7D85" color-old))
(= 1 opacity-old opacity-new)))))
(update-object [object]
(cond-> object
(affected-object? object)
(assoc :fills [])))
(update-container [container]
(update container :objects d/update-vals update-object))]
(-> data
(update :pages-index d/update-vals update-container)
(update :components d/update-vals update-container))))
;; TODO: pending to do a migration for delete already not used fill
;; and stroke props. This should be done for >1.14.x version.

View file

@ -63,16 +63,16 @@
(filter match?)
(seq))))
(defn ^boolean is-text-node?
(defn is-text-node?
[node]
(and (string? (:text node))
(not= (:text node) "")))
(defn ^boolean is-paragraph-node?
(defn is-paragraph-node?
[node]
(= "paragraph" (:type node)))
(defn ^boolean is-root-node?
(defn is-root-node?
[node]
(= "root" (:type node)))

View file

@ -38,7 +38,7 @@
:components {}
:version 7}
res (cpm/migrate-data data nil 8)]
res (cpm/migrate-data data 8)]
;; (pprint data)
;; (pprint res)
@ -81,7 +81,7 @@
(let [id (uuid/custom 1 2)]
(into [] (remove #(= id %)) shapes)))))
res (cpm/migrate-data data nil 8)]
res (cpm/migrate-data data 8)]
;; (pprint res)
;; (pprint expect)

View file

@ -1,6 +1,7 @@
{:paths ["src" "vendor" "resources" "test"]
:deps
{penpot/common {:local/root "../common"}
org.clojure/clojure {:mvn/version "1.10.3"}
binaryage/devtools {:mvn/version "RELEASE"}
metosin/reitit-core {:mvn/version "0.5.16"}
funcool/beicon {:mvn/version "2021.07.05-1"}

View file

@ -3,6 +3,7 @@
{penpot/common
{:local/root "../common"}
org.clojure/clojure {:mvn/version "1.10.3"}
binaryage/devtools {:mvn/version "RELEASE"}
metosin/reitit-core {:mvn/version "0.5.15"}

View file

@ -1209,7 +1209,8 @@
}
.modal-container {
background-image: url("../images/deco-left.png"), url("../images/deco-right.png");
background-image: url("../images/deco-left.png"),
url("../images/deco-right.png");
background-repeat: no-repeat;
background-position: 10% 50px, 90% 50px;
background-size: 65px;
@ -1236,8 +1237,18 @@
--checkbox-border-radius: 3px;
--dropdown-option-background-color: rgba(0, 195, 139, 1);
--dropdown-option-active-background-color: rgba(0, 138, 98, 1);
--invalid-field-background-color: rgba(238.51780000000002, 205.7178, 204.11780000000002, 1);
--message-fail-background-color: rgba(238.51780000000002, 205.7178, 204.11780000000002, 1);
--invalid-field-background-color: rgba(
238.51780000000002,
205.7178,
204.11780000000002,
1
);
--message-fail-background-color: rgba(
238.51780000000002,
205.7178,
204.11780000000002,
1
);
--message-success-background-color: rgba(171, 232, 197, 1);
}
}

View file

@ -523,6 +523,12 @@
right: 0.5rem;
left: unset;
top: 0;
.context-menu-action {
overflow-wrap: break-word;
min-width: 223px;
white-space: break-spaces;
}
}
}
}

View file

@ -194,7 +194,8 @@
(ptk/reify ::initialize-page
ptk/WatchEvent
(watch [_ state _]
(when-not (contains? (get-in state [:workspace-data :pages-index]) page-id)
(if (contains? (get-in state [:workspace-data :pages-index]) page-id)
(rx/of (dwp/preload-data-uris))
(let [default-page-id (get-in state [:workspace-data :pages 0])]
(rx/of (go-to-page default-page-id)))))
@ -1356,26 +1357,28 @@
edit-id (get-in state [:workspace-local :edition])
is-editing-text? (and edit-id (= :text (get-in objects [edit-id :type])))]
(cond
(and (string? text-data)
(str/includes? text-data "<svg"))
(rx/of (paste-svg text-data))
;; Some paste events can be fired while we're editing a text
;; we forbid that scenario so the default behaviour is executed
(when-not is-editing-text?
(cond
(and (string? text-data)
(str/includes? text-data "<svg"))
(rx/of (paste-svg text-data))
(seq image-data)
(rx/from (map paste-image image-data))
(seq image-data)
(rx/from (map paste-image image-data))
(coll? decoded-data)
(->> (rx/of decoded-data)
(rx/filter #(= :copied-shapes (:type %)))
(rx/map #(paste-shape % in-viewport?)))
(coll? decoded-data)
(->> (rx/of decoded-data)
(rx/filter #(= :copied-shapes (:type %)))
(rx/map #(paste-shape % in-viewport?)))
;; Some paste events can be fired while we're editing a text
;; we forbid that scenario so the default behaviour is executed
(and (string? text-data) (not is-editing-text?))
(rx/of (paste-text text-data))
(string? text-data)
(rx/of (paste-text text-data))
:else
(rx/empty))))
:else
(rx/empty)))
(catch :default err
(js/console.error "Clipboard error:" err))))))

View file

@ -14,6 +14,7 @@
[app.common.spec.change :as spec.change]
[app.common.spec.file :as spec.file]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
[app.main.data.fonts :as df]
@ -653,6 +654,9 @@
frame-changes (->> stream
(rx/filter dch/commit-changes?)
;; Async so we wait for additional side-effects of commit-changes
(rx/observe-on :async)
(rx/filter (comp not thumbnail-change?))
(rx/with-latest-from objects-stream)
(rx/map extract-frame-changes)
@ -678,3 +682,26 @@
(rx/buffer-until (->> frame-changes (rx/debounce 1000)))
(rx/flat-map #(reduce set/union %))
(rx/map #(update-frame-thumbnail %)))))))))
(defn preload-data-uris
"Preloads the image data so it's ready when necesary"
[]
(ptk/reify ::preload-data-uris
ptk/WatchEvent
(watch [_ state _]
(let [extract-urls
(fn [{:keys [metadata fill-image]}]
(cond
(some? metadata)
[(cfg/resolve-file-media metadata)]
(some? fill-image)
[(cfg/resolve-file-media fill-image)]))
uris (into #{}
(comp (mapcat extract-urls)
(filter some?))
(vals (wsh/lookup-page-objects state)))]
(->> (rx/from uris)
(rx/merge-map #(http/fetch-data-uri % false))
(rx/ignore))))))

View file

@ -110,7 +110,10 @@
(rx/dedupe)
(rx/map #(select-shapes-by-current-selrect preserve? ignore-groups?))))
(rx/of (update-selrect nil)))))))
(->> (rx/of (update-selrect nil))
;; We need the async so the current event finishes before updating the selrect
;; otherwise the `on-click` event will trigger with a `nil` selrect
(rx/observe-on :async)))))))
;; --- Toggle shape's selection status (selected or deselected)
@ -123,13 +126,7 @@
(ptk/reify ::select-shape
ptk/UpdateEvent
(update [_ state]
(update-in state [:workspace-local :selected]
(fn [selected]
(if-not toggle?
(conj (d/ordered-set) id)
(if (contains? selected id)
(disj selected id)
(conj selected id))))))
(update-in state [:workspace-local :selected] d/toggle-selection id toggle?))
ptk/WatchEvent
(watch [_ state _]
@ -506,6 +503,7 @@
id-duplicated (when (= (count selected) 1) (first selected))]
;; Warning: This order is important for the focus mode.
(rx/of (dch/commit-changes changes)
(select-shapes selected)
(memorize-duplicated id-original id-duplicated)))))))

View file

@ -592,31 +592,46 @@
(defn start-move-selected
"Enter mouse move mode, until mouse button is released."
[]
(ptk/reify ::start-move-selected
ptk/WatchEvent
(watch [_ state stream]
(let [initial (deref ms/mouse-position)
selected (wsh/lookup-selected state {:omit-blocked? true})
stopper (rx/filter ms/mouse-up? stream)
zoom (get-in state [:workspace-local :zoom] 1)]
(when-not (empty? selected)
(->> ms/mouse-position
(rx/map #(gpt/to-vec initial %))
(rx/map #(gpt/length %))
(rx/filter #(> % (/ 10 zoom)))
(rx/take 1)
(rx/with-latest vector ms/mouse-position-alt)
(rx/mapcat
(fn [[_ alt?]]
(if alt?
;; When alt is down we start a duplicate+move
(rx/of (start-move-duplicate initial)
(dws/duplicate-selected false))
;; Otherwise just plain old move
(rx/of (start-move initial selected)))))
(rx/take-until stopper)))))))
([]
(start-move-selected nil false))
([id shift?]
(ptk/reify ::start-move-selected
ptk/WatchEvent
(watch [_ state stream]
(let [initial (deref ms/mouse-position)
stopper (rx/filter ms/mouse-up? stream)
zoom (get-in state [:workspace-local :zoom] 1)
;; We toggle the selection so we don't have to wait for the event
selected
(cond-> (wsh/lookup-selected state {:omit-blocked? true})
(some? id)
(d/toggle-selection id shift?))]
(when (or (d/not-empty? selected) (some? id))
(->> ms/mouse-position
(rx/map #(gpt/to-vec initial %))
(rx/map #(gpt/length %))
(rx/filter #(> % (/ 10 zoom)))
(rx/take 1)
(rx/with-latest vector ms/mouse-position-alt)
(rx/mapcat
(fn [[_ alt?]]
(rx/concat
(if (some? id)
(rx/of (dws/select-shape id shift?))
(rx/empty))
(if alt?
;; When alt is down we start a duplicate+move
(rx/of (start-move-duplicate initial)
(dws/duplicate-selected false))
;; Otherwise just plain old move
(rx/of (start-move initial selected))))))
(rx/take-until stopper))))))))
(defn- start-move-duplicate
[from-position]
(ptk/reify ::start-move-duplicate

View file

@ -73,44 +73,44 @@
parse-value
(mf/use-callback
(mf/deps ref min-val max-val value nillable default-val)
(fn []
(let [input-node (mf/ref-val ref)
new-value (-> (dom/get-value input-node)
(str/strip-suffix ".")
(sm/expr-eval value))]
(cond
(d/num? new-value)
(-> new-value
(cljs.core/max us/min-safe-int)
(cljs.core/min us/max-safe-int)
(cond->
(d/num? min-val)
(cljs.core/max min-val)
(mf/deps ref min-val max-val value nillable default-val)
(fn []
(let [input-node (mf/ref-val ref)
new-value (-> (dom/get-value input-node)
(str/strip-suffix ".")
(sm/expr-eval value))]
(cond
(d/num? new-value)
(-> new-value
(cljs.core/max (/ us/min-safe-int 2))
(cljs.core/min (/ us/max-safe-int 2))
(cond->
(d/num? min-val)
(cljs.core/max min-val)
(d/num? max-val)
(cljs.core/min max-val)))
(d/num? max-val)
(cljs.core/min max-val)))
nillable
default-val
nillable
default-val
:else value))))
:else value))))
update-input
(mf/use-callback
(mf/deps ref)
(fn [new-value]
(let [input-node (mf/ref-val ref)]
(dom/set-value! input-node (fmt/format-number new-value)))))
(mf/deps ref)
(fn [new-value]
(let [input-node (mf/ref-val ref)]
(dom/set-value! input-node (fmt/format-number new-value)))))
apply-value
(mf/use-callback
(mf/deps on-change update-input value)
(fn [new-value]
(mf/set-ref-val! dirty-ref false)
(when (and (not= new-value value) (some? on-change))
(on-change new-value))
(update-input new-value)))
(mf/deps on-change update-input value)
(fn [new-value]
(mf/set-ref-val! dirty-ref false)
(when (and (not= new-value value) (some? on-change))
(on-change new-value))
(update-input new-value)))
set-delta
(mf/use-callback

View file

@ -33,10 +33,10 @@
(let [xf-get-bounds (comp (map #(get objects %)) (map #(calc-bounds % objects)))
padding (filters/calculate-padding object)
obj-bounds (-> (filters/get-filters-bounds object)
(update :x - padding)
(update :y - padding)
(update :width + (* 2 padding))
(update :height + (* 2 padding)))]
(update :x - (:horizontal padding))
(update :y - (:vertical padding))
(update :width + (* 2 (:horizontal padding)))
(update :height + (* 2 (:vertical padding))))]
(cond
(and (= :group (:type object))

View file

@ -175,12 +175,12 @@
(obj/set! styles "fill" (str "url(#fill-0-" render-id ")"))
;; imported svgs can have fill and fill-opacity attributes
(obj/contains? svg-styles "fill")
(and (some? svg-styles) (obj/contains? svg-styles "fill"))
(-> styles
(obj/set! "fill" (obj/get svg-styles "fill"))
(obj/set! "fillOpacity" (obj/get svg-styles "fillOpacity")))
(obj/contains? svg-attrs "fill")
(and (some? svg-attrs) (obj/contains? svg-attrs "fill"))
(-> styles
(obj/set! "fill" (obj/get svg-attrs "fill"))
(obj/set! "fillOpacity" (obj/get svg-attrs "fillOpacity")))

View file

@ -9,6 +9,7 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.shapes :as gsh]
[app.common.pages.helpers :as cph]
[app.main.ui.context :as muc]
[app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.gradients :as grad]
@ -327,8 +328,12 @@
(obj/clone))
props (cond-> props
(d/not-empty? (:shadow shape))
(obj/set! "filter" (dm/fmt "url(#filter_%)" render-id)))
(or
;; There are any shadows
(and (d/not-empty? (:shadow shape)) (not (cph/frame-shape? shape)))
;; There are no strokes and a blur
(and (some? (:blur shape)) (not (cph/frame-shape? shape)) (empty? (:strokes shape))))
(obj/set! "filter" (dm/fmt "url(#filter_%)" render-id)))
svg-defs (:svg-defs shape {})
svg-attrs (:svg-attrs shape {})
@ -345,7 +350,7 @@
(obj/without ["fill" "fillOpacity"])))]
(obj/set! props "fill" (dm/fmt "url(#fill-0-%)" render-id)))
(obj/contains? svg-styles "fill")
(and (some? svg-styles) (obj/contains? svg-styles "fill"))
(let [style
(-> (obj/get props "style")
(obj/clone)
@ -354,7 +359,7 @@
(-> props
(obj/set! "style" style)))
(obj/contains? svg-attrs "fill")
(and (some? svg-attrs) (obj/contains? svg-attrs "fill"))
(let [style
(-> (obj/get props "style")
(obj/clone)
@ -374,10 +379,7 @@
(cond-> (obj/merge! props fill-props)
(some? style)
(obj/set! "style" style)))
:else
props)))
(obj/set! "style" style))))))
(defn build-stroke-props [position child value render-id]
(let [props (-> (obj/get child "props")
@ -391,7 +393,19 @@
(obj/set! "fillOpacity" "none")))
(add-style (obj/get (attrs/extract-stroke-attrs value position render-id) "style")))))
(mf/defc shape-custom-strokes
(mf/defc shape-fills
{::mf/wrap-props false}
[props]
(let [child (obj/get props "children")
shape (obj/get props "shape")
elem-name (obj/get child "type")
render-id (mf/use-ctx muc/render-ctx)]
[:g {:id (dm/fmt "fills-%" (:id shape))}
[:> elem-name (build-fill-props shape child render-id)]]))
(mf/defc shape-strokes
{::mf/wrap-props false}
[props]
(let [child (obj/get props "children")
@ -401,13 +415,9 @@
stroke-props (-> (obj/new)
(obj/set! "id" (dm/fmt "strokes-%" (:id shape)))
(cond->
(some? (:blur shape))
(and (some? (:blur shape)) (not (cph/frame-shape? shape)))
(obj/set! "filter" (dm/fmt "url(#filter_blur_%)" render-id))))]
[:*
[:g {:id (dm/fmt "fills-%" (:id shape))}
[:> elem-name (build-fill-props shape child render-id)]]
(when
(d/not-empty? (:strokes shape))
[:> :g stroke-props
@ -416,3 +426,16 @@
shape (assoc value :points (:points shape))]
[:& shape-custom-stroke {:shape shape :index index}
[:> elem-name props]]))])]))
(mf/defc shape-custom-strokes
{::mf/wrap-props false}
[props]
(let [child (obj/get props "children")
shape (obj/get props "shape")]
[:*
[:& shape-fills {:shape shape}
child]
[:& shape-strokes {:shape shape}
child]]))

View file

@ -170,7 +170,6 @@
([shape filters blur-value]
(let [svg-root? (and (= :svg-raw (:type shape)) (not= :svg (get-in shape [:content :tag])))
frame? (= :frame (:type shape))
{:keys [x y width height]} (:selrect shape)]
(if svg-root?
;; When is a raw-svg but not the root we use the whole svg as bound for the filter. Is the maximum
@ -183,6 +182,7 @@
(map (partial filter-bounds shape)))
;; We add the selrect so the minimum size will be the selrect
filter-bounds (conj filter-bounds (-> shape :points gsh/points->selrect))
x1 (apply min (map :x1 filter-bounds))
y1 (apply min (map :y1 filter-bounds))
x2 (apply max (map :x2 filter-bounds))
@ -195,18 +195,30 @@
;; We should move the frame filter coordinates because they should be
;; relative with the frame. By default they come as absolute
{:x (if frame? (- x1 x) x1)
:y (if frame? (- y1 y) y1)
{:x x1
:y y1
:width (- x2 x1)
:height (- y2 y1)})))))
(defn calculate-padding [shape]
(let [stroke-width (apply max 0 (map #(case (:stroke-alignment % :center)
:center (/ (:stroke-width % 0) 2)
:outer (:stroke-width % 0)
0) (:strokes shape)))
margin (apply max 0 (map #(gsh/shape-stroke-margin % stroke-width) (:strokes shape)))]
(+ stroke-width margin)))
:center (/ (:stroke-width % 0) 2)
:outer (:stroke-width % 0)
0) (:strokes shape)))
margin (apply max 0 (map #(gsh/shape-stroke-margin % stroke-width) (:strokes shape)))
shadow-width (apply max 0 (map #(case (:style % :drop-shadow)
:drop-shadow (+ (mth/abs (:offset-x %)) (* (:spread %) 2) (* (:blur %) 2) 10)
0) (:shadow shape)))
shadow-height (apply max 0 (map #(case (:style % :drop-shadow)
:drop-shadow (+ (mth/abs (:offset-y %)) (* (:spread %) 2) (* (:blur %) 2) 10)
0) (:shadow shape)))]
{:horizontal (+ stroke-width margin shadow-width)
:vertical (+ stroke-width margin shadow-height)}))
(defn change-filter-in
"Adds the previous filter as `filter-in` parameter"
@ -220,10 +232,10 @@
bounds (get-filters-bounds shape filters (or (-> shape :blur :value) 0))
padding (calculate-padding shape)
selrect (:selrect shape)
filter-x (/ (- (:x bounds) (:x selrect) padding) (:width selrect))
filter-y (/ (- (:y bounds) (:y selrect) padding) (:height selrect))
filter-width (/ (+ (:width bounds) (* 2 padding)) (:width selrect))
filter-height (/ (+ (:height bounds) (* 2 padding)) (:height selrect))]
filter-x (/ (- (:x bounds) (:x selrect) (:horizontal padding)) (:width selrect))
filter-y (/ (- (:y bounds) (:y selrect) (:vertical padding)) (:height selrect))
filter-width (/ (+ (:width bounds) (* 2 (:horizontal padding))) (:width selrect))
filter-height (/ (+ (:height bounds) (* 2 (:vertical padding))) (:height selrect))]
(when (> (count filters) 2)
[:filter {:id filter-id
:x filter-x

View file

@ -7,9 +7,10 @@
(ns app.main.ui.shapes.frame
(:require
[app.common.data.macros :as dm]
[app.common.geom.shapes :as gsh]
[app.main.ui.context :as muc]
[app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]]
[app.main.ui.shapes.filters :as filters]
[app.main.ui.shapes.custom-stroke :refer [shape-fills shape-strokes]]
[app.util.object :as obj]
[debug :refer [debug?]]
[rumext.alpha :as mf]))
@ -27,13 +28,12 @@
[{:keys [shape render-id]}]
(when (= :frame (:type shape))
(let [{:keys [x y width height]} shape
padding (filters/calculate-padding shape)
props (-> (attrs/extract-style-attrs shape)
(obj/merge!
#js {:x (- x padding)
:y (- y padding)
:width (+ width (* 2 padding))
:height (+ height (* 2 padding))}))
#js {:x x
:y y
:width width
:height height}))
path? (some? (.-d props))]
[:clipPath {:id (frame-clip-id shape render-id) :class "frame-clip"}
(if path?
@ -63,22 +63,32 @@
(let [childs (unchecked-get props "childs")
shape (unchecked-get props "shape")
{:keys [x y width height]} shape
transform (gsh/transform-matrix shape)
props (-> (attrs/extract-style-attrs shape)
(obj/merge!
#js {:x x
:y y
:transform transform
:width width
:height height
:className "frame-background"}))
path? (some? (.-d props))]
[:*
[:& shape-custom-strokes {:shape shape}
(if path?
[:> :path props]
[:> :rect props])]
(for [item childs]
[:& shape-wrapper {:shape item
:key (dm/str (:id item))}])])))
path? (some? (.-d props))
render-id (mf/use-ctx muc/render-ctx)]
[:*
[:g {:clip-path (frame-clip-url shape render-id)}
[:*
[:& shape-fills {:shape shape}
(if path?
[:> :path props]
[:> :rect props])]
(for [item childs]
[:& shape-wrapper {:shape item
:key (dm/str (:id item))}])
[:& shape-strokes {:shape shape}
(if path?
[:> :path props]
[:> :rect props])]]]])))

View file

@ -50,14 +50,11 @@
wrapper-props
(cond-> wrapper-props
(some #(= (:type shape) %) [:group :svg-raw])
(some #(= (:type shape) %) [:group :svg-raw :frame])
(obj/set! "filter" (filters/filter-str filter-id shape)))
wrapper-props
(cond-> wrapper-props
(= :frame type)
(obj/set! "clipPath" (frame/frame-clip-url shape render-id))
(= :group type)
(attrs/add-style-attrs shape render-id))]

View file

@ -43,24 +43,24 @@
transform-mask? (and (= :mask tag)
(= "userSpaceOnUse" (get attrs :maskUnits "objectBoundingBox")))
attrs (-> attrs
(usvg/update-attr-ids prefix-id)
(usvg/clean-attrs)
;; This clasname will be used to change the transform on the viewport
;; only necessary for groups because shapes have their own transform
(cond-> (and (or transform-gradient?
transform-pattern?
transform-clippath?
transform-filter?
transform-mask?)
(= :group type))
(update :className #(if % (dm/str % " svg-def") "svg-def")))
(cond->
transform-gradient? (add-matrix :gradientTransform transform)
transform-pattern? (add-matrix :patternTransform transform)
transform-clippath? (add-matrix :transform transform)
(or transform-filter?
transform-mask?) (merge attrs bounds)))
attrs
(-> attrs
(usvg/update-attr-ids prefix-id)
(usvg/clean-attrs)
;; This clasname will be used to change the transform on the viewport
;; only necessary for groups because shapes have their own transform
(cond-> (and (or transform-gradient?
transform-pattern?
transform-clippath?
transform-filter?
transform-mask?)
(= :group type))
(update :className #(if % (dm/str % " svg-def") "svg-def")))
(cond->
transform-gradient? (add-matrix :gradientTransform transform)
transform-pattern? (add-matrix :patternTransform transform)
transform-clippath? (add-matrix :transform transform)
(or transform-filter? transform-mask?) (merge bounds)))
[wrapper wrapper-props] (if (= tag :mask)
["g" #js {:className "svg-mask-wrapper"

View file

@ -7,6 +7,7 @@
(ns app.main.ui.shapes.text.fontfaces
(:require
[app.common.data :as d]
[app.common.pages.helpers :as cph]
[app.main.fonts :as fonts]
[app.main.ui.hooks :as hooks]
[app.main.ui.shapes.embed :as embed]
@ -76,14 +77,10 @@
{::mf/wrap-props false
::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]}
[props]
(let [shapes (->> (obj/get props "shapes")
(filterv #(= :text (:type %))))
content (->> shapes (mapv :content))
;; Retrieve the fonts ids used by the text shapes
fonts (->> content
(mapv fonts/get-content-fonts)
(let [;; Retrieve the fonts ids used by the text shapes
fonts (->> (obj/get props "shapes")
(filterv cph/text-shape?)
(mapv (comp fonts/get-content-fonts :content))
(reduce set/union #{})
(hooks/use-equal-memo))]

View file

@ -9,9 +9,12 @@
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.geom.point :as gpt]
[app.common.pages.helpers :as cph]
[app.common.text :as txt]
[app.main.data.comments :as dcm]
[app.main.data.viewer :as dv]
[app.main.data.viewer.shortcuts :as sc]
[app.main.fonts :as fonts]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.hooks :as hooks]
@ -32,12 +35,17 @@
(defn- calculate-size
[frame zoom]
(let [{:keys [_ _ width height]} (filters/get-filters-bounds frame)]
(let [{:keys [_ _ width height]} (filters/get-filters-bounds frame)
padding (filters/calculate-padding frame)
x (- (:horizontal padding))
y (- (:vertical padding))
width (+ width (* 2 (:horizontal padding)))
height (+ height (* 2 (:vertical padding)))]
{:base-width width
:base-height height
:width (* width zoom)
:height (* height zoom)
:vbox (str "0 0 " width " " height)}))
:vbox (str x " " y " " width " " height)}))
(defn- calculate-wrapper
[size1 size2 zoom]
@ -70,6 +78,12 @@
(fn []
(get-in data [:pages page-id])))
text-shapes
(hooks/use-equal-memo
(->> (:objects page)
(vals)
(filter cph/text-shape?)))
zoom (:zoom local)
frames (:frames page)
frame (get frames index)
@ -214,6 +228,13 @@
nil))))
(mf/use-effect
(mf/deps text-shapes)
(fn []
(let [text-nodes (->> text-shapes (mapcat #(txt/node-seq txt/is-text-node? (:content %))))
fonts (into #{} (keep :font-id) text-nodes)]
(run! fonts/ensure-loaded! fonts))))
[:div#viewer-layout {:class (dom/classnames
:force-visible (:show-thumbnails local)
:viewer-layout (not= section :handoff)

View file

@ -21,7 +21,7 @@
(def type->options
{:multiple [:fill :stroke :image :text :shadow :blur]
:frame [:layout :fill :stroke]
:frame [:layout :fill :stroke :shadow :blur]
:group [:layout :svg]
:rect [:layout :fill :stroke :shadow :blur :svg]
:circle [:layout :fill :stroke :shadow :blur :svg]

View file

@ -13,6 +13,7 @@
[app.main.store :as st]
[app.main.ui.shapes.bool :as bool]
[app.main.ui.shapes.circle :as circle]
[app.main.ui.shapes.filters :as filters]
[app.main.ui.shapes.frame :as frame]
[app.main.ui.shapes.group :as group]
[app.main.ui.shapes.image :as image]
@ -190,9 +191,18 @@
frame (get objects (:id frame))
zoom (:zoom local 1)
width (* (:width frame) zoom)
height (* (:height frame) zoom)
vbox (str "0 0 " (:width frame 0) " " (:height frame 0))
{:keys [_ _ width height]} (filters/get-filters-bounds frame)
padding (filters/calculate-padding frame)
x (- (:horizontal padding))
y (- (:vertical padding))
width (+ width (* 2 (:horizontal padding)))
height (+ height (* 2 (:vertical padding)))
vbox (str x " " y " " width " " height)
width (* width zoom)
height (* height zoom)
render (mf/use-memo
(mf/deps objects)

View file

@ -403,9 +403,9 @@
modifier-ids (into [frame-id] (cph/get-children-ids objects frame-id))
objects (reduce update-fn objects modifier-ids)
frame (assoc-in frame [:modifiers :displacement] modifier)
width (* (:width frame) zoom)
height (* (:height frame) zoom)
vbox (str "0 0 " (:width frame 0)
" " (:height frame 0))
wrapper (mf/use-memo

View file

@ -36,7 +36,7 @@
;; NOTE: this is necessary because the `cph/get-component`
;; expects a map of all libraries, including the local one.
libraries (assoc libraries (:id local-file) local-file)
libraries (assoc libraries (:id local-file) {:data local-file})
component (cph/get-component libraries library-id component-id)
show? (some? component-id)

View file

@ -88,6 +88,7 @@
;; REFS
viewport-ref (mf/use-ref nil)
overlays-ref (mf/use-ref nil)
;; VARS
disable-paste (mf/use-var false)
@ -121,7 +122,7 @@
node-editing? (and edition (not= :text (get-in base-objects [edition :type])))
text-editing? (and edition (= :text (get-in base-objects [edition :type])))
on-click (actions/on-click hover selected edition drawing-path? drawing-tool space?)
on-click (actions/on-click hover selected edition drawing-path? drawing-tool space? selrect)
on-context-menu (actions/on-context-menu hover hover-ids)
on-double-click (actions/on-double-click hover hover-ids drawing-path? base-objects edition)
on-drag-enter (actions/on-drag-enter)
@ -169,7 +170,7 @@
disabled-guides? (or drawing-tool transform)]
(hooks/setup-dom-events viewport-ref zoom disable-paste in-viewport?)
(hooks/setup-dom-events viewport-ref overlays-ref zoom disable-paste in-viewport?)
(hooks/setup-viewport-size viewport-ref)
(hooks/setup-cursor cursor alt? mod? space? panning drawing-tool drawing-path? node-editing?)
(hooks/setup-keyboard alt? mod? space?)
@ -179,7 +180,7 @@
(hooks/setup-active-frames base-objects vbox hover active-frames)
[:div.viewport
[:div.viewport-overlays
[:div.viewport-overlays {:ref overlays-ref}
[:& wtr/frame-renderer {:objects base-objects
:background background}]

View file

@ -97,9 +97,7 @@
(st/emit! (dw/handle-area-selection shift? mod?))
(not drawing-tool)
(st/emit! (when (or shift? (not selected?))
(dw/select-shape id shift?))
(dw/start-move-selected)))))))))))
(st/emit! (dw/start-move-selected id shift?)))))))))))
(defn on-move-selected
[hover hover-ids selected space?]
@ -147,27 +145,25 @@
(reset! frame-hover nil))))
(defn on-click
[hover selected edition drawing-path? drawing-tool space?]
[hover selected edition drawing-path? drawing-tool space? selrect]
(mf/use-callback
(mf/deps @hover selected edition drawing-path? drawing-tool @space?)
(mf/deps @hover selected edition drawing-path? drawing-tool @space? selrect)
(fn [event]
(when (or (dom/class? (dom/get-target event) "viewport-controls")
(dom/class? (dom/get-target event) "viewport-selrect"))
(when (and (nil? selrect)
(or (dom/class? (dom/get-target event) "viewport-controls")
(dom/class? (dom/get-target event) "viewport-selrect")))
(let [ctrl? (kbd/ctrl? event)
shift? (kbd/shift? event)
alt? (kbd/alt? event)
meta? (kbd/meta? event)
mod? (kbd/mod? event)
hovering? (some? @hover)
frame? (= :frame (:type @hover))
selected? (contains? selected (:id @hover))]
frame? (= :frame (:type @hover))]
(st/emit! (ms/->MouseEvent :click ctrl? shift? alt? meta?))
(when (and hovering?
(or (not frame?) mod?)
(not @space?)
(not selected?)
(not edition)
(not drawing-path?)
(not drawing-tool))
@ -367,21 +363,23 @@
pt (utils/translate-point-to-viewport viewport zoom raw-pt)]
(rx/push! move-stream pt)))))
(defn on-mouse-wheel [viewport-ref zoom]
(defn on-mouse-wheel [viewport-ref overlays-ref zoom]
(mf/use-callback
(mf/deps zoom)
(fn [event]
(let [viewport (mf/ref-val viewport-ref)
overlays (mf/ref-val overlays-ref)
event (.getBrowserEvent ^js event)
target (dom/get-target event)]
(when (.contains ^js viewport target)
target (dom/get-target event)
mod? (kbd/mod? event)]
(when (or (dom/is-child? viewport target)
(dom/is-child? overlays target))
(dom/prevent-default event)
(dom/stop-propagation event)
(let [pt (->> (dom/get-client-position event)
(utils/translate-point-to-viewport viewport zoom))
mod? (kbd/mod? event)
delta-mode (.-deltaMode ^js event)
unit (cond

View file

@ -27,11 +27,11 @@
[rumext.alpha :as mf])
(:import goog.events.EventType))
(defn setup-dom-events [viewport-ref zoom disable-paste in-viewport?]
(defn setup-dom-events [viewport-ref overlays-ref zoom disable-paste in-viewport?]
(let [on-key-down (actions/on-key-down)
on-key-up (actions/on-key-up)
on-mouse-move (actions/on-mouse-move viewport-ref zoom)
on-mouse-wheel (actions/on-mouse-wheel viewport-ref zoom)
on-mouse-wheel (actions/on-mouse-wheel viewport-ref overlays-ref zoom)
on-paste (actions/on-paste disable-paste in-viewport?)]
(mf/use-layout-effect
(mf/deps on-key-down on-key-up on-mouse-move on-mouse-wheel on-paste)

View file

@ -530,3 +530,8 @@
(when onfinish
(set! (.-onfinish animation) onfinish)))))
(defn is-child?
[^js node ^js candidate]
(and (some? node)
(some? candidate)
(.contains node candidate)))