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

This commit is contained in:
alonso.torres 2022-05-20 11:10:14 +02:00
commit 235d3dbf3d
110 changed files with 1833 additions and 1006 deletions

View file

@ -55,8 +55,11 @@
### :bug: Bugs fixed ### :bug: Bugs fixed
- Fix typo in viewer comment section [Taiga #3401](https://tree.taiga.io/project/penpot/issue/3401)
- Do not show team-up modal for users already on a team [Taiga #3311](https://tree.taiga.io/project/penpot/issue/3311)
- Constraints are not well assigned when default and multiselection [Taiga #3069](https://tree.taiga.io/project/penpot/issue/3069) - Constraints are not well assigned when default and multiselection [Taiga #3069](https://tree.taiga.io/project/penpot/issue/3069)
- Duplicate artboards create new flows if needed [Taiga #2221](https://tree.taiga.io/project/penpot/issue/2221) - Duplicate artboards create new flows if needed [Taiga #2221](https://tree.taiga.io/project/penpot/issue/2221)
- Round the size values on handoff to two decimals [Taiga #3227](https://tree.taiga.io/project/penpot/issue/3227)
- Fix paste shapes while editing text [Taiga #2396](https://tree.taiga.io/project/penpot/issue/2396) - Fix paste shapes while editing text [Taiga #2396](https://tree.taiga.io/project/penpot/issue/2396)
- Round the size values on handoff to two decimals [Taiga #3227](https://tree.taiga.io/project/penpot/issue/3227) - Round the size values on handoff to two decimals [Taiga #3227](https://tree.taiga.io/project/penpot/issue/3227)
- Fix blend modes ignored in component updates [Taiga #2626](https://tree.taiga.io/project/penpot/issue/2626) - Fix blend modes ignored in component updates [Taiga #2626](https://tree.taiga.io/project/penpot/issue/2626)

View file

@ -7,6 +7,7 @@
(ns app.http.feedback (ns app.http.feedback
"A general purpose feedback module." "A general purpose feedback module."
(:require (:require
[app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
[app.config :as cf] [app.config :as cf]
@ -44,7 +45,8 @@
[{:keys [pool] :as cfg} {:keys [profile-id] :as request}] [{:keys [pool] :as cfg} {:keys [profile-id] :as request}]
(let [ftoken (cf/get :feedback-token ::no-token) (let [ftoken (cf/get :feedback-token ::no-token)
token (yrq/get-header request "x-feedback-token") token (yrq/get-header request "x-feedback-token")
params (::yrq/params request)] params (d/merge (:params request)
(:body-params request))]
(cond (cond
(uuid? profile-id) (uuid? profile-id)
(let [profile (profile/retrieve-profile-data pool profile-id) (let [profile (profile/retrieve-profile-data pool profile-id)

View file

@ -223,6 +223,9 @@
{:name "0071-add-file-object-thumbnail-table" {:name "0071-add-file-object-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0071-add-file-object-thumbnail-table.sql")} :fn (mg/resource "app/migrations/sql/0071-add-file-object-thumbnail-table.sql")}
{:name "0072-mod-file-object-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0072-mod-file-object-thumbnail-table.sql")}
]) ])

View file

@ -0,0 +1,4 @@
TRUNCATE TABLE file_object_thumbnail;
ALTER TABLE file_object_thumbnail
ALTER COLUMN object_id TYPE text;

View file

@ -487,7 +487,7 @@
update set data = ?;") update set data = ?;")
(s/def ::data (s/nilable ::us/string)) (s/def ::data (s/nilable ::us/string))
(s/def ::object-id ::us/uuid) (s/def ::object-id ::us/string)
(s/def ::upsert-file-object-thumbnail (s/def ::upsert-file-object-thumbnail
(s/keys :req-un [::profile-id ::file-id ::object-id ::data])) (s/keys :req-un [::profile-id ::file-id ::object-id ::data]))

View file

@ -12,7 +12,7 @@
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.rpc.mutations.teams :as teams] [app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile] [app.rpc.queries.profile :as profile]
[app.util.services :as sv] [app.util.services :as sv]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str])) [cuerdas.core :as str]))
@ -114,15 +114,25 @@
{:is-active true} {:is-active true}
{:id member-id})) {:id member-id}))
(assoc member :is-active true) (assoc member :is-active true)
;; Delete the invitation ;; Delete the invitation
(db/delete! conn :team-invitation (db/delete! conn :team-invitation
{:team-id team-id :email-to (str/lower member-email)}))) {:team-id team-id :email-to (str/lower member-email)})))
(defmethod process-token :team-invitation (defmethod process-token :team-invitation
[cfg {:keys [profile-id token]} {:keys [member-id] :as claims}] [cfg {:keys [profile-id token]} {:keys [member-id] :as claims}]
(us/assert ::team-invitation-claims claims) (us/assert ::team-invitation-claims claims)
#_(let [conn (:conn cfg)
team-id (:team-id claims)
member-email (:member-email claims)
invitation (db/get-by-params conn :team-invitation
{:team-id team-id :email-to (str/lower member-email)}
{:check-not-found false})]
(when (nil? invitation)
(ex/raise :type :validation
:code :invalid-token)))
(cond (cond
;; This happens when token is filled with member-id and current ;; This happens when token is filled with member-id and current
;; user is already logged in with exactly invited account. ;; user is already logged in with exactly invited account.

View file

@ -197,13 +197,13 @@
(->> (db/exec! pool [sql file-id]) (->> (db/exec! pool [sql file-id])
(d/index-by :object-id :data)))) (d/index-by :object-id :data))))
([{:keys [pool]} file-id frame-ids] ([{:keys [pool]} file-id object-ids]
(with-open [conn (db/open pool)] (with-open [conn (db/open pool)]
(let [sql (str/concat (let [sql (str/concat
"select object_id, data " "select object_id, data "
" from file_object_thumbnail" " from file_object_thumbnail"
" where file_id=? and object_id = ANY(?)") " where file_id=? and object_id = ANY(?)")
ids (db/create-array conn "uuid" (seq frame-ids))] ids (db/create-array conn "text" (seq object-ids))]
(->> (db/exec! conn [sql file-id ids]) (->> (db/exec! conn [sql file-id ids])
(d/index-by :object-id :data)))))) (d/index-by :object-id :data))))))
@ -298,19 +298,21 @@
;; function responsible of assoc available thumbnails ;; function responsible of assoc available thumbnails
;; to frames and remove all children shapes from objects if ;; to frames and remove all children shapes from objects if
;; thumbnails is available ;; thumbnails is available
(assoc-thumbnails [objects thumbnails] (assoc-thumbnails [objects page-id thumbnails]
(loop [objects objects (loop [objects objects
frames (filter cph/frame-shape? (vals objects))] frames (filter cph/frame-shape? (vals objects))]
(if-let [{:keys [id] :as frame} (first frames)] (if-let [frame (-> frames first)]
(let [frame (if-let [thumb (get thumbnails id)] (let [frame-id (:id frame)
object-id (str page-id frame-id)
frame (if-let [thumb (get thumbnails object-id)]
(assoc frame :thumbnail thumb :shapes []) (assoc frame :thumbnail thumb :shapes [])
(dissoc frame :thumbnail))] (dissoc frame :thumbnail))]
(if (:thumbnail frame) (if (:thumbnail frame)
(recur (-> (assoc objects id frame) (recur (-> (assoc objects frame-id frame)
(d/without-keys (cph/get-children-ids objects id))) (d/without-keys (cph/get-children-ids objects frame-id)))
(rest frames)) (rest frames))
(recur (assoc objects id frame) (recur (assoc objects frame-id frame)
(rest frames)))) (rest frames))))
objects)))] objects)))]
@ -319,10 +321,11 @@
frame-id (:id frame) frame-id (:id frame)
page-id (or (:page-id frame) page-id (or (:page-id frame)
(-> data :pages first)) (-> data :pages first))
page (dm/get-in data [:pages-index page-id])
obj-ids (or (some-> frame-id list) page (dm/get-in data [:pages-index page-id])
(map :id (cph/get-frames page))) frame-ids (if (some? frame) (list frame-id) (map :id (cph/get-frames (:objects page))))
obj-ids (map #(str page-id %) frame-ids)
thumbs (retrieve-object-thumbnails cfg id obj-ids)] thumbs (retrieve-object-thumbnails cfg id obj-ids)]
(cond-> page (cond-> page
@ -335,7 +338,7 @@
;; Assoc the available thumbnails and prune not visible shapes ;; Assoc the available thumbnails and prune not visible shapes
;; for avoid transfer unnecesary data. ;; for avoid transfer unnecesary data.
:always :always
(update :objects assoc-thumbnails thumbs))))) (update :objects assoc-thumbnails page-id thumbs)))))
(s/def ::file-data-for-thumbnail (s/def ::file-data-for-thumbnail
(s/keys :req-un [::profile-id ::file-id])) (s/keys :req-un [::profile-id ::file-id]))

View file

@ -67,6 +67,38 @@
(db/insert! conn :file params) (db/insert! conn :file params)
(:id file)))))) (:id file))))))
(defn repair-orphaned-components
"We have detected some cases of component instances that are not nested, but
however they have not the :component-root? attribute (so the system considers
them nested). This script fixes this adding them the attribute.
Use it with the update-file function above."
[data]
(let [update-page
(fn [page]
(prn "================= Page:" (:name page))
(letfn [(is-nested? [object]
(and (some? (:component-id object))
(nil? (:component-root? object))))
(is-instance? [object]
(some? (:shape-ref object)))
(get-parent [object]
(get (:objects page) (:parent-id object)))
(update-object [object]
(if (and (is-nested? object)
(not (is-instance? (get-parent object))))
(do
(prn "Orphan:" (:name object))
(assoc object :component-root? true))
object))]
(update page :objects d/update-vals update-object)))]
(update data :pages-index d/update-vals update-page)))
;; (defn check-image-shapes ;; (defn check-image-shapes
;; [{:keys [data] :as file} stats] ;; [{:keys [data] :as file} stats]
;; (println "=> analizing file:" (:name file) (:id file)) ;; (println "=> analizing file:" (:name file) (:id file))

View file

@ -12,6 +12,7 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.pages.helpers :as cph]
[app.common.pages.migrations :as pmg] [app.common.pages.migrations :as pmg]
[app.db :as db] [app.db :as db]
[app.util.blob :as blob] [app.util.blob :as blob]
@ -125,10 +126,14 @@
{:columns [:object-id]}) {:columns [:object-id]})
(into #{} (map :object-id))) (into #{} (map :object-id)))
using (->> (concat (vals (:pages-index data)) get-objects-ids
(vals (:components data))) (fn [{:keys [id objects]}]
(into #{} (comp (map :objects) (->> (cph/get-frames objects)
(mapcat keys)))) (map #(str id (:id %)))))
using (into #{}
(mapcat get-objects-ids)
(vals (:pages-index data)))
unused (set/difference stored using)] unused (set/difference stored using)]
@ -136,7 +141,7 @@
(let [sql (str/concat (let [sql (str/concat
"delete from file_object_thumbnail " "delete from file_object_thumbnail "
" where file_id=? and object_id=ANY(?)") " where file_id=? and object_id=ANY(?)")
res (db/exec-one! conn [sql file-id (db/create-array conn "uuid" unused)])] res (db/exec-one! conn [sql file-id (db/create-array conn "text" unused)])]
(l/debug :hint "delete object thumbnails" :total (:next.jdbc/update-count res)))))) (l/debug :hint "delete object thumbnails" :total (:next.jdbc/update-count res))))))
(defn- clean-file-thumbnails! (defn- clean-file-thumbnails!

View file

@ -527,7 +527,7 @@
(let [data {::th/type :upsert-file-object-thumbnail (let [data {::th/type :upsert-file-object-thumbnail
:profile-id (:id prof) :profile-id (:id prof)
:file-id (:id file) :file-id (:id file)
:object-id frame1-id :object-id (str page-id frame1-id)
:data "random-data-1"} :data "random-data-1"}
{:keys [error result] :as out} (th/mutation! data)] {:keys [error result] :as out} (th/mutation! data)]
@ -553,7 +553,7 @@
(let [data {::th/type :upsert-file-object-thumbnail (let [data {::th/type :upsert-file-object-thumbnail
:profile-id (:id prof) :profile-id (:id prof)
:file-id (:id file) :file-id (:id file)
:object-id frame1-id :object-id (str page-id frame1-id)
:data nil} :data nil}
{:keys [error result] :as out} (th/mutation! data)] {:keys [error result] :as out} (th/mutation! data)]
(t/is (nil? error)) (t/is (nil? error))
@ -579,7 +579,7 @@
(let [data {::th/type :upsert-file-object-thumbnail (let [data {::th/type :upsert-file-object-thumbnail
:profile-id (:id prof) :profile-id (:id prof)
:file-id (:id file) :file-id (:id file)
:object-id frame1-id :object-id (str page-id frame1-id)
:data "new-data"} :data "new-data"}
{:keys [error result] :as out} (th/mutation! data)] {:keys [error result] :as out} (th/mutation! data)]
(t/is (nil? error)) (t/is (nil? error))
@ -602,7 +602,7 @@
(let [data {::th/type :upsert-file-object-thumbnail (let [data {::th/type :upsert-file-object-thumbnail
:profile-id (:id prof) :profile-id (:id prof)
:file-id (:id file) :file-id (:id file)
:object-id (uuid/next) :object-id (str page-id (uuid/next))
:data "new-data-2"} :data "new-data-2"}
{:keys [error result] :as out} (th/mutation! data)] {:keys [error result] :as out} (th/mutation! data)]
(t/is (nil? error)) (t/is (nil? error))

View file

@ -5,7 +5,49 @@
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.common.attrs (ns app.common.attrs
(:refer-clojure :exclude [merge])) (:require
[app.common.geom.shapes.transforms :as gst]
[app.common.math :as mth]))
(defn- get-attr
[obj attr]
(if (= (get obj attr) :multiple)
:multiple
(cond
;; For rotated or stretched shapes, the origin point we show in the menu
;; is not the (:x :y) shape attribute, but the top left coordinate of the
;; wrapping recangle (see measures.cljs). As the :points attribute cannot
;; be merged for several objects, we calculate the origin point in two fake
;; attributes to be used in the measures menu.
(#{:ox :oy} attr)
(if-let [value (get obj attr)]
value
(if-let [points (:points obj)]
(if (not= points :multiple)
(let [rect (gst/selection-rect [obj])]
(if (= attr :ox) (:x rect) (:y rect)))
:multiple)
(get obj attr ::unset)))
;; Not all shapes have width and height (e.g. paths), so we extract
;; them from the :selrect attribute.
(#{:width :height} attr)
(if-let [value (get obj attr)]
value
(if-let [selrect (:selrect obj)]
(if (not= selrect :multiple)
(get (:selrect obj) attr)
:multiple)
(get obj attr ::unset)))
:else
(get obj attr ::unset))))
(defn- default-equal
[val1 val2]
(if (and (number? val1) (number? val2))
(mth/close? val1 val2)
(= val1 val2)))
;; Extract some attributes of a list of shapes. ;; Extract some attributes of a list of shapes.
;; For each attribute, if the value is the same in all shapes, ;; For each attribute, if the value is the same in all shapes,
@ -36,13 +78,11 @@
;; :rx nil ;; :rx nil
;; :ry nil} ;; :ry nil}
;; ;;
(defn get-attrs-multi (defn get-attrs-multi
([objs attrs] ([objs attrs]
(get-attrs-multi objs attrs = identity)) (get-attrs-multi objs attrs default-equal identity))
([objs attrs eqfn sel] ([objs attrs eqfn sel]
(loop [attr (first attrs) (loop [attr (first attrs)
attrs (rest attrs) attrs (rest attrs)
result (transient {})] result (transient {})]
@ -50,34 +90,25 @@
(let [value (let [value
(loop [curr (first objs) (loop [curr (first objs)
objs (rest objs) objs (rest objs)
value ::undefined] value ::unset]
(if (and curr (not= value :multiple)) (if (and curr (not= value :multiple))
;; (let [new-val (get-attr curr attr)
(let [new-val (get curr attr ::undefined)
value (cond value (cond
(= new-val ::undefined) value (= new-val ::unset) value
(= new-val :multiple) :multiple (= new-val :multiple) :multiple
(= value ::undefined) (sel new-val) (= value ::unset) (sel new-val)
(eqfn new-val value) value (eqfn new-val value) value
:else :multiple)] :else :multiple)]
(recur (first objs) (rest objs) value)) (recur (first objs) (rest objs) value))
;;
value))] value))]
(recur (first attrs) (recur (first attrs)
(rest attrs) (rest attrs)
(cond-> result (cond-> result
(not= value ::undefined) (not= value ::unset)
(assoc! attr value)))) (assoc! attr value))))
(persistent! result))))) (persistent! result)))))
(defn merge
"Attrs specific merge function."
[obj attrs]
(reduce-kv (fn [obj k v]
(if (nil? v)
(dissoc obj k)
(assoc obj k v)))
obj
attrs))

View file

@ -336,6 +336,16 @@
[& maps] [& maps]
(reduce conj (or (first maps) {}) (rest maps))) (reduce conj (or (first maps) {}) (rest maps)))
(defn txt-merge
"Text attrs specific merge function."
[obj attrs]
(reduce-kv (fn [obj k v]
(if (nil? v)
(dissoc obj k)
(assoc obj k v)))
obj
attrs))
(defn distinct-xf (defn distinct-xf
[f] [f]
(fn [rf] (fn [rf]
@ -574,17 +584,20 @@
(assert (string? basename)) (assert (string? basename))
(assert (set? used)) (assert (set? used))
(let [[prefix initial] (extract-numeric-suffix basename)] (if (> (count basename) 1000)
(if (and (not prefix-first?) ;; We skip generating names for long strings. If the name is too long the regex can hang
(not (contains? used basename))) basename
basename (let [[prefix initial] (extract-numeric-suffix basename)]
(loop [counter initial] (if (and (not prefix-first?)
(let [candidate (if (and (= 1 counter) prefix-first?) (not (contains? used basename)))
(str prefix) basename
(str prefix "-" counter))] (loop [counter initial]
(if (contains? used candidate) (let [candidate (if (and (= 1 counter) prefix-first?)
(recur (inc counter)) (str prefix)
candidate))))))) (str prefix "-" counter))]
(if (contains? used candidate)
(recur (inc counter))
candidate))))))))
(defn deep-mapm (defn deep-mapm
"Applies a map function to an associative map and recurses over its children "Applies a map function to an associative map and recurses over its children

View file

@ -219,3 +219,13 @@
e' (/ (- (* c f) (* d e)) det) e' (/ (- (* c f) (* d e)) det)
f' (/ (- (* b e) (* a f)) det)] f' (/ (- (* b e) (* a f)) det)]
(Matrix. a' b' c' d' e' f'))) (Matrix. a' b' c' d' e' f')))
(defn round
[mtx]
(-> mtx
(update :a mth/precision 4)
(update :b mth/precision 4)
(update :c mth/precision 4)
(update :d mth/precision 4)
(update :e mth/precision 4)
(update :f mth/precision 4)))

View file

@ -100,7 +100,6 @@
(assert (point? other)) (assert (point? other))
(Point. (/ x ox) (/ y oy))) (Point. (/ x ox) (/ y oy)))
(defn min (defn min
([] (min nil nil)) ([] (min nil nil))
([p1] (min p1 nil)) ([p1] (min p1 nil))
@ -139,6 +138,15 @@
(mth/sqrt (+ (mth/pow dx 2) (mth/sqrt (+ (mth/pow dx 2)
(mth/pow dy 2))))) (mth/pow dy 2)))))
(defn distance-vector
"Calculate the distance, separated x and y."
[{x :x y :y :as p} {ox :x oy :y :as other}]
(assert (point? p))
(assert (point? other))
(let [dx (mth/abs (- x ox))
dy (mth/abs (- y oy))]
(Point. dx dy)))
(defn length (defn length
[{x :x y :y :as p}] [{x :x y :y :as p}]
(assert (point? p)) (assert (point? p))

View file

@ -6,6 +6,7 @@
(ns app.common.geom.shapes (ns app.common.geom.shapes
(:require (:require
[app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes.bool :as gsb] [app.common.geom.shapes.bool :as gsb]
@ -38,6 +39,14 @@
;; --- Helpers ;; --- Helpers
(defn left-bound
[shape]
(get shape :x (:x (:selrect shape)))) ; Paths don't have :x attribute
(defn top-bound
[shape]
(get shape :y (:y (:selrect shape)))) ; Paths don't have :y attribute
(defn fully-contained? (defn fully-contained?
"Checks if one rect is fully inside the other" "Checks if one rect is fully inside the other"
[rect other] [rect other]
@ -96,6 +105,37 @@
(mth/sqrt (* 2 stroke-width stroke-width)) (mth/sqrt (* 2 stroke-width stroke-width))
(- (mth/sqrt (* 2 stroke-width stroke-width)) stroke-width))) (- (mth/sqrt (* 2 stroke-width stroke-width)) stroke-width)))
(defn close-attrs?
"Compares two shapes attributes to see if they are equal or almost
equal (in case of numeric). Takes into account attributes that are
data structures with numbers inside."
([attr val1 val2]
(close-attrs? attr val1 val2 mth/float-equal-precision))
([attr val1 val2 precision]
(let [close-val? (fn [num1 num2]
(when (and (number? num1) (number? num2))
(< (mth/abs (- num1 num2)) precision)))]
(cond
(and (number? val1) (number? val2))
(close-val? val1 val2)
(= attr :selrect)
(every? #(close-val? (get val1 %) (get val2 %))
[:x :y :x1 :y1 :x2 :y2 :width :height])
(= attr :points)
(every? #(and (close-val? (:x (first %)) (:x (second %)))
(close-val? (:y (first %)) (:y (second %))))
(d/zip val1 val2))
(= attr :position-data)
(every? #(and (close-val? (:x (first %)) (:x (second %)))
(close-val? (:y (first %)) (:y (second %))))
(d/zip val1 val2))
:else
(= val1 val2)))))
;; EXPORTS ;; EXPORTS
(dm/export gco/center-shape) (dm/export gco/center-shape)

View file

@ -152,45 +152,74 @@
:top :top
:scale))) :scale)))
(defn clean-modifiers
"Remove redundant modifiers"
[{:keys [displacement resize-vector resize-vector-2] :as modifiers}]
(cond-> modifiers
;; Displacement with value 0. We don't move in any direction
(and (some? displacement)
(mth/almost-zero? (:e displacement))
(mth/almost-zero? (:f displacement)))
(dissoc :displacement)
;; Resize with value very close to 1 means no resize
(and (some? resize-vector)
(mth/almost-zero? (- 1.0 (:x resize-vector)))
(mth/almost-zero? (- 1.0 (:y resize-vector))))
(dissoc :resize-origin :resize-vector)
(and (some? resize-vector)
(mth/almost-zero? (- 1.0 (:x resize-vector-2)))
(mth/almost-zero? (- 1.0 (:y resize-vector-2))))
(dissoc :resize-origin-2 :resize-vector-2)))
(defn calc-child-modifiers (defn calc-child-modifiers
[parent child modifiers ignore-constraints transformed-parent-rect] [parent child modifiers ignore-constraints transformed-parent-rect]
(let [constraints-h
(if-not ignore-constraints
(:constraints-h child (default-constraints-h child))
:scale)
constraints-v (if (and (nil? (:resize-vector modifiers))
(if-not ignore-constraints (nil? (:resize-vector-2 modifiers)))
(:constraints-v child (default-constraints-v child)) ;; If we don't have a resize modifier we return the same modifiers
:scale) modifiers
(let [constraints-h
(if-not ignore-constraints
(:constraints-h child (default-constraints-h child))
:scale)
modifiers-h (constraint-modifier (constraints-h const->type+axis) :x parent child modifiers transformed-parent-rect) constraints-v
modifiers-v (constraint-modifier (constraints-v const->type+axis) :y parent child modifiers transformed-parent-rect)] (if-not ignore-constraints
(:constraints-v child (default-constraints-v child))
:scale)
;; Build final child modifiers. Apply transform again to the result, to get the modifiers-h (constraint-modifier (constraints-h const->type+axis) :x parent child modifiers transformed-parent-rect)
;; real modifiers that need to be applied to the child, including rotation as needed. modifiers-v (constraint-modifier (constraints-v const->type+axis) :y parent child modifiers transformed-parent-rect)]
(cond-> {}
(or (contains? modifiers-h :displacement)
(contains? modifiers-v :displacement))
(assoc :displacement (cond-> (gpt/point (get-in modifiers-h [:displacement :x] 0)
(get-in modifiers-v [:displacement :y] 0))
(some? (:resize-transform modifiers))
(gpt/transform (:resize-transform modifiers))
:always ;; Build final child modifiers. Apply transform again to the result, to get the
(gmt/translate-matrix))) ;; real modifiers that need to be applied to the child, including rotation as needed.
(cond-> {}
(or (contains? modifiers-h :displacement)
(contains? modifiers-v :displacement))
(assoc :displacement (cond-> (gpt/point (get-in modifiers-h [:displacement :x] 0)
(get-in modifiers-v [:displacement :y] 0))
(some? (:resize-transform modifiers))
(gpt/transform (:resize-transform modifiers))
(:resize-vector modifiers-h) :always
(assoc :resize-origin (:resize-origin modifiers-h) (gmt/translate-matrix)))
:resize-vector (gpt/point (get-in modifiers-h [:resize-vector :x] 1)
(get-in modifiers-h [:resize-vector :y] 1)))
(:resize-vector modifiers-v) (:resize-vector modifiers-h)
(assoc :resize-origin-2 (:resize-origin modifiers-v) (assoc :resize-origin (:resize-origin modifiers-h)
:resize-vector-2 (gpt/point (get-in modifiers-v [:resize-vector :x] 1) :resize-vector (gpt/point (get-in modifiers-h [:resize-vector :x] 1)
(get-in modifiers-v [:resize-vector :y] 1))) (get-in modifiers-h [:resize-vector :y] 1)))
(:resize-transform modifiers) (:resize-vector modifiers-v)
(assoc :resize-transform (:resize-transform modifiers) (assoc :resize-origin-2 (:resize-origin modifiers-v)
:resize-transform-inverse (:resize-transform-inverse modifiers))))) :resize-vector-2 (gpt/point (get-in modifiers-v [:resize-vector :x] 1)
(get-in modifiers-v [:resize-vector :y] 1)))
(:resize-transform modifiers)
(assoc :resize-transform (:resize-transform modifiers)
:resize-transform-inverse (:resize-transform-inverse modifiers))
:always
(clean-modifiers)))))

View file

@ -28,4 +28,3 @@
[shape] [shape]
(gpr/points->selrect (position-data-points shape))) (gpr/points->selrect (position-data-points shape)))

View file

@ -6,7 +6,6 @@
(ns app.common.geom.shapes.transforms (ns app.common.geom.shapes.transforms
(:require (:require
[app.common.attrs :as attrs]
[app.common.data :as d] [app.common.data :as d]
[app.common.geom.matrix :as gmt] [app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
@ -533,7 +532,7 @@
(* (get-in modifiers [:resize-vector :x] 1)) (* (get-in modifiers [:resize-vector :x] 1))
(* (get-in modifiers [:resize-vector-2 :x] 1)) (* (get-in modifiers [:resize-vector-2 :x] 1))
(str))] (str))]
(attrs/merge attrs {:font-size font-size})))] (d/txt-merge attrs {:font-size font-size})))]
(update shape :content #(txt/transform-nodes (update shape :content #(txt/transform-nodes
txt/is-text-node? txt/is-text-node?
merge-attrs merge-attrs

View file

@ -11,6 +11,7 @@
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.geom.shapes.bool :as gshb] [app.common.geom.shapes.bool :as gshb]
[app.common.math :as mth]
[app.common.pages.common :refer [component-sync-attrs]] [app.common.pages.common :refer [component-sync-attrs]]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.common.pages.init :as init] [app.common.pages.init :as init]
@ -433,25 +434,35 @@
(defmethod process-operation :set (defmethod process-operation :set
[shape op] [shape op]
(let [attr (:attr op) (let [attr (:attr op)
val (:val op) group (get component-sync-attrs attr)
ignore (:ignore-touched op) val (:val op)
shape-val (get shape attr)
ignore (:ignore-touched op)
ignore-geometry (:ignore-geometry op) ignore-geometry (:ignore-geometry op)
shape-ref (:shape-ref shape) is-geometry? (and (or (= group :geometry-group)
group (get component-sync-attrs attr) (and (= group :content-group) (= (:type shape) :path)))
root-name? (and (= group :name-group) (not (#{:width :height} attr))) ;; :content in paths are also considered geometric
(:component-root? shape))] shape-ref (:shape-ref shape)
root-name? (and (= group :name-group)
(:component-root? shape))
;; For geometric attributes, there are cases in that the value changes
;; slightly (e.g. when rounding to pixel, or when recalculating text
;; positions in different zoom levels). To take this into account, we
;; ignore geometric changes smaller than 1 pixel.
equal? (if is-geometry?
(gsh/close-attrs? attr val shape-val 1)
(gsh/close-attrs? attr val shape-val))]
(cond-> shape (cond-> shape
;; Depending on the origin of the attribute change, we need or not to ;; Depending on the origin of the attribute change, we need or not to
;; set the "touched" flag for the group the attribute belongs to. ;; set the "touched" flag for the group the attribute belongs to.
;; In some cases we need to ignore touched only if the attribute is ;; In some cases we need to ignore touched only if the attribute is
;; geometric (position, width or transformation). ;; geometric (position, width or transformation).
(and shape-ref group (not ignore) (not= val (get shape attr)) (and shape-ref group (not ignore) (not equal?)
(not root-name?) (not root-name?)
(not (and ignore-geometry (not (and ignore-geometry is-geometry?)))
(and (= group :geometry-group)
(not (#{:width :height} attr))))))
(-> (->
(update :touched cph/set-touched-group group) (update :touched cph/set-touched-group group)
(dissoc :remote-synced?)) (dissoc :remote-synced?))

View file

@ -25,6 +25,7 @@
:content :content-group :content :content-group
:hidden :visibility-group :hidden :visibility-group
:blocked :modifiable-group :blocked :modifiable-group
:grow-type :text-font-group
:font-family :text-font-group :font-family :text-font-group
:font-size :text-font-group :font-size :text-font-group
:font-style :text-font-group :font-style :text-font-group
@ -58,8 +59,10 @@
:y :geometry-group :y :geometry-group
:width :geometry-group :width :geometry-group
:height :geometry-group :height :geometry-group
:rotation :geometry-group
:transform :geometry-group :transform :geometry-group
:transform-inverse :geometry-group :transform-inverse :geometry-group
:position-data :geometry-group
:opacity :layer-effects-group :opacity :layer-effects-group
:blend-mode :layer-effects-group :blend-mode :layer-effects-group
:shadow :shadow-group :shadow :shadow-group
@ -78,6 +81,7 @@
:rx :ry :rx :ry
:r1 :r2 :r3 :r4 :r1 :r2 :r3 :r4
:selrect :selrect
:points
:opacity :opacity
:blend-mode :blend-mode
@ -111,6 +115,7 @@
:x :y :x :y
:rotation :rotation
:selrect :selrect
:points
:constraints-h :constraints-h
:constraints-v :constraints-v
@ -136,6 +141,7 @@
:rx :ry :rx :ry
:r1 :r2 :r3 :r4 :r1 :r2 :r3 :r4
:selrect :selrect
:points
:constraints-h :constraints-h
:constraints-v :constraints-v
@ -178,6 +184,7 @@
:x :y :x :y
:rotation :rotation
:selrect :selrect
:points
:constraints-h :constraints-h
:constraints-v :constraints-v
@ -220,6 +227,7 @@
:x :y :x :y
:rotation :rotation
:selrect :selrect
:points
:constraints-h :constraints-h
:constraints-v :constraints-v
@ -262,6 +270,7 @@
:x :y :x :y
:rotation :rotation
:selrect :selrect
:points
:constraints-h :constraints-h
:constraints-v :constraints-v
@ -329,6 +338,7 @@
:rx :ry :rx :ry
:r1 :r2 :r3 :r4 :r1 :r2 :r3 :r4
:selrect :selrect
:points
:constraints-h :constraints-h
:constraints-v :constraints-v
@ -354,6 +364,7 @@
:rx :ry :rx :ry
:r1 :r2 :r3 :r4 :r1 :r2 :r3 :r4
:selrect :selrect
:points
:constraints-h :constraints-h
:constraints-v :constraints-v
@ -398,6 +409,7 @@
:rx :ry :rx :ry
:r1 :r2 :r3 :r4 :r1 :r2 :r3 :r4
:selrect :selrect
:points
:constraints-h :constraints-h
:constraints-v :constraints-v

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<path d="M0 0h500v500H0Zm50 50v400h400V50Zm241.416 73.877c-5.652 0-10.234 4.582-10.234 10.234v71.637c0 5.652 4.582 10.234 10.234 10.234h71.638c5.652 0 10.234-4.582 10.234-10.234v-71.637c0-5.652-4.582-10.234-10.234-10.234zm-157.894 0c-5.652 0-10.234 4.582-10.234 10.234v71.637c0 5.652 4.582 10.234 10.234 10.234h71.637c5.652 0 10.234-4.582 10.234-10.234v-71.637c0-5.652-4.582-10.234-10.234-10.234zM291.416 281.77c-5.652 0-10.234 4.582-10.234 10.234v71.638c0 5.652 4.582 10.234 10.234 10.234h71.638c5.652 0 10.234-4.582 10.234-10.234v-71.638c0-5.652-4.582-10.234-10.234-10.234zm-157.894 0c-5.652 0-10.234 4.582-10.234 10.234v71.638c0 5.652 4.582 10.234 10.234 10.234h71.637c5.652 0 10.234-4.582 10.234-10.234v-71.638c0-5.652-4.582-10.234-10.234-10.234z"/>
</svg>

After

Width:  |  Height:  |  Size: 826 B

View file

@ -195,7 +195,7 @@
border-radius: 4px; border-radius: 4px;
box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25); box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25);
z-index: 12; z-index: 12;
top: 40px; top: 30px;
left: 6px; left: 6px;
width: 155px; width: 155px;

View file

@ -144,7 +144,7 @@
.color-bullet { .color-bullet {
width: 24px; width: 24px;
height: 24px; height: 24px;
border-radius: $br-small; border-radius: 50%;
border: 1px solid $color-gray-60; border: 1px solid $color-gray-60;
} }

View file

@ -316,6 +316,10 @@ $height-palette-max: 80px;
} }
} }
.workspace-frame-icon {
fill: $color-gray-40;
}
.workspace-frame-label { .workspace-frame-label {
fill: $color-gray-40; fill: $color-gray-40;
font-size: $fs12; font-size: $fs12;

View file

@ -277,7 +277,10 @@
page (get-in state [:workspace-data :pages-index page-id]) page (get-in state [:workspace-data :pages-index page-id])
name (dwc/generate-unique-name unames (:name page)) name (dwc/generate-unique-name unames (:name page))
page (-> page (assoc :name name :id id)) no_thumbnails_objects (->> (:objects page)
(d/mapm (fn [_ val] (dissoc val :use-for-thumbnail?))))
page (-> page (assoc :name name :id id :objects no_thumbnails_objects))
changes (-> (pcb/empty-changes it) changes (-> (pcb/empty-changes it)
(pcb/add-page id page))] (pcb/add-page id page))]
@ -1428,7 +1431,12 @@
wrapper (gsh/selection-rect selected-objs) wrapper (gsh/selection-rect selected-objs)
orig-pos (gpt/point (:x1 wrapper) (:y1 wrapper))] orig-pos (gpt/point (:x1 wrapper) (:y1 wrapper))]
(cond (cond
(and (selected-frame? state) (not has-frame?)) has-frame?
(let [index (cph/get-position-on-parent page-objects uuid/zero)
delta (gpt/subtract mouse-pos orig-pos)]
[uuid/zero uuid/zero delta index])
(selected-frame? state)
(let [frame-id (first page-selected) (let [frame-id (first page-selected)
frame-object (get page-objects frame-id) frame-object (get page-objects frame-id)

View file

@ -45,7 +45,7 @@
ptk/WatchEvent ptk/WatchEvent
(watch [it state _] (watch [it state _]
(let [page-id (or page-id (:current-page-id state)) (let [page-id (or page-id (:current-page-id state))
objects (wsh/lookup-page-objects state) objects (wsh/lookup-page-objects state page-id)
ids (into [] (filter some?) ids) ids (into [] (filter some?) ids)
changes (reduce changes (reduce

View file

@ -124,7 +124,7 @@
(let [edition (get-in state [:workspace-local :edition]) (let [edition (get-in state [:workspace-local :edition])
drawing (get state :workspace-drawing)] drawing (get state :workspace-drawing)]
;; Editors handle their own undo's ;; Editors handle their own undo's
(when-not (or (some? edition) (not-empty drawing)) (when-not (or (some? edition) (and (not-empty drawing) (nil? (:object drawing))))
(let [undo (:workspace-undo state) (let [undo (:workspace-undo state)
items (:items undo) items (:items undo)
index (or (:index undo) (dec (count items)))] index (or (:index undo) (dec (count items)))]

View file

@ -88,7 +88,7 @@
changes (-> (pcb/empty-changes it page-id) changes (-> (pcb/empty-changes it page-id)
(pcb/with-objects objects) (pcb/with-objects objects)
(pcb/add-object group) (pcb/add-object group {:index (::index (first shapes))})
(pcb/change-parent (:id group) shapes) (pcb/change-parent (:id group) shapes)
(pcb/remove-objects ids-to-delete))] (pcb/remove-objects ids-to-delete))]

View file

@ -260,12 +260,13 @@
ptk/WatchEvent ptk/WatchEvent
(watch [it state _] (watch [it state _]
(let [data (get state :workspace-data) (let [data (get state :workspace-data)
[path name] (cph/parse-path-name new-name) [path name] (cph/parse-path-name (:name typography))
object (get-in data [:typographies id]) path (if (and (:path typography) (= "" path))
new-object (assoc object :path path :name name)] (:path typography)
path)
typography (assoc typography :path path :name name)]
(do-update-tipography it state new-object file-id))))) (do-update-tipography it state new-object file-id)))))
(defn delete-typography (defn delete-typography
[id] [id]
(us/assert ::us/uuid id) (us/assert ::us/uuid id)
@ -606,47 +607,48 @@
ptk/WatchEvent ptk/WatchEvent
(watch [it state _] (watch [it state _]
(log/info :msg "SYNC-FILE" (when (and (some? file-id) (some? library-id)) ; Prevent race conditions while navigating out of the file
:file (dwlh/pretty-file file-id state) (log/info :msg "SYNC-FILE"
:library (dwlh/pretty-file library-id state)) :file (dwlh/pretty-file file-id state)
(let [file (wsh/get-file state file-id) :library (dwlh/pretty-file library-id state))
(let [file (wsh/get-file state file-id)
library-changes (reduce library-changes (reduce
pcb/concat-changes pcb/concat-changes
(pcb/empty-changes it) (pcb/empty-changes it)
[(dwlh/generate-sync-library it file-id :components library-id state) [(dwlh/generate-sync-library it file-id :components library-id state)
(dwlh/generate-sync-library it file-id :colors library-id state) (dwlh/generate-sync-library it file-id :colors library-id state)
(dwlh/generate-sync-library it file-id :typographies library-id state)]) (dwlh/generate-sync-library it file-id :typographies library-id state)])
file-changes (reduce file-changes (reduce
pcb/concat-changes pcb/concat-changes
(pcb/empty-changes it) (pcb/empty-changes it)
[(dwlh/generate-sync-file it file-id :components library-id state) [(dwlh/generate-sync-file it file-id :components library-id state)
(dwlh/generate-sync-file it file-id :colors library-id state) (dwlh/generate-sync-file it file-id :colors library-id state)
(dwlh/generate-sync-file it file-id :typographies library-id state)]) (dwlh/generate-sync-file it file-id :typographies library-id state)])
changes (pcb/concat-changes library-changes file-changes)] changes (pcb/concat-changes library-changes file-changes)]
(log/debug :msg "SYNC-FILE finished" :js/rchanges (log-changes (log/debug :msg "SYNC-FILE finished" :js/rchanges (log-changes
(:redo-changes changes) (:redo-changes changes)
file)) file))
(rx/concat (rx/concat
(rx/of (dm/hide-tag :sync-dialog)) (rx/of (dm/hide-tag :sync-dialog))
(when (seq (:redo-changes changes)) (when (seq (:redo-changes changes))
(rx/of (dch/commit-changes (assoc changes ;; TODO a ver qué pasa con esto (rx/of (dch/commit-changes (assoc changes ;; TODO a ver qué pasa con esto
:file-id file-id)))) :file-id file-id))))
(when (not= file-id library-id) (when (not= file-id library-id)
;; When we have just updated the library file, give some time for the ;; When we have just updated the library file, give some time for the
;; update to finish, before marking this file as synced. ;; update to finish, before marking this file as synced.
;; TODO: look for a more precise way of syncing this. ;; TODO: look for a more precise way of syncing this.
;; Maybe by using the stream (second argument passed to watch) ;; Maybe by using the stream (second argument passed to watch)
;; to wait for the corresponding changes-committed and then proceed ;; to wait for the corresponding changes-committed and then proceed
;; with the :update-sync mutation. ;; with the :update-sync mutation.
(rx/concat (rx/timer 3000) (rx/concat (rx/timer 3000)
(rp/mutation :update-sync (rp/mutation :update-sync
{:file-id file-id {:file-id file-id
:library-id library-id}))) :library-id library-id})))
(when (seq (:redo-changes library-changes)) (when (seq (:redo-changes library-changes))
(rx/of (sync-file-2nd-stage file-id library-id)))))))) (rx/of (sync-file-2nd-stage file-id library-id)))))))))
(defn sync-file-2nd-stage (defn sync-file-2nd-stage
"If some components have been modified, we need to launch another synchronization "If some components have been modified, we need to launch another synchronization

View file

@ -549,18 +549,19 @@
(:shapes shape-main)) (:shapes shape-main))
only-inst (fn [changes child-inst] only-inst (fn [changes child-inst]
(when-not (and omit-touched? (if-not (and omit-touched?
(contains? (:touched shape-inst) (contains? (:touched shape-inst)
:shapes-group)) :shapes-group))
(remove-shape changes (remove-shape changes
child-inst child-inst
container container
omit-touched?))) omit-touched?)
changes))
only-main (fn [changes child-main] only-main (fn [changes child-main]
(when-not (and omit-touched? (if-not (and omit-touched?
(contains? (:touched shape-inst) (contains? (:touched shape-inst)
:shapes-group)) :shapes-group))
(add-shape-to-instance changes (add-shape-to-instance changes
child-main child-main
(d/index-of children-main (d/index-of children-main
@ -570,7 +571,8 @@
root-inst root-inst
root-main root-main
omit-touched? omit-touched?
set-remote-synced?))) set-remote-synced?)
changes))
both (fn [changes child-inst child-main] both (fn [changes child-inst child-main]
(generate-sync-shape-direct-recursive changes (generate-sync-shape-direct-recursive changes

View file

@ -54,8 +54,8 @@
;; Subscribe to notifications of the subscription ;; Subscribe to notifications of the subscription
(->> stream (->> stream
(rx/filter (ptk/type? ::dws/message)) (rx/filter (ptk/type? ::dws/message))
(rx/map deref) (rx/map deref) ;; :library-change events occur in a different file, but need to be processed anyway
(rx/filter #(= subs-id (:subs-id %))) (rx/filter #(or (= subs-id (:subs-id %)) (= (:type %) :library-change)))
(rx/map process-message)) (rx/map process-message))
;; On reconnect, send again the subscription messages ;; On reconnect, send again the subscription messages

View file

@ -23,9 +23,22 @@
(us/verify ::spec/content new-content) (us/verify ::spec/content new-content)
(let [shape-id (:id shape) (let [shape-id (:id shape)
[old-points old-selrect]
(helpers/content->points+selrect shape old-content)
[new-points new-selrect] [new-points new-selrect]
(helpers/content->points+selrect shape new-content) (helpers/content->points+selrect shape new-content)
;; We set the old values so the update-shapes works
objects
(-> objects
(update
shape-id
assoc
:content old-content
:selrect old-selrect
:points old-points))
changes (-> (pcb/empty-changes it page-id) changes (-> (pcb/empty-changes it page-id)
(pcb/with-objects objects))] (pcb/with-objects objects))]

View file

@ -324,6 +324,7 @@
:name frame-name :name frame-name
:frame-id uuid/zero :frame-id uuid/zero
:shapes []) :shapes [])
(dissoc :use-for-thumbnail?)
(geom/move delta) (geom/move delta)
(d/update-when :interactions #(cti/remap-interactions % ids-map objects))) (d/update-when :interactions #(cti/remap-interactions % ids-map objects)))

View file

@ -8,7 +8,9 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.pages.helpers :as cph])) [app.common.geom.shapes :as gsh]
[app.common.pages.helpers :as cph]
[app.common.path.commands :as upc]))
(defn lookup-page (defn lookup-page
([state] ([state]
@ -50,6 +52,10 @@
(filter selectable?) (filter selectable?)
selected))))) selected)))))
(defn lookup-selected-raw
[state]
(dm/get-in state [:workspace-local :selected]))
(defn lookup-selected (defn lookup-selected
([state] ([state]
(lookup-selected state nil)) (lookup-selected state nil))
@ -94,3 +100,26 @@
(-> (:workspace-libraries state) (-> (:workspace-libraries state)
(assoc id {:id id (assoc id {:id id
:data local})))) :data local}))))
(defn- set-content-modifiers [state]
(fn [id shape]
(let [content-modifiers (dm/get-in state [:workspace-local :edit-path id :content-modifiers])]
(if (some? content-modifiers)
(update shape :content upc/apply-content-modifiers content-modifiers)
shape))))
(defn select-bool-children
[parent-id state]
(let [objects (lookup-page-objects state)
selected (lookup-selected-raw state)
modifiers (:workspace-modifiers state)
children-ids (cph/get-children-ids objects parent-id)
selected-children (into [] (filter selected) children-ids)
modifiers (select-keys modifiers selected-children)
children (select-keys objects children-ids)]
(as-> children $
(gsh/merge-modifiers $ modifiers)
(d/mapm (set-content-modifiers state) $))))

View file

@ -326,7 +326,7 @@
transform (->> svg-transform transform (->> svg-transform
(gmt/transform-in (gpt/point svg-data))) (gmt/transform-in (gpt/point svg-data)))
image-url (:xlink:href attrs) image-url (or (:href attrs) (:xlink:href attrs))
image-data (get-in svg-data [:image-data image-url]) image-data (get-in svg-data [:image-data image-url])
rect (->> (select-keys attrs [:x :y :width :height]) rect (->> (select-keys attrs [:x :y :width :height])
@ -352,7 +352,7 @@
(merge rect-metadata) (merge rect-metadata)
(assoc :svg-viewbox (select-keys rect [:x :y :width :height])) (assoc :svg-viewbox (select-keys rect [:x :y :width :height]))
(assoc :svg-attrs (dissoc attrs :x :y :width :height :xlink:href)))))) (assoc :svg-attrs (dissoc attrs :x :y :width :height :href :xlink:href))))))
(defn parse-svg-element [frame-id svg-data element-data unames] (defn parse-svg-element [frame-id svg-data element-data unames]
(let [{:keys [tag attrs]} element-data (let [{:keys [tag attrs]} element-data
@ -414,7 +414,6 @@
new-shape (dwc/make-new-shape shape objects selected) new-shape (dwc/make-new-shape shape objects selected)
changes (-> changes changes (-> changes
(pcb/with-objects objects)
(pcb/add-object new-shape) (pcb/add-object new-shape)
(pcb/change-parent parent-id [new-shape] index)) (pcb/change-parent parent-id [new-shape] index))
@ -464,9 +463,25 @@
root-shape (create-svg-root frame-id svg-data) root-shape (create-svg-root frame-id svg-data)
root-id (:id root-shape) root-id (:id root-shape)
;; In penpot groups have the size of their children. To respect the imported svg size and empty space let's create a transparent shape as background to respect the imported size
base-background-shape {:tag :rect
:attrs {:x "0"
:y "0"
:width (str (:width root-shape))
:height (str (:height root-shape))
:fill "none"
:id "base-background"}
:content []}
svg-data (-> svg-data
(assoc :defs def-nodes)
(assoc :content (into [base-background-shape] (:content svg-data))))
;; Creates the root shape ;; Creates the root shape
new-shape (dwc/make-new-shape root-shape objects selected) new-shape (dwc/make-new-shape root-shape objects selected)
changes (-> (pcb/empty-changes it page-id) changes (-> (pcb/empty-changes it page-id)
(pcb/with-objects objects)
(pcb/add-object new-shape)) (pcb/add-object new-shape))
root-attrs (-> (:attrs svg-data) root-attrs (-> (:attrs svg-data)
@ -478,7 +493,6 @@
[unames changes] [unames changes]
(d/enumerate (->> (:content svg-data) (d/enumerate (->> (:content svg-data)
(mapv #(usvg/inherit-attributes root-attrs %))))) (mapv #(usvg/inherit-attributes root-attrs %)))))
changes (pcb/resize-parents changes changes (pcb/resize-parents changes
(->> changes (->> changes
:redo-changes :redo-changes

View file

@ -187,8 +187,8 @@
update-fn update-fn
(fn [shape] (fn [shape]
(if (some? (:content shape)) (if (some? (:content shape))
(update-text-content shape txt/is-root-node? attrs/merge attrs) (update-text-content shape txt/is-root-node? d/txt-merge attrs)
(assoc shape :content (attrs/merge {:type "root"} attrs)))) (assoc shape :content (d/txt-merge {:type "root"} attrs))))
shape-ids (cond (cph/text-shape? shape) [id] shape-ids (cond (cph/text-shape? shape) [id]
(cph/group-shape? shape) (cph/get-children-ids objects id))] (cph/group-shape? shape) (cph/get-children-ids objects id))]
@ -240,18 +240,19 @@
shape-ids (cond shape-ids (cond
(cph/text-shape? shape) [id] (cph/text-shape? shape) [id]
(cph/group-shape? shape) (cph/get-children-ids objects id))] (cph/group-shape? shape) (cph/get-children-ids objects id))]
(rx/of (dch/update-shapes shape-ids #(update-text-content % update-node? attrs/merge attrs)))))))) (rx/of (dch/update-shapes shape-ids #(update-text-content % update-node? d/txt-merge attrs))))))))
(defn migrate-node (defn migrate-node
[node] [node]
(let [color-attrs (select-keys node [:fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient])] (let [color-attrs (select-keys node [:fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient])]
(cond-> node (cond-> node
(d/not-empty? color-attrs)
(-> (dissoc :fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient)
(assoc :fills [color-attrs]))
(nil? (:fills node)) (nil? (:fills node))
(assoc :fills (:fills txt/default-text-attrs))))) (assoc :fills (:fills txt/default-text-attrs))
(and (d/not-empty? color-attrs) (nil? (:fills node)))
(-> (dissoc :fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient)
(assoc :fills [color-attrs])))
))
(defn migrate-content (defn migrate-content
[content] [content]
@ -374,6 +375,15 @@
(update [_ state] (update [_ state]
(update-in state [:workspace-text-modifier id] (fnil merge {}) props)))) (update-in state [:workspace-text-modifier id] (fnil merge {}) props))))
(defn clean-text-modifier
[id]
(ptk/reify ::clean-text-modifier
ptk/WatchEvent
(watch [_ _ _]
(->> (rx/of #(update % :workspace-text-modifier dissoc id))
;; We delay a bit the change so there is no weird transition to the user
(rx/delay 50)))))
(defn remove-text-modifier (defn remove-text-modifier
[id] [id]
(ptk/reify ::remove-text-modifier (ptk/reify ::remove-text-modifier

View file

@ -7,6 +7,7 @@
(ns app.main.data.workspace.thumbnails (ns app.main.data.workspace.thumbnails
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.data.workspace.changes :as dch] [app.main.data.workspace.changes :as dch]
@ -27,57 +28,44 @@
(defn update-thumbnail (defn update-thumbnail
"Updates the thumbnail information for the given frame `id`" "Updates the thumbnail information for the given frame `id`"
[id data] [page-id frame-id data]
(let [lock (uuid/next)] (let [lock (uuid/next)
object-id (dm/str page-id frame-id)]
(ptk/reify ::update-thumbnail (ptk/reify ::update-thumbnail
IDeref IDeref
(-deref [_] {:id id :data data}) (-deref [_] {:object-id object-id :data data})
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (-> state
(assoc-in [:workspace-file :thumbnails id] data) (assoc-in [:workspace-file :thumbnails object-id] data)
(cond-> (nil? (get-in state [::update-thumbnail-lock id])) (cond-> (nil? (get-in state [::update-thumbnail-lock object-id]))
(assoc-in [::update-thumbnail-lock id] lock)))) (assoc-in [::update-thumbnail-lock object-id] lock))))
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(when (= lock (get-in state [::update-thumbnail-lock id])) (when (= lock (get-in state [::update-thumbnail-lock object-id]))
(let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize))) (let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize)))
params {:file-id (:current-file-id state) params {:file-id (:current-file-id state)
:object-id id}] :object-id object-id}]
;; Sends the first event and debounce the rest. Will only make one update once ;; Sends the first event and debounce the rest. Will only make one update once
;; the 2 second debounce is finished ;; the 2 second debounce is finished
(rx/merge (rx/merge
(->> stream (->> stream
(rx/filter (ptk/type? ::update-thumbnail)) (rx/filter (ptk/type? ::update-thumbnail))
(rx/map deref) (rx/map deref)
(rx/filter #(= id (:id %))) (rx/filter #(= object-id (:object-id %)))
(rx/debounce 2000) (rx/debounce 2000)
(rx/take 1) (rx/take 1)
(rx/map :data) (rx/map :data)
(rx/flat-map #(rp/mutation! :upsert-file-object-thumbnail (assoc params :data %))) (rx/flat-map #(rp/mutation! :upsert-file-object-thumbnail (assoc params :data %)))
(rx/map #(fn [state] (d/dissoc-in state [::update-thumbnail-lock id]))) (rx/map #(fn [state] (d/dissoc-in state [::update-thumbnail-lock object-id])))
(rx/take-until stopper)) (rx/take-until stopper))
(->> (rx/of (update-thumbnail id data)) (->> (rx/of (update-thumbnail page-id frame-id data))
(rx/observe-on :async))))))))) (rx/observe-on :async)))))))))
(defn remove-thumbnail
[id]
(ptk/reify ::remove-thumbnail
ptk/UpdateEvent
(update [_ state]
(-> state (d/dissoc-in [:workspace-file :thumbnails id])))
ptk/WatchEvent
(watch [_ state _]
(let [params {:file-id (:current-file-id state)
:object-id id
:data nil}]
(->> (rp/mutation! :upsert-file-object-thumbnail params)
(rx/ignore))))))
(defn- extract-frame-changes (defn- extract-frame-changes
"Process a changes set in a commit to extract the frames that are changing" "Process a changes set in a commit to extract the frames that are changing"
[[event [old-objects new-objects]]] [[event [old-objects new-objects]]]
@ -165,7 +153,8 @@
(ptk/reify ::duplicate-thumbnail (ptk/reify ::duplicate-thumbnail
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [old-shape-thumbnail (get-in state [:workspace-file :thumbnails old-id])] (let [page-id (get state :current-page-id)
(-> state (assoc-in [:workspace-file :thumbnails new-id] old-shape-thumbnail)))))) 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

@ -252,15 +252,23 @@
shape-delta shape-delta
(when root (when root
(gpt/point (- (:x shape) (:x root)) (gpt/point (- (gsh/left-bound shape) (gsh/left-bound root))
(- (:y shape) (:y root)))) (- (gsh/top-bound shape) (gsh/top-bound root))))
transformed-shape-delta transformed-shape-delta
(when transformed-root (when transformed-root
(gpt/point (- (:x transformed-shape) (:x transformed-root)) (gpt/point (- (gsh/left-bound transformed-shape) (gsh/left-bound transformed-root))
(- (:y transformed-shape) (:y transformed-root)))) (- (gsh/top-bound transformed-shape) (gsh/top-bound transformed-root))))
ignore-geometry? (= shape-delta transformed-shape-delta)] ;; There are cases in that the coordinates change slightly (e.g. when
;; rounding to pixel, or when recalculating text positions in different
;; zoom levels). To take this into account, we ignore movements smaller
;; than 1 pixel.
distance (if (and shape-delta transformed-shape-delta)
(gpt/distance-vector shape-delta transformed-shape-delta)
(gpt/point 0 0))
ignore-geometry? (and (< (:x distance) 1) (< (:y distance) 1))]
[root transformed-root ignore-geometry?])) [root transformed-root ignore-geometry?]))
@ -356,22 +364,27 @@
[modif-tree shape modifiers] [modif-tree shape modifiers]
(let [children (map (d/getf objects) (:shapes shape)) (let [children (map (d/getf objects) (:shapes shape))
modifiers (cond-> modifiers snap-pixel? (set-pixel-precision shape))
transformed-rect (gsh/transform-selrect (:selrect shape) modifiers) transformed-rect (gsh/transform-selrect (:selrect shape) modifiers)
set-child set-child
(fn [modif-tree child] (fn [snap-pixel? modif-tree child]
(let [child-modifiers (gsh/calc-child-modifiers shape child modifiers ignore-constraints transformed-rect)] (let [child-modifiers (gsh/calc-child-modifiers shape child modifiers ignore-constraints transformed-rect)
child-modifiers (cond-> child-modifiers snap-pixel? (set-pixel-precision child))]
(cond-> modif-tree (cond-> modif-tree
(not (gsh/empty-modifiers? child-modifiers)) (not (gsh/empty-modifiers? child-modifiers))
(set-modifiers-rec child child-modifiers)))) (set-modifiers-rec child child-modifiers))))
modif-tree modif-tree
(-> modif-tree (-> modif-tree
(assoc-in [(:id shape) :modifiers] modifiers))] (assoc-in [(:id shape) :modifiers] modifiers))
(reduce set-child modif-tree children)))] resize-modif?
(set-modifiers-rec modif-tree shape modifiers))) (or (:resize-vector modifiers) (:resize-vector-2 modifiers))]
(reduce (partial set-child (and snap-pixel? resize-modif?)) modif-tree children)))]
(let [modifiers (cond-> modifiers snap-pixel? (set-pixel-precision shape))]
(set-modifiers-rec modif-tree shape modifiers))))
(defn- get-ignore-tree (defn- get-ignore-tree
"Retrieves a map with the flag `ignore-geometry?` given a tree of modifiers" "Retrieves a map with the flag `ignore-geometry?` given a tree of modifiers"

View file

@ -9,9 +9,7 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.geom.shapes :as gsh]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.common.path.commands :as upc]
[app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.state-helpers :as wsh]
[app.main.store :as st] [app.main.store :as st]
[okulary.core :as l])) [okulary.core :as l]))
@ -193,28 +191,29 @@
(assoc :pages (:pages data))))) (assoc :pages (:pages data)))))
st/state =)) st/state =))
(def workspace-data
(l/derived :workspace-data st/state))
(def workspace-file-colors (def workspace-file-colors
(l/derived (fn [state] (l/derived (fn [data]
(when-let [file (:workspace-data state)] (when data
(->> (:colors file) (->> (:colors data)
(d/mapm #(assoc %2 :file-id (:id file)))))) (d/mapm #(assoc %2 :file-id (:id data))))))
st/state)) workspace-data
=))
(def workspace-recent-colors (def workspace-recent-colors
(l/derived (fn [state] (l/derived (fn [data]
(dm/get-in state [:workspace-data :recent-colors] [])) (get data :recent-colors []))
st/state)) workspace-data))
(def workspace-recent-fonts (def workspace-recent-fonts
(l/derived (fn [state] (l/derived (fn [data]
(dm/get-in state [:workspace-data :recent-fonts] [])) (get data :workspace-data []))
st/state)) workspace-data))
(def workspace-file-typography (def workspace-file-typography
(l/derived (fn [state] (l/derived :typographies workspace-data))
(when-let [file (:workspace-data state)]
(:typographies file)))
st/state))
(def workspace-project (def workspace-project
(l/derived :workspace-project st/state)) (l/derived :workspace-project st/state))
@ -313,24 +312,8 @@
workspace-modifiers-with-objects workspace-modifiers-with-objects
=)) =))
(defn- set-content-modifiers [state]
(fn [id shape]
(let [content-modifiers (dm/get-in state [:workspace-local :edit-path id :content-modifiers])]
(if (some? content-modifiers)
(update shape :content upc/apply-content-modifiers content-modifiers)
shape))))
(defn select-bool-children [id] (defn select-bool-children [id]
(let [selector (l/derived (partial wsh/select-bool-children id) st/state =))
(fn [state]
(let [objects (wsh/lookup-page-objects state)
modifiers (:workspace-modifiers state)
children (->> (cph/get-children-ids objects id)
(select-keys objects))]
(as-> children $
(gsh/merge-modifiers $ modifiers)
(d/mapm (set-content-modifiers state) $))))]
(l/derived selector st/state =)))
(def selected-data (def selected-data
(l/derived #(let [selected (wsh/lookup-selected %) (l/derived #(let [selected (wsh/lookup-selected %)
@ -399,11 +382,14 @@
(l/derived #(dm/get-in % [:workspace-file :thumbnails] {}) st/state)) (l/derived #(dm/get-in % [:workspace-file :thumbnails] {}) st/state))
(defn thumbnail-frame-data (defn thumbnail-frame-data
[frame-id] [page-id frame-id]
(l/derived #(get % frame-id) thumbnail-data)) (l/derived
(fn [thumbnails]
(get thumbnails (dm/str page-id frame-id)))
thumbnail-data))
(def workspace-text-modifier (def workspace-text-modifier
(l/derived :workspace-text-modifier st/state)) (l/derived :workspace-text-modifier st/state))
(defn workspace-text-modifier-by-id [id] (defn workspace-text-modifier-by-id [id]
(l/derived #(get % id) workspace-text-modifier)) (l/derived #(get % id) workspace-text-modifier =))

View file

@ -189,7 +189,7 @@
(defn get-object-bounds (defn get-object-bounds
[objects object-id] [objects object-id]
(let [object (get objects object-id) (let [object (get objects object-id)
padding (filters/calculate-padding object) padding (filters/calculate-padding object true)
bounds (-> (filters/get-filters-bounds object) bounds (-> (filters/get-filters-bounds object)
(update :x - (:horizontal padding)) (update :x - (:horizontal padding))
(update :y - (:vertical padding)) (update :y - (:vertical padding))
@ -402,7 +402,7 @@
:style {:-webkit-print-color-adjust :exact} :style {:-webkit-print-color-adjust :exact}
:fill "none"} :fill "none"}
(let [fonts (ff/frame->fonts object objects)] (let [fonts (ff/shape->fonts object objects)]
[:& ff/fontfaces-style {:fonts fonts}]) [:& ff/fontfaces-style {:fonts fonts}])
(case (:type object) (case (:type object)

View file

@ -11,6 +11,7 @@
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.static :as static]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt] [app.util.router :as rt]
@ -59,7 +60,8 @@
(mf/defc verify-token (mf/defc verify-token
[{:keys [route] :as props}] [{:keys [route] :as props}]
(let [token (get-in route [:query-params :token])] (let [token (get-in route [:query-params :token])
bad-token (mf/use-state false)]
(mf/use-effect (mf/use-effect
(fn [] (fn []
(dom/set-html-title (tr "title.default")) (dom/set-html-title (tr "title.default"))
@ -69,13 +71,10 @@
(handle-token tdata)) (handle-token tdata))
(fn [{:keys [type code] :as error}] (fn [{:keys [type code] :as error}]
(cond (cond
(and (= :validation type) (or (= :validation type)
(= :invalid-token code) (= :invalid-token code)
(= :token-expired (:reason error))) (= :token-expired (:reason error)))
(let [msg (tr "errors.token-expired")] (reset! bad-token true)
(ts/schedule 100 #(st/emit! (dm/error msg)))
(st/emit! (rt/nav :auth-login)))
(= :email-already-exists code) (= :email-already-exists code)
(let [msg (tr "errors.email-already-exists")] (let [msg (tr "errors.email-already-exists")]
(ts/schedule 100 #(st/emit! (dm/error msg))) (ts/schedule 100 #(st/emit! (dm/error msg)))
@ -91,5 +90,10 @@
(ts/schedule 100 #(st/emit! (dm/error msg))) (ts/schedule 100 #(st/emit! (dm/error msg)))
(st/emit! (rt/nav :auth-login))))))))) (st/emit! (rt/nav :auth-login)))))))))
[:div.verify-token (if @bad-token
i/loader-pencil])) [:> static/static-header {}
[:div.image i/unchain]
[:div.main-message (tr "errors.invite-invalid")]
[:div.desc-message (tr "errors.invite-invalid.info")]]
[:div.verify-token
i/loader-pencil])))

View file

@ -10,6 +10,7 @@
[app.common.math :as mth] [app.common.math :as mth]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.numeric-input :refer [numeric-input]]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
@ -141,13 +142,19 @@
[:div.editable-select {:class class [:div.editable-select {:class class
:ref on-node-load} :ref on-node-load}
[:input.input-text {:value (or (some-> @state :current-value value->label) "") (if (= type "number")
:on-change handle-change-input [:> numeric-input {:value (or (some-> @state :current-value value->label) "")
:on-key-down handle-key-down :on-change set-value
:on-focus handle-focus :on-focus handle-focus
:on-blur handle-blur :on-blur handle-blur
:placeholder placeholder :placeholder placeholder}]
:type type}] [:input.input-text {:value (or (some-> @state :current-value value->label) "")
:on-change handle-change-input
:on-key-down handle-key-down
:on-focus handle-focus
:on-blur handle-blur
:placeholder placeholder
:type type}])
[:span.dropdown-button {:on-click open-dropdown} i/arrow-down] [:span.dropdown-button {:on-click open-dropdown} i/arrow-down]
[:& dropdown {:show (get @state :is-open? false) [:& dropdown {:show (get @state :is-open? false)

View file

@ -79,6 +79,7 @@
on-focus #(reset! focus? true) on-focus #(reset! focus? true)
on-change (fn [event] on-change (fn [event]
(let [value (-> event dom/get-target dom/get-input-value)] (let [value (-> event dom/get-target dom/get-input-value)]
(swap! form assoc-in [:touched input-name] true)
(fm/on-input-change form input-name value trim))) (fm/on-input-change form input-name value trim)))
on-blur on-blur

View file

@ -13,5 +13,5 @@
class (str "icon-" (name id))] class (str "icon-" (name id))]
`(rumext.alpha/html `(rumext.alpha/html
[:svg {:width 500 :height 500 :class ~class} [:svg {:width 500 :height 500 :class ~class}
[:use {:xlinkHref ~href}]]))) [:use {:href ~href}]])))

View file

@ -138,6 +138,7 @@
(def ruler (icon-xref :ruler)) (def ruler (icon-xref :ruler))
(def ruler-tool (icon-xref :ruler-tool)) (def ruler-tool (icon-xref :ruler-tool))
(def search (icon-xref :search)) (def search (icon-xref :search))
(def set-thumbnail (icon-xref :set-thumbnail))
(def shape-halign-center (icon-xref :shape-halign-center)) (def shape-halign-center (icon-xref :shape-halign-center))
(def shape-halign-left (icon-xref :shape-halign-left)) (def shape-halign-left (icon-xref :shape-halign-left))
(def shape-halign-right (icon-xref :shape-halign-right)) (def shape-halign-right (icon-xref :shape-halign-right))

View file

@ -11,6 +11,7 @@
[app.main.data.dashboard :as dd] [app.main.data.dashboard :as dd]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.forms :as fm] [app.main.ui.components.forms :as fm]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
@ -38,25 +39,27 @@
on-team-up on-team-up
(fn [] (fn []
(st/emit! (modal/show {:type :onboarding-team}))) (st/emit! (modal/show {:type :onboarding-team})))
] teams (mf/deref refs/teams)]
[:div.modal-overlay (if (< (count teams) 2)
[:div.modal-container.onboarding.final.animated.fadeInUp [:div.modal-overlay
[:div.modal-top [:div.modal-container.onboarding.final.animated.fadeInUp
[:h1 {:data-test "onboarding-welcome-title"} (tr "onboarding.welcome.title")] [:div.modal-top
[:p (tr "onboarding.welcome.desc3")]] [:h1 {:data-test "onboarding-welcome-title"} (tr "onboarding.welcome.title")]
[:div.modal-columns [:p (tr "onboarding.welcome.desc3")]]
[:div.modal-left [:div.modal-columns
[:div.content-button {:on-click on-fly-solo [:div.modal-left
:data-test "fly-solo-op"} [:div.content-button {:on-click on-fly-solo
[:h2 (tr "onboarding.choice.fly-solo")] :data-test "fly-solo-op"}
[:p (tr "onboarding.choice.fly-solo-desc")]]] [:h2 (tr "onboarding.choice.fly-solo")]
[:div.modal-right [:p (tr "onboarding.choice.fly-solo-desc")]]]
[:div.content-button {:on-click on-team-up :data-test "team-up-button"} [:div.modal-right
[:h2 (tr "onboarding.choice.team-up")] [:div.content-button {:on-click on-team-up :data-test "team-up-button"}
[:p (tr "onboarding.choice.team-up-desc")]]]] [:h2 (tr "onboarding.choice.team-up")]
[:img.deco {:src "images/deco-left.png" :border "0"}] [:p (tr "onboarding.choice.team-up-desc")]]]]
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]])) [:img.deco {:src "images/deco-left.png" :border "0"}]
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]
[:div {:on-load on-fly-solo}])))
(mf/defc onboarding-team-modal (mf/defc onboarding-team-modal
{::mf/register modal/components {::mf/register modal/components

View file

@ -24,7 +24,7 @@
[:h2 "What's new?"]] [:h2 "What's new?"]]
[:span.release "Beta version " version] [:span.release "Beta version " version]
[:div.modal-content [:div.modal-content
[:p "Penpot continues growing with new features that improve performance, user experience and visual design."] [:p "Penpot continues to grow with new features that improve performance, user experience and visual design."]
[:p "We are happy to show you a sneak peak of the most important stuff that the Beta 1.13 version brings."]] [:p "We are happy to show you a sneak peak of the most important stuff that the Beta 1.13 version brings."]]
[:div.modal-navigation [:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]]] [:button.btn-secondary {:on-click next} "Continue"]]]
@ -41,8 +41,8 @@
[:div.modal-title [:div.modal-title
[:h2 "Multiple exports"]] [:h2 "Multiple exports"]]
[:div.modal-content [:div.modal-content
[:p "Speed your workflow exporting multiple elements simultaneously."] [:p "Speed up your workflow exporting multiple elements simultaneously."]
[:p "Use the export window to manage your multiple exports and be informed about the download progress. Big exports will happen in the background so you can continue designing in the meantime ;)"]] [:p "Use the export window to manage your multiple exports and be informed about the download progress. Big exports will happen in the background so you can keep designing in the meantime ;)"]]
[:div.modal-navigation [:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"] [:button.btn-secondary {:on-click next} "Continue"]
[:& c/navigation-bullets [:& c/navigation-bullets
@ -61,7 +61,7 @@
[:h2 "Multiple fills and strokes"]] [:h2 "Multiple fills and strokes"]]
[:div.modal-content [:div.modal-content
[:p "Now you can add multiple color fills and strokes to a single element, including shapes and texts."] [:p "Now you can add multiple color fills and strokes to a single element, including shapes and texts."]
[:p "This opens endless graphic possibilities such as combining gradients and blending modes in the same element to create visual effects."]] [:p "This opens endless graphic possibilities such as combining gradients and blending modes in the same element to create sophisticated visual effects."]]
[:div.modal-navigation [:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"] [:button.btn-secondary {:on-click next} "Continue"]
[:& c/navigation-bullets [:& c/navigation-bullets
@ -80,7 +80,7 @@
[:h2 "Members area redesign"]] [:h2 "Members area redesign"]]
[:div.modal-content [:div.modal-content
[:p "Penpot is meant for teams, thats why we decided to give some love to the members area."] [:p "Penpot is meant for teams, thats why we decided to give some love to the members area."]
[:p "A refreshed interface and two new features: the Invitations section where you can check the state of the team invites and the ability to invite multiple members at the same time."]] [:p "A refreshed interface and two new features! The Invitations section allows you to check the status of current team invites plus you now have the ability to invite multiple members at the same time."]]
[:div.modal-navigation [:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"] [:button.btn-secondary {:on-click next} "Continue"]
[:& c/navigation-bullets [:& c/navigation-bullets
@ -98,8 +98,8 @@
[:div.modal-title [:div.modal-title
[:h2 "Focus mode"]] [:h2 "Focus mode"]]
[:div.modal-content [:div.modal-content
[:p "Select the elements of a page you want to work with in a specific moment hiding the rest so they dont get in the way of your attention."] [:p "Enjoy a distraction-less design mode by selecting the elements of a page that matter to you and temporarily hiding the rest."]
[:p "This option is also useful to improve the performance in cases where the page has a large number of elements."]] [:p "As a side effect, this can give you a performance boost in massive designs."]]
[:div.modal-navigation [:div.modal-navigation
[:button.btn-secondary {:on-click finish} "Start!"] [:button.btn-secondary {:on-click finish} "Start!"]
[:& c/navigation-bullets [:& c/navigation-bullets

View file

@ -43,7 +43,10 @@
[] []
(let [profile (mf/deref refs/profile) (let [profile (mf/deref refs/profile)
initial (mf/with-memo [profile] initial (mf/with-memo [profile]
(let [subscribed? (-> profile :props :newsletter-subscribed)] (let [subscribed? (-> profile
:props
:newsletter-subscribed
boolean)]
(assoc profile :newsletter-subscribed subscribed?))) (assoc profile :newsletter-subscribed subscribed?)))
form (fm/use-form :spec ::profile-form :initial initial)] form (fm/use-form :spec ::profile-form :initial initial)]

View file

@ -6,6 +6,7 @@
(ns app.main.ui.shapes.attrs (ns app.main.ui.shapes.attrs
(:require (:require
[app.common.colors :as clr]
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
@ -65,7 +66,7 @@
(let [fill-image-id (str "fill-image-" render-id)] (let [fill-image-id (str "fill-image-" render-id)]
{:fill (str "url(#" fill-image-id ")")}) {:fill (str "url(#" fill-image-id ")")})
(contains? shape :fill-color-gradient) (and (contains? shape :fill-color-gradient) (some? (:fill-color-gradient shape)))
(let [fill-color-gradient-id (str "fill-color-gradient_" render-id (if index (str "_" index) ""))] (let [fill-color-gradient-id (str "fill-color-gradient_" render-id (if index (str "_" index) ""))]
{:fill (str "url(#" fill-color-gradient-id ")")}) {:fill (str "url(#" fill-color-gradient-id ")")})
@ -186,11 +187,12 @@
(obj/set! "fillOpacity" (obj/get svg-attrs "fillOpacity"))) (obj/set! "fillOpacity" (obj/get svg-attrs "fillOpacity")))
;; If contains svg-attrs the origin is svg. If it's not svg origin ;; If contains svg-attrs the origin is svg. If it's not svg origin
;; we setup the default fill as transparent (instead of black) ;; we setup the default fill as black
(and (contains? shape :svg-attrs) (and (contains? shape :svg-attrs)
(#{:svg-raw :group} (:type shape)) (#{:svg-raw :group} (:type shape))
(empty? (:fills shape))) (empty? (:fills shape)))
styles (-> styles
(obj/set! "fill" (or (obj/get (:wrapper-styles shape) "fill") clr/black)))
(d/not-empty? (:fills shape)) (d/not-empty? (:fills shape))
(add-fill styles (d/without-nils (get-in shape [:fills 0])) render-id 0) (add-fill styles (d/without-nils (get-in shape [:fills 0])) render-id 0)

View file

@ -6,7 +6,6 @@
(ns app.main.ui.shapes.custom-stroke (ns app.main.ui.shapes.custom-stroke
(:require (:require
[app.common.colors :as clr]
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
@ -35,7 +34,7 @@
clip-id (str "inner-stroke-" render-id "-" (:id shape) suffix) clip-id (str "inner-stroke-" render-id "-" (:id shape) suffix)
shape-id (str "stroke-shape-" render-id "-" (:id shape) suffix)] shape-id (str "stroke-shape-" render-id "-" (:id shape) suffix)]
[:> "clipPath" #js {:id clip-id} [:> "clipPath" #js {:id clip-id}
[:use {:xlinkHref (str "#" shape-id)}]])) [:use {:href (str "#" shape-id)}]]))
(mf/defc outer-stroke-mask (mf/defc outer-stroke-mask
[{:keys [shape render-id index]}] [{:keys [shape render-id index]}]
@ -59,10 +58,10 @@
:width (:width bounding-box) :width (:width bounding-box)
:height (:height bounding-box) :height (:height bounding-box)
:maskUnits "userSpaceOnUse"} :maskUnits "userSpaceOnUse"}
[:use {:xlinkHref (str "#" shape-id) [:use {:href (str "#" shape-id)
:style #js {:fill "none" :stroke "white" :strokeWidth (* stroke-width 2)}}] :style #js {:fill "none" :stroke "white" :strokeWidth (* stroke-width 2)}}]
[:use {:xlinkHref (str "#" shape-id) [:use {:href (str "#" shape-id)
:style #js {:fill "black" :style #js {:fill "black"
:stroke "none"}}]])) :stroke "none"}}]]))
@ -234,7 +233,7 @@
(obj/clone) (obj/clone)
(obj/without ["fill" "fillOpacity" "stroke" "strokeWidth" "strokeOpacity" "strokeStyle" "strokeDasharray"]))))]] (obj/without ["fill" "fillOpacity" "stroke" "strokeWidth" "strokeOpacity" "strokeStyle" "strokeDasharray"]))))]]
[:use {:xlinkHref (str "#" shape-id) [:use {:href (str "#" shape-id)
:mask (str "url(#" stroke-mask-id ")") :mask (str "url(#" stroke-mask-id ")")
:style (-> (obj/get base-props "style") :style (-> (obj/get base-props "style")
(obj/clone) (obj/clone)
@ -242,7 +241,7 @@
(obj/without ["fill" "fillOpacity"]) (obj/without ["fill" "fillOpacity"])
(obj/set! "fill" "none"))}] (obj/set! "fill" "none"))}]
[:use {:xlinkHref (str "#" shape-id) [:use {:href (str "#" shape-id)
:style (-> (obj/get base-props "style") :style (-> (obj/get base-props "style")
(obj/clone) (obj/clone)
(obj/set! "stroke" "none"))}]])) (obj/set! "stroke" "none"))}]]))
@ -278,7 +277,7 @@
[:& stroke-defs {:shape shape :render-id render-id :index index}] [:& stroke-defs {:shape shape :render-id render-id :index index}]
[:> elem-name shape-props]] [:> elem-name shape-props]]
[:use {:xlinkHref (str "#" shape-id) [:use {:href (str "#" shape-id)
:clipPath clip-path}]])) :clipPath clip-path}]]))
; The SVG standard does not implement yet the 'stroke-alignment' ; The SVG standard does not implement yet the 'stroke-alignment'
@ -319,7 +318,7 @@
[:& stroke-defs {:shape shape :render-id render-id :index index}]] [:& stroke-defs {:shape shape :render-id render-id :index index}]]
child]))) child])))
(defn build-fill-props [shape child render-id] (defn build-fill-props [shape child position render-id]
(let [url-fill? (or (some? (:fill-image shape)) (let [url-fill? (or (some? (:fill-image shape))
(= :image (:type shape)) (= :image (:type shape))
(> (count (:fills shape)) 1) (> (count (:fills shape)) 1)
@ -350,7 +349,7 @@
(-> (obj/get props "style") (-> (obj/get props "style")
(obj/clone) (obj/clone)
(obj/without ["fill" "fillOpacity"])))] (obj/without ["fill" "fillOpacity"])))]
(obj/set! props "fill" (dm/fmt "url(#fill-0-%)" render-id))) (obj/set! props "fill" (dm/fmt "url(#fill-%-%)" position render-id)))
(and (some? svg-styles) (obj/contains? svg-styles "fill")) (and (some? svg-styles) (obj/contains? svg-styles "fill"))
(let [style (let [style
@ -361,16 +360,19 @@
(-> props (-> props
(obj/set! "style" style))) (obj/set! "style" style)))
(and (some? svg-attrs) (obj/contains? svg-attrs "fill")) (some? svg-attrs)
(let [style (let [style
(-> (obj/get props "style") (-> (obj/get props "style")
(obj/clone) (obj/clone))
(obj/set! "fill" (obj/get svg-attrs "fill"))
(obj/set! "fillOpacity" (obj/get svg-attrs "fillOpacity")))] style (cond-> style
(obj/contains? svg-attrs "fill")
(->
(obj/set! "fill" (obj/get svg-attrs "fill"))
(obj/set! "fillOpacity" (obj/get svg-attrs "fillOpacity"))))]
(-> props (-> props
(obj/set! "style" style))) (obj/set! "style" style)))
(d/not-empty? (:fills shape)) (d/not-empty? (:fills shape))
(let [fill-props (let [fill-props
(attrs/extract-fill-attrs (get-in shape [:fills 0]) render-id 0) (attrs/extract-fill-attrs (get-in shape [:fills 0]) render-id 0)
@ -383,14 +385,6 @@
(some? style) (some? style)
(obj/set! "style" style))) (obj/set! "style" style)))
(some? (:svg-attrs shape))
(let [style
(-> (obj/get props "style")
(obj/clone)
(obj/set! "fill" clr/black))]
(-> props
(obj/set! "style" style)))
(and (= :path (:type shape)) (empty? (:fills shape))) (and (= :path (:type shape)) (empty? (:fills shape)))
(let [style (let [style
(-> (obj/get props "style") (-> (obj/get props "style")
@ -421,9 +415,10 @@
(let [child (obj/get props "children") (let [child (obj/get props "children")
shape (obj/get props "shape") shape (obj/get props "shape")
elem-name (obj/get child "type") elem-name (obj/get child "type")
render-id (mf/use-ctx muc/render-ctx)] position (or (obj/get props "position") 0)
render-id (or (obj/get props "render-id") (mf/use-ctx muc/render-ctx))]
[:g {:id (dm/fmt "fills-%" (:id shape))} [:g {:id (dm/fmt "fills-%" (:id shape))}
[:> elem-name (build-fill-props shape child render-id)]])) [:> elem-name (build-fill-props shape child position render-id)]]))
(mf/defc shape-strokes (mf/defc shape-strokes
{::mf/wrap-props false} {::mf/wrap-props false}
@ -431,7 +426,7 @@
(let [child (obj/get props "children") (let [child (obj/get props "children")
shape (obj/get props "shape") shape (obj/get props "shape")
elem-name (obj/get child "type") elem-name (obj/get child "type")
render-id (mf/use-ctx muc/render-ctx) render-id (or (obj/get props "render-id") (mf/use-ctx muc/render-ctx))
stroke-id (dm/fmt "strokes-%" (:id shape)) stroke-id (dm/fmt "strokes-%" (:id shape))
stroke-props (-> (obj/new) stroke-props (-> (obj/new)
(obj/set! "id" stroke-id) (obj/set! "id" stroke-id)
@ -456,8 +451,10 @@
(mf/defc shape-custom-strokes (mf/defc shape-custom-strokes
{::mf/wrap-props false} {::mf/wrap-props false}
[props] [props]
(let [children (obj/get props "children") (let [children (obj/get props "children")
shape (obj/get props "shape")] shape (obj/get props "shape")
position (obj/get props "position")
render-id (obj/get props "render-id")]
[:* [:*
[:& shape-fills {:shape shape} children] [:& shape-fills {:shape shape :position position :render-id render-id} children]
[:& shape-strokes {:shape shape} children]])) [:& shape-strokes {:shape shape :position position :render-id render-id} children]]))

View file

@ -79,6 +79,7 @@
(obj/set! "height" height))]) (obj/set! "height" height))])
(when has-image? (when has-image?
[:image {:xlinkHref (get embed uri uri) [:image {:href (get embed uri uri)
:preserveAspectRatio "none"
:width width :width width
:height height}])]])]))))) :height height}])]])])))))

View file

@ -7,6 +7,7 @@
(ns app.main.ui.shapes.filters (ns app.main.ui.shapes.filters
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.math :as mth] [app.common.math :as mth]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
@ -200,25 +201,30 @@
:width (- x2 x1) :width (- x2 x1)
:height (- y2 y1)}))))) :height (- y2 y1)})))))
(defn calculate-padding [shape] (defn calculate-padding
(let [stroke-width (apply max 0 (map #(case (:stroke-alignment % :center) ([shape]
:center (/ (:stroke-width % 0) 2) (calculate-padding shape false))
:outer (:stroke-width % 0)
0) (:strokes shape)))
margin (apply max 0 (map #(gsh/shape-stroke-margin % stroke-width) (:strokes shape))) ([shape ignore-margin?]
(let [stroke-width (apply max 0 (map #(case (:stroke-alignment % :center)
:center (/ (:stroke-width % 0) 2)
:outer (:stroke-width % 0)
0) (:strokes shape)))
margin (if ignore-margin?
0
(apply max 0 (map #(gsh/shape-stroke-margin % stroke-width) (:strokes shape))))
shadow-width (apply max 0 (map #(case (:style % :drop-shadow) shadow-width (apply max 0 (map #(case (:style % :drop-shadow)
:drop-shadow (+ (mth/abs (:offset-x %)) (* (:spread %) 2) (* (:blur %) 2) 10) :drop-shadow (+ (mth/abs (:offset-x %)) (* (:spread %) 2) (* (:blur %) 2) 10)
0) (:shadow shape))) 0) (:shadow shape)))
shadow-height (apply max 0 (map #(case (:style % :drop-shadow) shadow-height (apply max 0 (map #(case (:style % :drop-shadow)
:drop-shadow (+ (mth/abs (:offset-y %)) (* (:spread %) 2) (* (:blur %) 2) 10) :drop-shadow (+ (mth/abs (:offset-y %)) (* (:spread %) 2) (* (:blur %) 2) 10)
0) (:shadow shape)))] 0) (:shadow shape)))]
{:horizontal (+ stroke-width margin shadow-width) {:horizontal (+ stroke-width margin shadow-width)
:vertical (+ stroke-width margin shadow-height)})) :vertical (+ stroke-width margin shadow-height)})))
(defn change-filter-in (defn change-filter-in
"Adds the previous filter as `filter-in` parameter" "Adds the previous filter as `filter-in` parameter"
@ -244,6 +250,7 @@
:height filter-height :height filter-height
:filterUnits "objectBoundingBox" :filterUnits "objectBoundingBox"
:color-interpolation-filters "sRGB"} :color-interpolation-filters "sRGB"}
(for [entry filters] (for [[index entry] (d/enumerate filters)]
[:& filter-entry {:entry entry}])]))) [:& filter-entry {:key (dm/str filter-id "-" index)
:entry entry}])])))

View file

@ -45,15 +45,41 @@
[props] [props]
(let [shape (obj/get props "shape")] (let [shape (obj/get props "shape")]
(when (:thumbnail shape) (when (:thumbnail shape)
[:image.frame-thumbnail (let [{:keys [x y width height]} shape
{:id (dm/str "thumbnail-" (:id shape)) transform (gsh/transform-matrix shape)
:xlinkHref (:thumbnail shape) props (-> (attrs/extract-style-attrs shape)
:x (:x shape) (obj/merge!
:y (:y shape) #js {:x x
:width (:width shape) :y y
:height (:height shape) :transform (str transform)
;; DEBUG :width width
:style {:filter (when (debug? :thumbnails) "sepia(1)")}}]))) :height height
:className "frame-background"}))
path? (some? (.-d props))
render-id (mf/use-ctx muc/render-ctx)]
[:*
[:g {:clip-path (frame-clip-url shape render-id)}
[:& frame-clip-def {:shape shape :render-id render-id}]
[:& shape-fills {:shape shape}
(if path?
[:> :path props]
[:> :rect props])]
[:image.frame-thumbnail
{:id (dm/str "thumbnail-" (:id shape))
:href (:thumbnail shape)
:x (:x shape)
:y (:y shape)
:width (:width shape)
:height (:height shape)
;; DEBUG
:style {:filter (when (debug? :thumbnails) "sepia(1)")}}]]
[:& shape-strokes {:shape shape}
(if path?
[:> :path props]
[:> :rect props])]]))))
(defn frame-shape (defn frame-shape
[shape-wrapper] [shape-wrapper]
@ -79,17 +105,18 @@
[:* [:*
[:g {:clip-path (frame-clip-url shape render-id)} [:g {:clip-path (frame-clip-url shape render-id)}
[:* [:& shape-fills {:shape shape}
[:& shape-fills {:shape shape} (if path?
(if path? [:> :path props]
[:> :path props] [:> :rect props])]
[:> :rect props])]
[:g.frame-children
(for [item childs] (for [item childs]
[:& shape-wrapper {:shape item [:& shape-wrapper {:shape item
:key (dm/str (:id item))}]) :key (dm/str (:id item))}])]]
[:& shape-strokes {:shape shape}
(if path? [:& shape-strokes {:shape shape}
[:> :path props] (if path?
[:> :rect props])]]]]))) [:> :path props]
[:> :rect props])]])))

View file

@ -19,6 +19,31 @@
[app.util.object :as obj] [app.util.object :as obj]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
(defn propagate-wrapper-styles-child
[child wrapper-props]
(let [child-props-childs
(-> (obj/get child "props")
(obj/clone)
(-> (obj/get "childs")))
child-props-childs
(->> child-props-childs
(map #(assoc % :wrapper-styles (obj/get wrapper-props "style"))))
child-props
(-> (obj/get child "props")
(obj/clone)
(obj/set! "childs" child-props-childs))]
(-> (obj/clone child)
(obj/set! "props" child-props))))
(defn propagate-wrapper-styles
([children wrapper-props]
(if (.isArray js/Array children)
(->> children (map #(propagate-wrapper-styles-child % wrapper-props)))
(-> children (propagate-wrapper-styles-child wrapper-props)))))
(mf/defc shape-container (mf/defc shape-container
{::mf/forward-ref true {::mf/forward-ref true
::mf/wrap-props false} ::mf/wrap-props false}
@ -56,7 +81,13 @@
wrapper-props wrapper-props
(cond-> wrapper-props (cond-> wrapper-props
(= :group type) (= :group type)
(attrs/add-style-attrs shape render-id))] (attrs/add-style-attrs shape render-id))
svg-group? (and (contains? shape :svg-attrs) (= :group type))
children (cond-> children
svg-group?
(propagate-wrapper-styles wrapper-props))]
[:& (mf/provider muc/render-ctx) {:value render-id} [:& (mf/provider muc/render-ctx) {:value render-id}
[:> :g wrapper-props [:> :g wrapper-props

View file

@ -94,9 +94,10 @@
{:keys [content]} shape {:keys [content]} shape
{:keys [tag]} content {:keys [tag]} content
svg-root? (and (map? content) (= tag :svg)) svg-root? (and (map? content) (= tag :svg))
svg-tag? (map? content) svg-tag? (map? content)
svg-leaf? (string? content)] svg-leaf? (string? content)
valid-tag? (contains? usvg/svg-tags-list tag)]
(cond (cond
svg-root? svg-root?
@ -104,12 +105,12 @@
(for [item childs] (for [item childs]
[:& shape-wrapper {:shape item :key (dm/str (:id item))}])] [:& shape-wrapper {:shape item :key (dm/str (:id item))}])]
svg-tag? (and svg-tag? valid-tag?)
[:& svg-element {:shape shape} [:& svg-element {:shape shape}
(for [item childs] (for [item childs]
[:& shape-wrapper {:shape item :key (dm/str (:id item))}])] [:& shape-wrapper {:shape item :key (dm/str (:id item))}])]
svg-leaf? (and svg-leaf? valid-tag?)
content content
:else nil)))) :else nil))))

View file

@ -64,21 +64,27 @@
(mf/deps fonts-css) (mf/deps fonts-css)
#(fonts/extract-fontface-urls fonts-css)) #(fonts/extract-fontface-urls fonts-css))
;; Calculate the data-uris for these fonts ;; Calculate the data-uris for these fonts
fonts-embed (embed/use-data-uris fonts-urls) fonts-embed (embed/use-data-uris fonts-urls)
loading? (d/seek #(not (contains? fonts-embed %)) fonts-urls)
;; Creates a style tag by replacing the urls with the data uri ;; Creates a style tag by replacing the urls with the data uri
style (replace-embeds fonts-css fonts-urls fonts-embed)] style (replace-embeds fonts-css fonts-urls fonts-embed)]
(when (d/not-empty? style) (when (d/not-empty? style)
[:style style]))) [:style {:data-loading loading?} style])))
(defn frame->fonts (defn shape->fonts
[frame objects] [shape objects]
(->> (cph/get-children objects (:id frame)) (let [initial (cond-> #{}
(filter cph/text-shape?) (cph/text-shape? shape)
(map (comp fonts/get-content-fonts :content)) (into (fonts/get-content-fonts (:content shape))))]
(reduce set/union #{}))) (->> (cph/get-children objects (:id shape))
(filter cph/text-shape?)
(map (comp fonts/get-content-fonts :content))
(reduce set/union initial))))
(defn shapes->fonts (defn shapes->fonts
[shapes] [shapes]

View file

@ -21,7 +21,7 @@
:width width :width width
:fontFamily "sourcesanspro" :fontFamily "sourcesanspro"
:display "flex" :display "flex"
:whiteSpace "pre-wrap"}] :whiteSpace "break-spaces"}]
(cond-> base (cond-> base
(= valign "top") (obj/set! "alignItems" "flex-start") (= valign "top") (obj/set! "alignItems" "flex-start")
(= valign "center") (obj/set! "alignItems" "center") (= valign "center") (obj/set! "alignItems" "center")
@ -50,7 +50,7 @@
text-align (:text-align data "start") text-align (:text-align data "start")
base #js {:fontSize (str (:font-size data (:font-size txt/default-text-attrs)) "px") base #js {:fontSize (str (:font-size data (:font-size txt/default-text-attrs)) "px")
:lineHeight (:line-height data (:line-height txt/default-text-attrs)) :lineHeight (:line-height data (:line-height txt/default-text-attrs))
:margin "inherit"}] :margin 0}]
(cond-> base (cond-> base
(some? line-height) (obj/set! "lineHeight" line-height) (some? line-height) (obj/set! "lineHeight" line-height)
(some? text-align) (obj/set! "textAlign" text-align)))) (some? text-align) (obj/set! "textAlign" text-align))))
@ -83,7 +83,9 @@
:lineHeight (or line-height "1.2") :lineHeight (or line-height "1.2")
:color (if show-text? text-color "transparent") :color (if show-text? text-color "transparent")
:caretColor (or text-color "black") :caretColor (or text-color "black")
:overflowWrap "initial"} :overflowWrap "initial"
:lineBreak "auto"
:whiteSpace "break-spaces"}
fills fills
(cond (cond
@ -126,7 +128,4 @@
(obj/set! "fontWeight" font-weight)) (obj/set! "fontWeight" font-weight))
(= grow-type :auto-width) (= grow-type :auto-width)
(obj/set! "whiteSpace" "pre") (obj/set! "whiteSpace" "pre")))))
(not= grow-type :auto-width)
(obj/set! "whiteSpace" "pre-wrap")))))

View file

@ -8,6 +8,8 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [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 :as gsh]
[app.config :as cfg] [app.config :as cfg]
[app.main.ui.context :as muc] [app.main.ui.context :as muc]
@ -30,6 +32,23 @@
(d/update-when :position-data #(mapv update-color %)) (d/update-when :position-data #(mapv update-color %))
(assoc :stroke-color "#FFFFFF" :stroke-opacity 1)))) (assoc :stroke-color "#FFFFFF" :stroke-opacity 1))))
(defn position-data-transform
[shape {:keys [x y width height]}]
(let [rect (gsh/make-rect x (- y height) width height)
center (gsh/center-rect rect)]
(when (or (:flip-x shape) (:flip-y shape))
(-> (gmt/matrix)
(gmt/translate center)
(cond-> (:flip-x shape)
(gmt/scale (gpt/point -1 1))
(:flip-y shape)
(gmt/scale (gpt/point 1 -1)))
(gmt/translate (gpt/negate center))
(dm/str)))))
(mf/defc text-shape (mf/defc text-shape
{::mf/wrap-props false {::mf/wrap-props false
::mf/wrap [mf/memo]} ::mf/wrap [mf/memo]}
@ -41,10 +60,11 @@
{:keys [x y width height position-data]} shape {:keys [x y width height position-data]} shape
transform (str (gsh/transform-matrix shape)) transform (str (gsh/transform-matrix shape {:no-flip true}))
;; These position attributes are not really necesary but they are convenient for for the export ;; These position attributes are not really necesary but they are convenient for for the export
group-props (-> #js {:transform transform group-props (-> #js {:transform transform
:className "text-container"
:x x :x x
:y y :y y
:width width :width width
@ -59,10 +79,11 @@
[:defs [:defs
(for [[index data] (d/enumerate position-data)] (for [[index data] (d/enumerate position-data)]
(when (some? (:fill-color-gradient data)) (when (some? (:fill-color-gradient data))
[:& grad/gradient {:id (str "fill-color-gradient_" (get-gradient-id index)) (let [id (dm/str "fill-color-gradient_" (get-gradient-id index))]
:key index [:& grad/gradient {:id id
:attr :fill-color-gradient :key id
:shape data}]))]) :attr :fill-color-gradient
:shape data}])))])
[:> :g group-props [:> :g group-props
(for [[index data] (d/enumerate position-data)] (for [[index data] (d/enumerate position-data)]
@ -72,9 +93,11 @@
alignment-bl (when (cfg/check-browser? :safari) "text-before-edge") alignment-bl (when (cfg/check-browser? :safari) "text-before-edge")
dominant-bl (when-not (cfg/check-browser? :safari) "ideographic") dominant-bl (when-not (cfg/check-browser? :safari) "ideographic")
rtl? (= "rtl" (:direction data))
props (-> #js {:key (dm/str "text-" (:id shape) "-" index) props (-> #js {:key (dm/str "text-" (:id shape) "-" index)
:x (:x data) :x (if rtl? (+ (:x data) (:width data)) (:x data))
:y y :y y
:transform (position-data-transform shape data)
:alignmentBaseline alignment-bl :alignmentBaseline alignment-bl
:dominantBaseline dominant-bl :dominantBaseline dominant-bl
:style (-> #js {:fontFamily (:font-family data) :style (-> #js {:fontFamily (:font-family data)
@ -82,11 +105,14 @@
:fontWeight (:font-weight data) :fontWeight (:font-weight data)
:textTransform (:text-transform data) :textTransform (:text-transform data)
:textDecoration (:text-decoration data) :textDecoration (:text-decoration data)
:letterSpacing (:letter-spacing data)
:fontStyle (:font-style data) :fontStyle (:font-style data)
:direction (if (:rtl data) "rtl" "ltr") :direction (:direction data)
:whiteSpace "pre"} :whiteSpace "pre"}
(obj/set! "fill" (str "url(#fill-" index "-" render-id ")")))}) (obj/set! "fill" (str "url(#fill-" index "-" render-id ")")))})
shape (assoc shape :fills (:fills data))] shape (assoc shape :fills (:fills data))]
[:& shape-custom-strokes {:shape shape :key index} [:& (mf/provider muc/render-ctx) {:key index :value (str render-id "_" (:id shape) "_" index)}
[:> :text props (:text data)]]))]])) [:& shape-custom-strokes {:shape shape :position index :render-id render-id}
[:> :text props (:text data)]]]))]]))

View file

@ -144,8 +144,9 @@
:key (:seqn item)}])) :key (:seqn item)}]))
(when-let [id (:open cstate)] (when-let [id (:open cstate)]
(when-let [thread (-> (get threads-map id) (when-let [thread (as-> (get threads-map id) $
(update :position gpt/transform modifier1))] (when (some? $)
(update $ :position gpt/transform modifier1)))]
[:& cmt/thread-comments {:thread thread [:& cmt/thread-comments {:thread thread
:users users :users users
:zoom zoom}])) :zoom zoom}]))

View file

@ -27,7 +27,7 @@
:circle [:layout :fill :stroke :shadow :blur :svg] :circle [:layout :fill :stroke :shadow :blur :svg]
:path [:layout :fill :stroke :shadow :blur :svg] :path [:layout :fill :stroke :shadow :blur :svg]
:image [:image :layout :fill :stroke :shadow :blur :svg] :image [:image :layout :fill :stroke :shadow :blur :svg]
:text [:layout :text :shadow :blur]}) :text [:layout :text :shadow :blur :stroke]})
(mf/defc attributes (mf/defc attributes
[{:keys [page-id file-id shapes frame]}] [{:keys [page-id file-id shapes frame]}]

View file

@ -58,7 +58,7 @@
(for [shape shapes] (for [shape shapes]
(if (seq (:fills shape)) (if (seq (:fills shape))
(for [value (:fills shape [])] (for [value (:fills shape [])]
[:& fill-block {:key (str "fill-block-" (:id shape)) [:& fill-block {:key (str "fill-block-" (:id shape) value)
:shape value}]) :shape value}])
[:& fill-block {:key (str "fill-block-" (:id shape)) [:& fill-block {:key (str "fill-block-only" (:id shape))
:shape shape}]))]))) :shape shape}]))])))

View file

@ -8,6 +8,7 @@
(:require (:require
[app.common.spec.radius :as ctr] [app.common.spec.radius :as ctr]
[app.main.ui.components.copy-button :refer [copy-button]] [app.main.ui.components.copy-button :refer [copy-button]]
[app.main.ui.formats :as fmt]
[app.util.code-gen :as cg] [app.util.code-gen :as cg]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[app.util.strings :as ust] [app.util.strings :as ust]
@ -39,46 +40,46 @@
[:* [:*
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (tr "handoff.attributes.layout.width")] [:div.attributes-label (tr "handoff.attributes.layout.width")]
[:div.attributes-value (ust/format-precision width 2) "px"] [:div.attributes-value (fmt/format-pixels width)]
[:& copy-button {:data (copy-data selrect :width)}]] [:& copy-button {:data (copy-data selrect :width)}]]
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (tr "handoff.attributes.layout.height")] [:div.attributes-label (tr "handoff.attributes.layout.height")]
[:div.attributes-value (ust/format-precision height 2) "px"] [:div.attributes-value (fmt/format-pixels height)]
[:& copy-button {:data (copy-data selrect :height)}]] [:& copy-button {:data (copy-data selrect :height)}]]
(when (not= (:x shape) 0) (when (not= (:x shape) 0)
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (tr "handoff.attributes.layout.left")] [:div.attributes-label (tr "handoff.attributes.layout.left")]
[:div.attributes-value (ust/format-precision x 2) "px"] [:div.attributes-value (fmt/format-pixels x)]
[:& copy-button {:data (copy-data selrect :x)}]]) [:& copy-button {:data (copy-data selrect :x)}]])
(when (not= (:y shape) 0) (when (not= (:y shape) 0)
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (tr "handoff.attributes.layout.top")] [:div.attributes-label (tr "handoff.attributes.layout.top")]
[:div.attributes-value (ust/format-precision y 2) "px"] [:div.attributes-value (fmt/format-pixels y)]
[:& copy-button {:data (copy-data selrect :y)}]]) [:& copy-button {:data (copy-data selrect :y)}]])
(when (ctr/radius-1? shape) (when (ctr/radius-1? shape)
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (tr "handoff.attributes.layout.radius")] [:div.attributes-label (tr "handoff.attributes.layout.radius")]
[:div.attributes-value (ust/format-precision (:rx shape 0) 2) "px"] [:div.attributes-value (fmt/format-pixels (:rx shape 0))]
[:& copy-button {:data (copy-data shape :rx)}]]) [:& copy-button {:data (copy-data shape :rx)}]])
(when (ctr/radius-4? shape) (when (ctr/radius-4? shape)
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (tr "handoff.attributes.layout.radius")] [:div.attributes-label (tr "handoff.attributes.layout.radius")]
[:div.attributes-value [:div.attributes-value
(ust/format-precision (:r1 shape) 2) ", " (fmt/format-number (:r1 shape)) ", "
(ust/format-precision (:r2 shape) 2) ", " (fmt/format-number (:r2 shape)) ", "
(ust/format-precision (:r3 shape) 2) ", " (fmt/format-number (:r3 shape))", "
(ust/format-precision (:r4 shape) 2) "px"] (fmt/format-pixels (:r4 shape))]
[:& copy-button {:data (copy-data shape :r1)}]]) [:& copy-button {:data (copy-data shape :r1)}]])
(when (not= (:rotation shape 0) 0) (when (not= (:rotation shape 0) 0)
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (tr "handoff.attributes.layout.rotation")] [:div.attributes-label (tr "handoff.attributes.layout.rotation")]
[:div.attributes-value (ust/format-precision (:rotation shape) 2) "deg"] [:div.attributes-value (fmt/format-number (:rotation shape)) "deg"]
[:& copy-button {:data (copy-data shape :rotation)}]])])) [:& copy-button {:data (copy-data shape :rotation)}]])]))

View file

@ -84,7 +84,7 @@
(for [shape shapes] (for [shape shapes]
(if (seq (:strokes shape)) (if (seq (:strokes shape))
(for [value (:strokes shape [])] (for [value (:strokes shape [])]
[:& stroke-block {:key (str "stroke-color-" (:id shape)) [:& stroke-block {:key (str "stroke-color-" (:id shape) value)
:shape value}]) :shape value}])
[:& stroke-block {:key (str "stroke-color-" (:id shape)) [:& stroke-block {:key (str "stroke-color-only" (:id shape))
:shape shape}]))]))) :shape shape}]))])))

View file

@ -101,12 +101,13 @@
[:div.attributes-content-row [:div.attributes-content-row
[:pre.attributes-content (str/trim text)] [:pre.attributes-content (str/trim text)]
[:& copy-button {:data (str/trim text)}]] [:& copy-button {:data (str/trim text)}]]
(when (:fills style)
(for [fill (:fills style)]
(when (or (:fill-color style) (:fill-color-gradient style)) [:& color-row {:format @color-format
[:& color-row {:format @color-format :color (shape->color fill)
:color (shape->color style) :copy-data (copy-style-data fill :fill-color :fill-color-gradient)
:copy-data (copy-style-data style :fill-color :fill-color-gradient) :on-change-format #(reset! color-format %)}]))
:on-change-format #(reset! color-format %)}])
(when (:font-id style) (when (:font-id style)
[:div.attributes-unit-row [:div.attributes-unit-row
@ -186,4 +187,5 @@
[:div.attributes-block-title-text (tr "handoff.attributes.typography")]] [:div.attributes-block-title-text (tr "handoff.attributes.typography")]]
(for [shape shapes] (for [shape shapes]
[:& text-block {:shape shape}])])) [:& text-block {:shape shape
:key (str "text-block" (:id shape))}])]))

View file

@ -65,7 +65,7 @@
[:& frame-wrapper {:shape item [:& frame-wrapper {:shape item
:key (:id item) :key (:id item)
:objects (get frame-objects (:id item)) :objects (get frame-objects (:id item))
:thumbnail? (not (get active-frames (:id item) false))}] :thumbnail? (not (contains? active-frames (:id item)))}]
[:& shape-wrapper {:shape item [:& shape-wrapper {:shape item
:key (:id item)}]))])) :key (:id item)}]))]))

View file

@ -8,8 +8,11 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.uuid :as uuid]
[app.main.data.workspace.thumbnails :as dwt] [app.main.data.workspace.thumbnails :as dwt]
[app.main.fonts :as fonts]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.ui.context :as ctx]
[app.main.ui.hooks :as hooks] [app.main.ui.hooks :as hooks]
[app.main.ui.shapes.embed :as embed] [app.main.ui.shapes.embed :as embed]
[app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.frame :as frame]
@ -25,19 +28,17 @@
[shape-wrapper] [shape-wrapper]
(let [frame-shape (frame/frame-shape shape-wrapper)] (let [frame-shape (frame/frame-shape shape-wrapper)]
(mf/fnc frame-shape-inner (mf/fnc frame-shape-inner
{::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "fonts"]))] {::mf/wrap [#(mf/memo' % (mf/check-props ["shape"]))]
::mf/wrap-props false ::mf/wrap-props false
::mf/forward-ref true} ::mf/forward-ref true}
[props ref] [props ref]
(let [shape (unchecked-get props "shape") (let [shape (unchecked-get props "shape")
fonts (unchecked-get props "fonts")
childs-ref (mf/use-memo (mf/deps (:id shape)) #(refs/children-objects (:id shape))) childs-ref (mf/use-memo (mf/deps (:id shape)) #(refs/children-objects (:id shape)))
childs (mf/deref childs-ref)] childs (mf/deref childs-ref)]
[:& (mf/provider embed/context) {:value true} [:& (mf/provider embed/context) {:value true}
[:& shape-container {:shape shape :ref ref} [:& shape-container {:shape shape :ref ref}
[:& ff/fontfaces-style {:fonts fonts}]
[:& frame-shape {:shape shape :childs childs} ]]])))) [:& frame-shape {:shape shape :childs childs} ]]]))))
(defn check-props (defn check-props
@ -60,16 +61,20 @@
thumbnail? (unchecked-get props "thumbnail?") thumbnail? (unchecked-get props "thumbnail?")
objects (unchecked-get props "objects") objects (unchecked-get props "objects")
fonts (mf/use-memo (mf/deps shape objects) #(ff/frame->fonts shape objects)) render-id (mf/use-memo #(str (uuid/next)))
fonts (mf/use-memo (mf/deps shape objects) #(ff/shape->fonts shape objects))
fonts (-> fonts (hooks/use-equal-memo)) fonts (-> fonts (hooks/use-equal-memo))
force-render (mf/use-state false) force-render (mf/use-state false)
;; Thumbnail data ;; Thumbnail data
frame-id (:id shape) frame-id (:id shape)
thumbnail-data-ref (mf/use-memo (mf/deps frame-id) #(refs/thumbnail-frame-data frame-id)) 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-data (mf/deref thumbnail-data-ref)
thumbnail? (and thumbnail? (or (some? (:thumbnail shape)) (some? thumbnail-data)))
thumbnail? (and thumbnail? (some? thumbnail-data))
;; References to the current rendered node and the its parentn ;; References to the current rendered node and the its parentn
node-ref (mf/use-var nil) node-ref (mf/use-var nil)
@ -84,13 +89,20 @@
disable-thumbnail? (d/not-empty? (dm/get-in modifiers [(:id shape) :modifiers])) disable-thumbnail? (d/not-empty? (dm/get-in modifiers [(:id shape) :modifiers]))
[on-load-frame-dom thumb-renderer] [on-load-frame-dom thumb-renderer]
(ftr/use-render-thumbnail shape node-ref rendered? thumbnail? disable-thumbnail?) (ftr/use-render-thumbnail page-id shape node-ref rendered? thumbnail-data-ref disable-thumbnail?)
on-frame-load on-frame-load
(fns/use-node-store thumbnail? node-ref rendered?)] (fns/use-node-store thumbnail? node-ref rendered?)]
(fdm/use-dynamic-modifiers objects @node-ref modifiers) (fdm/use-dynamic-modifiers objects @node-ref modifiers)
(mf/use-effect
(mf/deps fonts)
(fn []
(->> (rx/from fonts)
(rx/merge-map fonts/fetch-font-css)
(rx/ignore))))
(mf/use-effect (mf/use-effect
(fn [] (fn []
;; When a change in the data is received a "force-render" event is emited ;; When a change in the data is received a "force-render" event is emited
@ -113,11 +125,15 @@
@node-ref) @node-ref)
(when (not @rendered?) (reset! rendered? true))))) (when (not @rendered?) (reset! rendered? true)))))
[:g.frame-container {:key "frame-container" :ref on-frame-load} [:& (mf/provider ctx/render-ctx) {:value render-id}
thumb-renderer [:g.frame-container {:id (dm/str "frame-container-" (:id shape))
:key "frame-container"
:ref on-frame-load}
[:& 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))}]
[:g.frame-thumbnail thumb-renderer]]]))))
[:> frame/frame-thumbnail {:key (dm/str (:id shape))
:shape (cond-> shape
(some? thumbnail-data)
(assoc :thumbnail thumbnail-data))}]]]))))

View file

@ -7,10 +7,223 @@
(ns app.main.ui.workspace.shapes.frame.dynamic-modifiers (ns app.main.ui.workspace.shapes.frame.dynamic-modifiers
(:require (:require
[app.common.data :as d] [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 :as gsh]
[app.main.ui.workspace.viewport.utils :as utils] [app.util.dom :as dom]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
(defn- transform-no-resize
"If we apply a scale directly to the texts it will show deformed so we need to create this
correction matrix to \"undo\" the resize but keep the other transformations."
[{:keys [x y width height points transform transform-inverse] :as shape} current-transform modifiers]
(let [corner-pt (first points)
corner-pt (cond-> corner-pt (some? transform-inverse) (gpt/transform transform-inverse))
resize-x? (some? (:resize-vector modifiers))
resize-y? (some? (:resize-vector-2 modifiers))
flip-x? (neg? (get-in modifiers [:resize-vector :x]))
flip-y? (or (neg? (get-in modifiers [:resize-vector :y]))
(neg? (get-in modifiers [:resize-vector-2 :y])))
result (cond-> (gmt/matrix)
(and (some? transform) (or resize-x? resize-y?))
(gmt/multiply transform)
resize-x?
(gmt/scale (gpt/inverse (:resize-vector modifiers)) corner-pt)
resize-y?
(gmt/scale (gpt/inverse (:resize-vector-2 modifiers)) corner-pt)
flip-x?
(gmt/scale (gpt/point -1 1) corner-pt)
flip-y?
(gmt/scale (gpt/point 1 -1) corner-pt)
(and (some? transform) (or resize-x? resize-y?))
(gmt/multiply transform-inverse))
[width height]
(if (or resize-x? resize-y?)
(let [pc (cond-> (gpt/point x y)
(some? transform)
(gpt/transform transform)
(some? current-transform)
(gpt/transform current-transform))
pw (cond-> (gpt/point (+ x width) y)
(some? transform)
(gpt/transform transform)
(some? current-transform)
(gpt/transform current-transform))
ph (cond-> (gpt/point x (+ y height))
(some? transform)
(gpt/transform transform)
(some? current-transform)
(gpt/transform current-transform))]
[(gpt/distance pc pw) (gpt/distance pc ph)])
[width height])]
[result width height]))
(defn get-nodes
"Retrieve the DOM nodes to apply the matrix transformation"
[base-node {:keys [id type masked-group?]}]
(let [shape-node (dom/query base-node (str "#shape-" id))
frame? (= :frame type)
group? (= :group type)
mask? (and group? masked-group?)]
(cond
frame?
[shape-node
(dom/query shape-node ".frame-children")
(dom/query (str "#thumbnail-container-" id))
(dom/query (str "#thumbnail-" id))]
;; For groups we don't want to transform the whole group but only
;; its filters/masks
mask?
[(dom/query shape-node ".mask-clip-path")
(dom/query shape-node ".mask-shape")]
group?
(let [shape-defs (dom/query shape-node "defs")]
(d/concat-vec
(dom/query-all shape-defs ".svg-def")
(dom/query-all shape-defs ".svg-mask-wrapper")))
:else
[shape-node])))
(defn transform-region!
[node modifiers]
(let [{:keys [x y width height]}
(-> (gsh/make-selrect
(-> (dom/get-attribute node "data-old-x") d/parse-double)
(-> (dom/get-attribute node "data-old-y") d/parse-double)
(-> (dom/get-attribute node "data-old-width") d/parse-double)
(-> (dom/get-attribute node "data-old-height") d/parse-double))
(gsh/transform-selrect modifiers))]
(dom/set-attribute! node "x" x)
(dom/set-attribute! node "y" y)
(dom/set-attribute! node "width" width)
(dom/set-attribute! node "height" height)))
(defn start-transform!
[base-node shapes]
(doseq [shape shapes]
(when-let [nodes (get-nodes base-node shape)]
(doseq [node nodes]
(let [old-transform (dom/get-attribute node "transform")]
(when (some? old-transform)
(dom/set-attribute! node "data-old-transform" old-transform))
(when (or (= (dom/get-tag-name node) "linearGradient")
(= (dom/get-tag-name node) "radialGradient"))
(let [gradient-transform (dom/get-attribute node "gradientTransform")]
(when (some? gradient-transform)
(dom/set-attribute! node "data-old-gradientTransform" gradient-transform))))
(when (= (dom/get-tag-name node) "pattern")
(let [pattern-transform (dom/get-attribute node "patternTransform")]
(when (some? pattern-transform)
(dom/set-attribute! node "data-old-patternTransform" pattern-transform))))
(when (or (= (dom/get-tag-name node) "mask")
(= (dom/get-tag-name node) "filter"))
(let [old-x (dom/get-attribute node "x")
old-y (dom/get-attribute node "y")
old-width (dom/get-attribute node "width")
old-height (dom/get-attribute node "height")]
(dom/set-attribute! node "data-old-x" old-x)
(dom/set-attribute! node "data-old-y" old-y)
(dom/set-attribute! node "data-old-width" old-width)
(dom/set-attribute! node "data-old-height" old-height))))))))
(defn set-transform-att!
[node att value]
(let [old-att (dom/get-attribute node (dm/str "data-old-" att))
new-value (if (some? old-att)
(dm/str value " " old-att)
(str value))]
(dom/set-attribute! node att (str new-value))))
(defn override-transform-att!
[node att value]
(dom/set-attribute! node att (str value)))
(defn update-transform!
[base-node shapes transforms modifiers]
(doseq [{:keys [id type] :as shape} shapes]
(when-let [nodes (get-nodes base-node shape)]
(let [transform (get transforms id)
modifiers (get-in modifiers [id :modifiers])
text? (= type :text)
transform-text? (and text? (and (nil? (:resize-vector modifiers)) (nil? (:resize-vector-2 modifiers))))]
(doseq [node nodes]
(cond
;; Text shapes need special treatment because their resize only change
;; the text area, not the change size/position
(dom/class? node "frame-thumbnail")
(let [[transform] (transform-no-resize shape transform modifiers)]
(set-transform-att! node "transform" transform))
(dom/class? node "frame-children")
(set-transform-att! node "transform" (gmt/inverse transform))
(or (= (dom/get-tag-name node) "mask")
(= (dom/get-tag-name node) "filter"))
(transform-region! node modifiers)
(or (= (dom/get-tag-name node) "linearGradient")
(= (dom/get-tag-name node) "radialGradient"))
(set-transform-att! node "gradientTransform" transform)
(= (dom/get-tag-name node) "pattern")
(set-transform-att! node "patternTransform" transform)
(and (some? transform) (some? node) (or (not text?) transform-text?))
(set-transform-att! node "transform" transform)))))))
(defn remove-transform!
[base-node shapes]
(doseq [shape shapes]
(when-let [nodes (get-nodes base-node shape)]
(doseq [node nodes]
(when (some? node)
(cond
(= (dom/get-tag-name node) "foreignObject")
;; The shape width/height will be automaticaly setup when the modifiers are applied
nil
(or (= (dom/get-tag-name node) "mask")
(= (dom/get-tag-name node) "filter"))
(do
(dom/remove-attribute! node "data-old-x")
(dom/remove-attribute! node "data-old-y")
(dom/remove-attribute! node "data-old-width")
(dom/remove-attribute! node "data-old-height"))
:else
(let [old-transform (dom/get-attribute node "data-old-transform")]
(if (some? old-transform)
(dom/remove-attribute! node "data-old-transform")
(dom/remove-attribute! node "transform")))))))))
(defn use-dynamic-modifiers (defn use-dynamic-modifiers
[objects node modifiers] [objects node modifiers]
@ -20,7 +233,13 @@
(fn [] (fn []
(when (some? modifiers) (when (some? modifiers)
(d/mapm (fn [id {modifiers :modifiers}] (d/mapm (fn [id {modifiers :modifiers}]
(let [center (gsh/center-shape (get objects id))] (let [shape (get objects id)
center (gsh/center-shape shape)
modifiers (cond-> modifiers
;; For texts we only use the displacement because
;; resize needs to recalculate the text layout
(= :text (:type shape))
(select-keys [:displacement :rotation]))]
(gsh/modifiers->transform center modifiers))) (gsh/modifiers->transform center modifiers)))
modifiers)))) modifiers))))
@ -42,13 +261,13 @@
is-cur-val? (d/not-empty? modifiers)] is-cur-val? (d/not-empty? modifiers)]
(when (and (not is-prev-val?) is-cur-val?) (when (and (not is-prev-val?) is-cur-val?)
(utils/start-transform! node shapes)) (start-transform! node shapes))
(when is-cur-val? (when is-cur-val?
(utils/update-transform! node shapes transforms modifiers)) (update-transform! node shapes transforms modifiers))
(when (and is-prev-val? (not is-cur-val?)) (when (and is-prev-val? (not is-cur-val?))
(utils/remove-transform! node @prev-shapes)) (remove-transform! node @prev-shapes))
(reset! prev-modifiers modifiers) (reset! prev-modifiers modifiers)
(reset! prev-transforms transforms) (reset! prev-transforms transforms)

View file

@ -28,7 +28,6 @@
(when (and (some? node) (nil? @node-ref)) (when (and (some? node) (nil? @node-ref))
(let [content (-> (.createElementNS globals/document "http://www.w3.org/2000/svg" "g") (let [content (-> (.createElementNS globals/document "http://www.w3.org/2000/svg" "g")
(dom/add-class! "frame-content"))] (dom/add-class! "frame-content"))]
;;(.appendChild node content)
(reset! node-ref content) (reset! node-ref content)
(reset! parent-ref node) (reset! parent-ref node)
(swap! re-render inc)))))] (swap! re-render inc)))))]

View file

@ -6,6 +6,7 @@
(ns app.main.ui.workspace.shapes.frame.thumbnail-render (ns app.main.ui.workspace.shapes.frame.thumbnail-render
(:require (:require
[app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.math :as mth] [app.common.math :as mth]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
@ -14,26 +15,59 @@
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.object :as obj] [app.util.object :as obj]
[app.util.timers :as ts] [app.util.timers :as ts]
[beicon.core :as rx]
[cuerdas.core :as str]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
;; (def thumbnail-scale-factor 2)
(defn- draw-thumbnail-canvas (defn- draw-thumbnail-canvas
[canvas-node img-node] [canvas-node img-node]
(let [canvas-context (.getContext canvas-node "2d") (try
canvas-width (.-width canvas-node) (when (and (some? canvas-node) (some? img-node))
canvas-height (.-height canvas-node)] (let [canvas-context (.getContext canvas-node "2d")
(.clearRect canvas-context 0 0 canvas-width canvas-height) canvas-width (.-width canvas-node)
(.drawImage canvas-context img-node 0 0 canvas-width canvas-height) canvas-height (.-height canvas-node)]
(.toDataURL canvas-node "image/jpeg" 0.8)))
;; 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)))
(catch :default err
(.error js/console err)
nil)))
(defn- remove-image-loading
"Remove the changes related to change a url for its embed value. This is necessary
so we don't have to recalculate the thumbnail when the image loads."
[value]
(if (.isArray js/Array value)
(->> value
(remove (fn [change]
(or
(= "data-loading" (.-attributeName change))
(and (= "attributes" (.-type change))
(= "href" (.-attributeName change))
(str/starts-with? (.-oldValue change) "http"))))))
[value]))
(defn use-render-thumbnail (defn use-render-thumbnail
"Hook that will create the thumbnail thata" "Hook that will create the thumbnail thata"
[{:keys [id x y width height] :as shape} node-ref rendered? thumbnail? disable?] [page-id {:keys [id x y width height] :as shape} node-ref rendered? thumbnail-data-ref disable?]
(let [frame-canvas-ref (mf/use-ref nil) (let [frame-canvas-ref (mf/use-ref nil)
frame-image-ref (mf/use-ref nil) frame-image-ref (mf/use-ref nil)
disable-ref? (mf/use-var disable?) disable-ref? (mf/use-var disable?)
regenerate-thumbnail (mf/use-var false)
fixed-width (mth/clamp (:width shape) 250 2000) fixed-width (mth/clamp (:width shape) 250 2000)
fixed-height (/ (* (:height shape) fixed-width) (:width shape)) fixed-height (/ (* (:height shape) fixed-width) (:width shape))
@ -42,55 +76,91 @@
shape-ref (hooks/use-update-var shape) shape-ref (hooks/use-update-var shape)
thumbnail-ref? (mf/use-var thumbnail?) updates-str (mf/use-memo #(rx/subject))
on-image-load on-image-load
(mf/use-callback (mf/use-callback
(fn [] (fn []
(let [canvas-node (mf/ref-val frame-canvas-ref) (ts/raf
img-node (mf/ref-val frame-image-ref) #(let [canvas-node (mf/ref-val frame-canvas-ref)
thumb-data (draw-thumbnail-canvas canvas-node img-node)] img-node (mf/ref-val frame-image-ref)
(st/emit! (dw/update-thumbnail id thumb-data)) thumb-data (draw-thumbnail-canvas canvas-node img-node)]
(reset! image-url nil)))) (when (some? thumb-data)
(st/emit! (dw/update-thumbnail page-id id thumb-data))
(reset! image-url nil))))))
on-change generate-thumbnail
(mf/use-callback (mf/use-callback
(fn [] (fn []
(when (and (some? @node-ref) (not @disable-ref?)) (let [node @node-ref
(let [node @node-ref] frame-html (dom/node->xml node)
(ts/schedule-on-idle {:keys [x y width height]} @shape-ref
#(let [frame-html (dom/node->xml node)
{:keys [x y width height]} @shape-ref style-node (dom/query (dm/str "#frame-container-" (:id shape) " style"))
svg-node style-str (or (-> style-node dom/node->xml) "")
(-> (dom/make-node "http://www.w3.org/2000/svg" "svg")
(dom/set-property! "version" "1.1") svg-node
(dom/set-property! "viewBox" (dm/str x " " y " " width " " height)) (-> (dom/make-node "http://www.w3.org/2000/svg" "svg")
(dom/set-property! "width" width) (dom/set-property! "version" "1.1")
(dom/set-property! "height" height) (dom/set-property! "viewBox" (dm/str x " " y " " width " " height))
(dom/set-property! "fill" "none") (dom/set-property! "width" width)
(obj/set! "innerHTML" frame-html)) (dom/set-property! "height" height)
img-src (-> svg-node dom/node->xml dom/svg->data-uri)] (dom/set-property! "fill" "none")
(reset! image-url img-src))))))) (obj/set! "innerHTML" (dm/str style-str frame-html)))
img-src (-> svg-node dom/node->xml dom/svg->data-uri)]
(reset! image-url img-src))))
on-change-frame
(mf/use-callback
(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']")))]
(when (and (not loading-images?) (not loading-fonts?))
(generate-thumbnail)
(reset! regenerate-thumbnail false))))))
on-update-frame
(mf/use-callback
(fn []
(when (not @disable-ref?)
(reset! regenerate-thumbnail true))))
on-load-frame-dom on-load-frame-dom
(mf/use-callback (mf/use-callback
(fn [node] (fn [node]
(when (and (some? node) (nil? @observer-ref)) (when (and (some? node) (nil? @observer-ref))
(on-change []) (when-not (some? @thumbnail-data-ref)
(let [observer (js/MutationObserver. on-change)] (rx/push! updates-str :update))
(.observe observer node #js {:childList true :attributes true :characterData true :subtree true})
(let [observer (js/MutationObserver. (partial rx/push! updates-str))]
(.observe observer node #js {:childList true :attributes true :attributeOldValue true :characterData true :subtree true})
(reset! observer-ref observer)))))] (reset! observer-ref observer)))))]
(mf/use-effect
(fn []
(let [subid (->> updates-str
(rx/map remove-image-loading)
(rx/filter d/not-empty?)
(rx/catch (fn [err] (.error js/console err)))
(rx/subs on-update-frame))]
#(rx/dispose! subid))))
;; on-change-frame will get every change in the frame
(mf/use-effect
(fn []
(let [subid (->> updates-str
(rx/debounce 400)
(rx/observe-on :af)
(rx/catch (fn [err] (.error js/console err)))
(rx/subs on-change-frame))]
#(rx/dispose! subid))))
(mf/use-effect (mf/use-effect
(mf/deps disable?) (mf/deps disable?)
(fn [] (fn []
(reset! disable-ref? disable?))) (reset! disable-ref? disable?)))
(mf/use-effect
(mf/deps thumbnail?)
(fn []
(reset! thumbnail-ref? thumbnail?)))
(mf/use-effect (mf/use-effect
(fn [] (fn []
#(when (and (some? @node-ref) @rendered?) #(when (and (some? @node-ref) @rendered?)
@ -104,7 +174,7 @@
[on-load-frame-dom [on-load-frame-dom
(when (some? @image-url) (when (some? @image-url)
(mf/html (mf/html
[:g.thumbnail-rendering {:opacity 0} [:g.thumbnail-rendering
[:foreignObject {:x x :y y :width width :height height} [:foreignObject {:x x :y y :width width :height height}
[:canvas {:ref frame-canvas-ref [:canvas {:ref frame-canvas-ref
:width fixed-width :width fixed-width
@ -113,7 +183,7 @@
[:image {:ref frame-image-ref [:image {:ref frame-image-ref
:x (:x shape) :x (:x shape)
:y (:y shape) :y (:y shape)
:xlinkHref @image-url :href @image-url
:width (:width shape) :width (:width shape)
:height (:height shape) :height (:height shape)
:on-load on-image-load}]]))])) :on-load on-image-load}]]))]))

View file

@ -25,6 +25,17 @@
[rumext.alpha :as mf]) [rumext.alpha :as mf])
(:import goog.events.EventType)) (:import goog.events.EventType))
(def point-radius 5)
(def point-radius-selected 4)
(def point-radius-active-area 15)
(def point-radius-stroke-width 1)
(def handler-side 6)
(def handler-stroke-width 1)
(def path-preview-dasharray 4)
(def path-snap-stroke-width 1)
(mf/defc path-point [{:keys [position zoom edit-mode hover? selected? preview? start-path? last-p? new-point? curve?]}] (mf/defc path-point [{:keys [position zoom edit-mode hover? selected? preview? start-path? last-p? new-point? curve?]}]
(let [{:keys [x y]} position (let [{:keys [x y]} position
@ -72,8 +83,8 @@
[:circle.path-point [:circle.path-point
{:cx x {:cx x
:cy y :cy y
:r (if (or selected? hover?) (/ 3.5 zoom) (/ 3 zoom)) :r (if (or selected? hover?) (/ point-radius zoom) (/ point-radius-selected zoom))
:style {:stroke-width (/ 1 zoom) :style {:stroke-width (/ point-radius-stroke-width zoom)
:stroke (cond (or selected? hover?) pc/black-color :stroke (cond (or selected? hover?) pc/black-color
preview? pc/secondary-color preview? pc/secondary-color
:else pc/primary-color) :else pc/primary-color)
@ -81,7 +92,7 @@
:else pc/white-color)}}] :else pc/white-color)}}]
[:circle {:cx x [:circle {:cx x
:cy y :cy y
:r (/ 10 zoom) :r (/ point-radius-active-area zoom)
:on-mouse-down on-mouse-down :on-mouse-down on-mouse-down
:on-mouse-enter on-enter :on-mouse-enter on-enter
:on-mouse-leave on-leave :on-mouse-leave on-leave
@ -119,7 +130,7 @@
:x2 x :x2 x
:y2 y :y2 y
:style {:stroke (if hover? pc/black-color pc/gray-color) :style {:stroke (if hover? pc/black-color pc/gray-color)
:stroke-width (/ 1 zoom)}}] :stroke-width (/ point-radius-stroke-width zoom)}}]
(when snap-angle? (when snap-angle?
[:line [:line
@ -128,22 +139,22 @@
:x2 x :x2 x
:y2 y :y2 y
:style {:stroke pc/secondary-color :style {:stroke pc/secondary-color
:stroke-width (/ 1 zoom)}}]) :stroke-width (/ point-radius-stroke-width zoom)}}])
[:rect [:rect
{:x (- x (/ 3 zoom)) {:x (- x (/ handler-side 2 zoom))
:y (- y (/ 3 zoom)) :y (- y (/ handler-side 2 zoom))
:width (/ 6 zoom) :width (/ handler-side zoom)
:height (/ 6 zoom) :height (/ handler-side zoom)
:style {:stroke-width (/ 1 zoom) :style {:stroke-width (/ handler-stroke-width zoom)
:stroke (cond (or selected? hover?) pc/black-color :stroke (cond (or selected? hover?) pc/black-color
:else pc/primary-color) :else pc/primary-color)
:fill (cond selected? pc/primary-color :fill (cond selected? pc/primary-color
:else pc/white-color)}}] :else pc/white-color)}}]
[:circle {:cx x [:circle {:cx x
:cy y :cy y
:r (/ 10 zoom) :r (/ point-radius-active-area zoom)
:on-mouse-down on-mouse-down :on-mouse-down on-mouse-down
:on-mouse-enter on-enter :on-mouse-enter on-enter
:on-mouse-leave on-leave :on-mouse-leave on-leave
@ -156,8 +167,8 @@
(when (not= :move-to (:command command)) (when (not= :move-to (:command command))
[:path {:style {:fill "none" [:path {:style {:fill "none"
:stroke pc/black-color :stroke pc/black-color
:stroke-width (/ 1 zoom) :stroke-width (/ handler-stroke-width zoom)
:stroke-dasharray (/ 4 zoom)} :stroke-dasharray (/ path-preview-dasharray zoom)}
:d (upf/format-path [{:command :move-to :d (upf/format-path [{:command :move-to
:params {:x (:x from) :params {:x (:x from)
:y (:y from)}} :y (:y from)}}
@ -178,7 +189,7 @@
:x2 (:x to) :x2 (:x to)
:y2 (:y to) :y2 (:y to)
:style {:stroke pc/secondary-color :style {:stroke pc/secondary-color
:stroke-width (/ 1 zoom)}}])])) :stroke-width (/ path-snap-stroke-width zoom)}}])]))
(defn matching-handler? [content node handlers] (defn matching-handler? [content node handlers]
(when (= 2 (count handlers)) (when (= 2 (count handlers))

View file

@ -7,6 +7,7 @@
(ns app.main.ui.workspace.shapes.text (ns app.main.ui.workspace.shapes.text
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.math :as mth] [app.common.math :as mth]
[app.main.data.workspace.texts :as dwt] [app.main.data.workspace.texts :as dwt]
[app.main.refs :as refs] [app.main.refs :as refs]
@ -32,24 +33,23 @@
(dwt/apply-text-modifier text-modifier))] (dwt/apply-text-modifier text-modifier))]
[:> shape-container {:shape shape} [:> shape-container {:shape shape}
[:* [:g.text-shape {:key (dm/str "text-" (:id shape))}
[:g.text-shape [:& text/text-shape {:shape shape}]]
[:& text/text-shape {:shape shape}]]
(when (and (debug? :text-outline) (d/not-empty? (:position-data shape))) (when (and (debug? :text-outline) (d/not-empty? (:position-data shape)))
(for [data (:position-data shape)] (for [[index data] (d/enumerate (:position-data shape))]
(let [{:keys [x y width height]} data] (let [{:keys [x y width height]} data]
[:* [:g {:key (dm/str index)}
;; Text fragment bounding box ;; Text fragment bounding box
[:rect {:x x [:rect {:x x
:y (- y height) :y (- y height)
:width width :width width
:height height :height height
:style {:fill "none" :stroke "red"}}] :style {:fill "none" :stroke "red"}}]
;; Text baselineazo ;; Text baselineazo
[:line {:x1 (mth/round x) [:line {:x1 (mth/round x)
:y1 (mth/round (- (:y data) (:height data))) :y1 (mth/round (- (:y data) (:height data)))
:x2 (mth/round (+ x width)) :x2 (mth/round (+ x width))
:y2 (mth/round (- (:y data) (:height data))) :y2 (mth/round (- (:y data) (:height data)))
:style {:stroke "blue"}}]])))]])) :style {:stroke "blue"}}]])))]))

View file

@ -152,7 +152,10 @@
(let [old-state (mf/ref-val prev-value)] (let [old-state (mf/ref-val prev-value)]
(if (and (some? state) (some? old-state)) (if (and (some? state) (some? old-state))
(let [block-changes (ted/get-content-changes old-state state) (let [block-changes (ted/get-content-changes old-state state)
prev-data (ted/get-editor-current-inline-styles old-state)
prev-data (-> (ted/get-editor-current-inline-styles old-state)
(dissoc :text-align :text-direction))
block-to-setup (get-blocks-to-setup block-changes) block-to-setup (get-blocks-to-setup block-changes)
block-to-add-styles (get-blocks-to-add-styles block-changes)] block-to-add-styles (get-blocks-to-add-styles block-changes)]
(-> state (-> state

View file

@ -6,9 +6,9 @@
(ns app.main.ui.workspace.shapes.text.viewport-texts (ns app.main.ui.workspace.shapes.text.viewport-texts
(:require (:require
[app.common.attrs :as attrs]
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth] [app.common.math :as mth]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.common.text :as txt] [app.common.text :as txt]
@ -25,6 +25,24 @@
[app.util.timers :as ts] [app.util.timers :as ts]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
(defn strip-position-data [shape]
(dissoc shape :position-data :transform :transform-inverse))
(defn strip-modifier
[modifier]
(if (or (some? (get-in modifier [:modifiers :resize-vector]))
(some? (get-in modifier [:modifiers :resize-vector-2])))
modifier
(d/update-when modifier :modifiers dissoc :displacement :rotation)))
(defn process-shape [modifiers {:keys [id] :as shape}]
(let [modifier (-> (get modifiers id) strip-modifier)
shape (cond-> shape
(not (gsh/empty-modifiers? (:modifiers modifier)))
(-> (assoc :grow-type :fixed)
(merge modifier) gsh/transform-shape))]
(strip-position-data shape)))
(defn- update-with-editor-state (defn- update-with-editor-state
"Updates the shape with the current state in the editor" "Updates the shape with the current state in the editor"
[shape editor-state] [shape editor-state]
@ -37,7 +55,7 @@
(cond-> shape (cond-> shape
(and (some? shape) (some? editor-content)) (and (some? shape) (some? editor-content))
(assoc :content (attrs/merge content editor-content))))) (assoc :content (d/txt-merge content editor-content)))))
(defn- update-text-shape (defn- update-text-shape
[{:keys [grow-type id]} node] [{:keys [grow-type id]} node]
@ -53,11 +71,12 @@
;; Update the position-data of every text fragment ;; Update the position-data of every text fragment
(let [position-data (utp/calc-position-data node)] (let [position-data (utp/calc-position-data node)]
(st/emit! (dwt/update-position-data id position-data)))) (st/emit! (dwt/update-position-data id position-data)))
(st/emit! (dwt/clean-text-modifier id)))
(defn- update-text-modifier (defn- update-text-modifier
[{:keys [grow-type id]} node] [{:keys [grow-type id]} node]
(let [position-data (utp/calc-position-data node) (let [position-data (utp/calc-position-data node)
props {:position-data position-data} props {:position-data position-data}
@ -74,30 +93,18 @@
(st/emit! (dwt/update-text-modifier id props)))) (st/emit! (dwt/update-text-modifier id props))))
(mf/defc text-container (mf/defc text-container
{::mf/wrap-props false} {::mf/wrap-props false
::mf/wrap [mf/memo]}
[props] [props]
(let [shape (obj/get props "shape") (let [shape (obj/get props "shape")
on-update (obj/get props "on-update") on-update (obj/get props "on-update")
watch-edits (obj/get props "watch-edits")
handle-update handle-update
(mf/use-callback (mf/use-callback
(mf/deps shape on-update) (mf/deps shape on-update)
(fn [node] (fn [node]
(when (some? node) (when (some? node)
(on-update shape node)))) (on-update shape node))))]
text-modifier-ref
(mf/use-memo
(mf/deps (:id shape))
#(refs/workspace-text-modifier-by-id (:id shape)))
text-modifier
(when watch-edits (mf/deref text-modifier-ref))
shape (cond-> shape
(some? text-modifier)
(dwt/apply-text-modifier text-modifier))]
[:& fo/text-shape {:key (str "shape-" (:id shape)) [:& fo/text-shape {:key (str "shape-" (:id shape))
:ref handle-update :ref handle-update
@ -109,36 +116,39 @@
::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]} ::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]}
[props] [props]
(let [text-shapes (obj/get props "text-shapes") (let [text-shapes (obj/get props "text-shapes")
modifiers (obj/get props "modifiers")
prev-modifiers (hooks/use-previous modifiers)
prev-text-shapes (hooks/use-previous text-shapes) prev-text-shapes (hooks/use-previous text-shapes)
;; A change in position-data won't be a "real" change ;; A change in position-data won't be a "real" change
text-change? text-change?
(fn [id] (fn [id]
(let [old-shape (get prev-text-shapes id) (let [old-shape (get prev-text-shapes id)
new-shape (get text-shapes id)] new-shape (get text-shapes id)
(and (not (identical? old-shape new-shape)) old-modifiers (-> (get prev-modifiers id) strip-modifier)
(not= old-shape new-shape)))) new-modifiers (-> (get modifiers id) strip-modifier)]
(or (and (not (identical? old-shape new-shape))
(not= old-shape new-shape))
(not= new-modifiers old-modifiers))))
changed-texts changed-texts
(mf/use-memo (mf/use-memo
(mf/deps text-shapes) (mf/deps text-shapes modifiers)
#(->> (keys text-shapes) #(->> (keys text-shapes)
(filter text-change?) (filter text-change?)
(map (d/getf text-shapes)))) (map (d/getf text-shapes))))
handle-update-modifier (mf/use-callback update-text-modifier)
handle-update-shape (mf/use-callback update-text-shape)] handle-update-shape (mf/use-callback update-text-shape)]
[:* [:*
(for [{:keys [id] :as shape} changed-texts] (for [{:keys [id] :as shape} changed-texts]
[:& text-container {:shape shape [:& text-container {:shape (gsh/transform-shape shape)
:on-update handle-update-shape :on-update (if (some? (get modifiers (:id shape)))
handle-update-modifier
handle-update-shape)
:key (str (dm/str "text-container-" id))}])])) :key (str (dm/str "text-container-" id))}])]))
(defn strip-position-data [[id shape]]
(let [shape (dissoc shape :position-data :transform :transform-inverse)]
[id shape]))
(mf/defc viewport-text-editing (mf/defc viewport-text-editing
{::mf/wrap-props false} {::mf/wrap-props false}
[props] [props]
@ -150,10 +160,29 @@
(-> (mf/deref refs/workspace-editor-state) (-> (mf/deref refs/workspace-editor-state)
(get (:id shape))) (get (:id shape)))
text-modifier-ref
(mf/use-memo (mf/deps (:id shape)) #(refs/workspace-text-modifier-by-id (:id shape)))
text-modifier
(mf/deref text-modifier-ref)
shape (cond-> shape shape (cond-> shape
(some? editor-state) (some? editor-state)
(update-with-editor-state editor-state)) (update-with-editor-state editor-state))
;; When we have a text with grow-type :auto-height we need to check the correct height
;; otherwise the center alignment will break
shape
(if (or (not= :auto-height (:grow-type shape)) (empty? text-modifier))
shape
(let [tr-shape (dwt/apply-text-modifier shape text-modifier)]
(cond-> shape
;; we only change the height otherwise could cause problems with the other fields
(some? text-modifier)
(assoc :height (:height tr-shape)))))
shape (hooks/use-equal-memo shape)
handle-update-shape (mf/use-callback update-text-modifier)] handle-update-shape (mf/use-callback update-text-modifier)]
(mf/use-effect (mf/use-effect
@ -162,28 +191,34 @@
#(st/emit! (dwt/remove-text-modifier (:id shape))))) #(st/emit! (dwt/remove-text-modifier (:id shape)))))
[:& text-container {:shape shape [:& text-container {:shape shape
:watch-edits true
:on-update handle-update-shape}])) :on-update handle-update-shape}]))
(defn check-props (defn check-props
[new-props old-props] [new-props old-props]
(and (identical? (unchecked-get new-props "objects") (unchecked-get old-props "objects")) (and (identical? (unchecked-get new-props "objects")
(= (unchecked-get new-props "edition") (unchecked-get old-props "edition")))) (unchecked-get old-props "objects"))
(identical? (unchecked-get new-props "modifiers")
(unchecked-get old-props "modifiers"))
(= (unchecked-get new-props "edition")
(unchecked-get old-props "edition"))))
(mf/defc viewport-texts (mf/defc viewport-texts
{::mf/wrap-props false {::mf/wrap-props false
::mf/wrap [#(mf/memo' % check-props)]} ::mf/wrap [#(mf/memo' % check-props)]}
[props] [props]
(let [objects (obj/get props "objects") (let [objects (obj/get props "objects")
edition (obj/get props "edition") edition (obj/get props "edition")
modifiers (obj/get props "modifiers")
xf-texts (comp (filter (comp cph/text-shape? second))
(map strip-position-data))
text-shapes text-shapes
(mf/use-memo (mf/use-memo
(mf/deps objects) (mf/deps objects)
#(into {} xf-texts objects)) #(into {} (filter (comp cph/text-shape? second)) objects))
text-shapes
(mf/use-memo
(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)]
@ -198,4 +233,6 @@
[:* [:*
(when editing-shape (when editing-shape
[:& viewport-text-editing {:shape editing-shape}]) [:& viewport-text-editing {:shape editing-shape}])
[:& viewport-texts-wrapper {:text-shapes text-shapes}]]))
[:& viewport-texts-wrapper {:text-shapes (dissoc text-shapes edition)
:modifiers modifiers}]]))

View file

@ -33,6 +33,7 @@
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry]] [app.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry]]
[app.util.color :as uc]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.dom.dnd :as dnd] [app.util.dom.dnd :as dnd]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
@ -1089,10 +1090,21 @@
;; TODO: looks like the first argument is not necessary ;; TODO: looks like the first argument is not necessary
apply-color apply-color
(fn [_ event] (fn [_ event]
(let [ids (wsh/lookup-selected @st/state)] (let [objects (wsh/lookup-page-objects @st/state)
selected (->> (wsh/lookup-selected @st/state)
(cph/clean-loops objects))
selected-obj (keep (d/getf objects) selected)
select-shapes-for-color (fn [shape objects]
(let [shapes (case (:type shape)
:group (cph/get-children objects (:id shape))
[shape])]
(->> shapes
(remove cph/group-shape?)
(map :id))))
ids (mapcat #(select-shapes-for-color % objects) selected-obj)]
(if (kbd/alt? event) (if (kbd/alt? event)
(st/emit! (dc/change-stroke ids color 0)) (st/emit! (dc/change-stroke ids (merge uc/empty-color color) 0))
(st/emit! (dc/change-fill ids color 0))))) (st/emit! (dc/change-fill ids (merge uc/empty-color color) 0)))))
rename-color rename-color
(fn [name] (fn [name]

View file

@ -32,10 +32,9 @@
(l/derived (l/in [:workspace-local :shape-for-rename]) st/state)) (l/derived (l/in [:workspace-local :shape-for-rename]) st/state))
(mf/defc layer-name (mf/defc layer-name
[{:keys [shape on-start-edit on-stop-edit] :as props}] [{:keys [shape on-start-edit on-stop-edit name-ref] :as props}]
(let [local (mf/use-state {}) (let [local (mf/use-state {})
shape-for-rename (mf/deref shape-for-rename-ref) shape-for-rename (mf/deref shape-for-rename-ref)
name-ref (mf/use-ref)
start-edit (fn [] start-edit (fn []
(on-start-edit) (on-start-edit)
@ -96,7 +95,8 @@
container? (or (cph/frame-shape? item) container? (or (cph/frame-shape? item)
(cph/group-shape? item)) (cph/group-shape? item))
disable-drag (mf/use-state false) disable-drag (mf/use-state false)
scroll-to-middle? (mf/use-var true)
expanded-iref (mf/use-memo expanded-iref (mf/use-memo
(mf/deps id) (mf/deps id)
@ -129,6 +129,7 @@
select-shape select-shape
(fn [event] (fn [event]
(dom/prevent-default event) (dom/prevent-default event)
(reset! scroll-to-middle? false)
(let [id (:id item)] (let [id (:id item)]
(cond (cond
(kbd/shift? event) (kbd/shift? event)
@ -177,19 +178,26 @@
:detect-center? container? :detect-center? container?
:data {:id (:id item) :data {:id (:id item)
:index index :index index
:name (:name item)})] :name (:name item)})
ref (mf/use-ref)]
(mf/use-effect (mf/use-effect
(mf/deps selected? selected) (mf/deps selected? selected)
(fn [] (fn []
(let [single? (= (count selected) 1) (let [single? (= (count selected) 1)
node (mf/ref-val dref) node (mf/ref-val ref)
subid subid
(when (and single? selected?) (when (and single? selected?)
(ts/schedule (let [scroll-to @scroll-to-middle?]
100 (ts/schedule
#(dom/scroll-into-view! node #js {:block "center", :behavior "smooth"})))] 100
#(if scroll-to
(dom/scroll-into-view! node #js {:block "center", :behavior "smooth"})
(do
(dom/scroll-into-view-if-needed! node #js {:block "center", :behavior "smooth"})
(reset! scroll-to-middle? true))))))]
#(when (some? subid) #(when (some? subid)
(rx/dispose! subid))))) (rx/dispose! subid)))))
@ -211,6 +219,7 @@
:on-double-click #(dom/stop-propagation %)} :on-double-click #(dom/stop-propagation %)}
[:& si/element-icon {:shape item}] [:& si/element-icon {:shape item}]
[:& layer-name {:shape item [:& layer-name {:shape item
:name-ref ref
:on-start-edit #(reset! disable-drag true) :on-start-edit #(reset! disable-drag true)
:on-stop-edit #(reset! disable-drag false)}] :on-stop-edit #(reset! disable-drag false)}]
@ -368,7 +377,10 @@
search-and-filters search-and-filters
(fn [[id shape]] (fn [[id shape]]
(let [search (:search-text @filter-state) (let [search (:search-text @filter-state)
filters (:active-filters @filter-state)] filters (:active-filters @filter-state)
filters (cond-> filters
(some #{:shape} filters)
(conj :rect :circle :path :bool))]
(or (or
(= uuid/zero id) (= uuid/zero id)
(and (and
@ -431,8 +443,17 @@
[:div.active-filters [:div.active-filters
(for [f (:active-filters @filter-state)] (for [f (:active-filters @filter-state)]
[:span {:on-click (remove-filter f)} (let [name (case f
(tr f) i/cross])] :frame (tr "workspace.sidebar.layers.frames")
:group (tr "workspace.sidebar.layers.groups")
:mask (tr "workspace.sidebar.layers.masks")
:component (tr "workspace.sidebar.layers.components")
:text (tr "workspace.sidebar.layers.texts")
:image (tr "workspace.sidebar.layers.images")
:shape (tr "workspace.sidebar.layers.shapes")
(tr f))]
[:span {:on-click (remove-filter f)}
name i/cross]))]
(when (:show-filters-menu @filter-state) (when (:show-filters-menu @filter-state)
[:div.filters-container [:div.filters-container

View file

@ -25,10 +25,12 @@
[:proportion-lock [:proportion-lock
:width :height :width :height
:x :y :x :y
:ox :oy
:rotation :rotation
:rx :ry :rx :ry
:r1 :r2 :r3 :r4 :r1 :r2 :r3 :r4
:selrect]) :selrect
:points])
(def ^:private type->options (def ^:private type->options
{:bool #{:size :position :rotation} {:bool #{:size :position :rotation}
@ -46,7 +48,7 @@
;; -- User/drawing coords ;; -- User/drawing coords
(mf/defc measures-menu (mf/defc measures-menu
[{:keys [ids ids-with-children values type all-types shape] :as props}] [{:keys [ids ids-with-children values type all-types shape] :as props}]
(let [options (if (= type :multiple) (let [options (if (= type :multiple)
(reduce #(union %1 %2) (map #(get type->options %) all-types)) (reduce #(union %1 %2) (map #(get type->options %) all-types))
(get type->options type)) (get type->options type))
@ -58,21 +60,37 @@
[shape]) [shape])
frames (map #(deref (refs/object-by-id (:frame-id %))) old-shapes) frames (map #(deref (refs/object-by-id (:frame-id %))) old-shapes)
;; To show interactively the measures while the user is manipulating
;; the shape with the mouse, generate a copy of the shapes applying
;; the transient tranformations.
shapes (as-> old-shapes $ shapes (as-> old-shapes $
(map gsh/transform-shape $) (map gsh/transform-shape $)
(map gsh/translate-to-frame $ frames)) (map gsh/translate-to-frame $ frames))
values (let [{:keys [x y]} (-> shapes first :points gsh/points->selrect)] ;; For rotated or stretched shapes, the origin point we show in the menu
;; is not the (:x :y) shape attribute, but the top left coordinate of the
;; wrapping rectangle.
values (let [{:keys [x y]} (gsh/selection-rect [(first shapes)])]
(cond-> values (cond-> values
(not= (:x values) :multiple) (assoc :x x) (not= (:x values) :multiple) (assoc :x x)
(not= (:y values) :multiple) (assoc :y y))) (not= (:y values) :multiple) (assoc :y y)
;; In case of multiple selection, the origin point has been already
;; calculated and given in the fake :ox and :oy attributes. See
;; common/src/app/common/attrs.cljc
(some? (:ox values)) (assoc :x (:ox values))
(some? (:oy values)) (assoc :y (:oy values))))
;; For :height and :width we take those in the :selrect attribute, because
;; not all shapes have an own :width and :height (e. g. paths). Here the
;; rotation is ignored (selrect always has the original size excluding
;; transforms).
values (let [{:keys [width height]} (-> shapes first :selrect)] values (let [{:keys [width height]} (-> shapes first :selrect)]
(cond-> values (cond-> values
(not= (:width values) :multiple) (assoc :width width) (not= (:width values) :multiple) (assoc :width width)
(not= (:height values) :multiple) (assoc :height height))) (not= (:height values) :multiple) (assoc :height height)))
values (let [{:keys [rotation]} (-> shapes first)] ;; The :rotation, however, does use the transforms.
values (let [{:keys [rotation] :or {rotation 0}} (-> shapes first)]
(cond-> values (cond-> values
(not= (:rotation values) :multiple) (assoc :rotation rotation))) (not= (:rotation values) :multiple) (assoc :rotation rotation)))
@ -85,7 +103,6 @@
radius-multi? (mf/use-state nil) radius-multi? (mf/use-state nil)
radius-input-ref (mf/use-ref nil) radius-input-ref (mf/use-ref nil)
on-preset-selected on-preset-selected
(fn [width height] (fn [width height]
(st/emit! (udw/update-dimensions ids :width width) (st/emit! (udw/update-dimensions ids :width width)
@ -279,7 +296,6 @@
{:no-validate true {:no-validate true
:min 0 :min 0
:max 359 :max 359
:default 0
:data-wrap true :data-wrap true
:placeholder "--" :placeholder "--"
:on-click select-all :on-click select-all

View file

@ -34,7 +34,7 @@
(mf/defc stroke-menu (mf/defc stroke-menu
{::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type" "show-caps"]))]} {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type" "show-caps"]))]}
[{:keys [ids type values show-caps] :as props}] [{:keys [ids type values show-caps disable-stroke-style] :as props}]
(let [label (case type (let [label (case type
:multiple (tr "workspace.options.selection-stroke") :multiple (tr "workspace.options.selection-stroke")
:group (tr "workspace.options.group-stroke") :group (tr "workspace.options.group-stroke")
@ -191,4 +191,5 @@
:on-reorder (handle-reorder index) :on-reorder (handle-reorder index)
:disable-drag disable-drag :disable-drag disable-drag
:select-all select-all :select-all select-all
:on-blur on-blur}])])]])) :on-blur on-blur
:disable-stroke-style disable-stroke-style}])])]]))

View file

@ -462,21 +462,13 @@
name-input-ref (mf/use-ref) name-input-ref (mf/use-ref)
on-change-ref (mf/use-ref nil) on-change-ref (mf/use-ref nil)
name-ref (mf/use-ref (:name typography))
on-name-blur on-name-blur
(mf/use-callback (mf/use-callback
(mf/deps on-change) (mf/deps on-change)
(fn [event] (fn [event]
(let [content (dom/get-target-val event)] (let [name (dom/get-target-val event)]
(when-not (str/blank? content) (when-not (str/blank? name)
(let [[path name] (cph/parse-path-name content)] (on-change {:name name })))))]
(on-change {:name name :path path}))))))
on-name-change
(mf/use-callback
(fn [event]
(mf/set-ref-val! name-ref (dom/get-target-val event))))]
(mf/use-effect (mf/use-effect
(mf/deps editing?) (mf/deps editing?)
@ -498,16 +490,6 @@
(fn [] (fn []
(mf/set-ref-val! on-change-ref {:on-change on-change}))) (mf/set-ref-val! on-change-ref {:on-change on-change})))
(mf/use-effect
(fn []
(fn []
(let [content (mf/ref-val name-ref)]
;; On destroy we check if it changed
(when (and (some? content) (not= content (:name typography)))
(let [{:keys [on-change]} (mf/ref-val on-change-ref)
[path name] (cph/parse-path-name content)]
(on-change {:name name :path path})))))))
[:* [:*
[:div.element-set-options-group.typography-entry [:div.element-set-options-group.typography-entry
{:class (when selected? "selected") {:class (when selected? "selected")
@ -582,8 +564,7 @@
{:type "text" {:type "text"
:ref name-input-ref :ref name-input-ref
:default-value (cph/merge-path-item (:path typography) (:name typography)) :default-value (cph/merge-path-item (:path typography) (:name typography))
:on-blur on-name-blur :on-blur on-name-blur}]
:on-change on-name-change}]
[:div.element-set-actions-button [:div.element-set-actions-button
{:on-click #(reset! open? false)} {:on-click #(reset! open? false)}

View file

@ -47,7 +47,7 @@
(second)))) (second))))
(mf/defc stroke-row (mf/defc stroke-row
[{:keys [index stroke title show-caps on-color-change on-reorder on-color-detach on-remove on-stroke-width-change on-stroke-style-change on-stroke-alignment-change open-caps-select close-caps-select on-stroke-cap-start-change on-stroke-cap-end-change on-stroke-cap-switch disable-drag select-all on-blur]}] [{:keys [index stroke title show-caps on-color-change on-reorder on-color-detach on-remove on-stroke-width-change on-stroke-style-change on-stroke-alignment-change open-caps-select close-caps-select on-stroke-cap-start-change on-stroke-cap-end-change on-stroke-cap-switch disable-drag select-all on-blur disable-stroke-style]}]
(let [start-caps-state (mf/use-state {:open? false (let [start-caps-state (mf/use-state {:open? false
:top 0 :top 0
:left 0}) :left 0})
@ -110,14 +110,15 @@
[:option {:value ":inner"} (tr "workspace.options.stroke.inner")] [:option {:value ":inner"} (tr "workspace.options.stroke.inner")]
[:option {:value ":outer"} (tr "workspace.options.stroke.outer")]] [:option {:value ":outer"} (tr "workspace.options.stroke.outer")]]
[:select#style.input-select {:value (enum->string (:stroke-style stroke)) (when-not disable-stroke-style
:on-change (on-stroke-style-change index)} [:select#style.input-select {:value (enum->string (:stroke-style stroke))
(when (= (:stroke-style stroke) :multiple) :on-change (on-stroke-style-change index)}
[:option {:value ""} "--"]) (when (= (:stroke-style stroke) :multiple)
[:option {:value ":solid"} (tr "workspace.options.stroke.solid")] [:option {:value ""} "--"])
[:option {:value ":dotted"} (tr "workspace.options.stroke.dotted")] [:option {:value ":solid"} (tr "workspace.options.stroke.solid")]
[:option {:value ":dashed"} (tr "workspace.options.stroke.dashed")] [:option {:value ":dotted"} (tr "workspace.options.stroke.dotted")]
[:option {:value ":mixed"} (tr "workspace.options.stroke.mixed")]]] [:option {:value ":dashed"} (tr "workspace.options.stroke.dashed")]
[:option {:value ":mixed"} (tr "workspace.options.stroke.mixed")]])]
;; Stroke Caps ;; Stroke Caps
(when show-caps (when show-caps

View file

@ -241,6 +241,8 @@
type :multiple type :multiple
all-types (into #{} (map :type shapes)) all-types (into #{} (map :type shapes))
has-text? (contains? all-types :text)
[measure-ids measure-values] (get-attrs shapes objects :measure) [measure-ids measure-values] (get-attrs shapes objects :measure)
[layer-ids layer-values [layer-ids layer-values
@ -280,7 +282,8 @@
[:& fill-menu {:type type :ids fill-ids :values fill-values}]) [:& fill-menu {:type type :ids fill-ids :values fill-values}])
(when-not (empty? stroke-ids) (when-not (empty? stroke-ids)
[:& stroke-menu {:type type :ids stroke-ids :show-caps show-caps :values stroke-values}]) [:& stroke-menu {:type type :ids stroke-ids :show-caps show-caps :values stroke-values
:disable-stroke-style has-text?}])
(when-not (empty? shapes) (when-not (empty? shapes)
[:& color-selection-menu {:type type :shapes (vals objects-no-measures)}]) [:& color-selection-menu {:type type :shapes (vals objects-no-measures)}])

View file

@ -80,7 +80,8 @@
[:& stroke-menu {:ids ids [:& stroke-menu {:ids ids
:type type :type type
:values stroke-values}] :values stroke-values
:disable-stroke-style true}]
(when (= :multiple (:fills fill-values)) (when (= :multiple (:fills fill-values))
[:& color-selection-menu {:type type :shapes [shape]}]) [:& color-selection-menu {:type type :shapes [shape]}])

View file

@ -90,7 +90,7 @@
hover (mf/use-state nil) hover (mf/use-state nil)
hover-disabled? (mf/use-state false) hover-disabled? (mf/use-state false)
frame-hover (mf/use-state nil) frame-hover (mf/use-state nil)
active-frames (mf/use-state {}) active-frames (mf/use-state #{})
;; REFS ;; REFS
viewport-ref (mf/use-ref nil) viewport-ref (mf/use-ref nil)
@ -168,7 +168,7 @@
show-snap-points? (and (or (contains? layout :dynamic-alignment) show-snap-points? (and (or (contains? layout :dynamic-alignment)
(contains? layout :snap-grid)) (contains? layout :snap-grid))
(or drawing-obj transform)) (or drawing-obj transform))
show-selrect? (and selrect (empty? drawing) (not edition)) show-selrect? (and selrect (empty? drawing) (not text-editing?))
show-measures? (and (not transform) (not node-editing?) show-distances?) show-measures? (and (not transform) (not node-editing?) show-distances?)
show-artboard-names? (contains? layout :display-artboard-names) show-artboard-names? (contains? layout :display-artboard-names)
show-rules? (and (contains? layout :rules) (not (contains? layout :hide-ui))) show-rules? (and (contains? layout :rules) (not (contains? layout :hide-ui)))
@ -183,7 +183,7 @@
(hooks/setup-hover-shapes page-id move-stream base-objects transform selected mod? hover hover-ids @hover-disabled? focus zoom) (hooks/setup-hover-shapes page-id move-stream base-objects transform selected mod? hover hover-ids @hover-disabled? focus zoom)
(hooks/setup-viewport-modifiers modifiers base-objects) (hooks/setup-viewport-modifiers modifiers base-objects)
(hooks/setup-shortcuts node-editing? drawing-path?) (hooks/setup-shortcuts node-editing? drawing-path?)
(hooks/setup-active-frames base-objects vbox hover active-frames zoom) (hooks/setup-active-frames base-objects hover-ids selected active-frames zoom transform vbox)
[:div.viewport [:div.viewport
[:div.viewport-overlays {:ref overlays-ref} [:div.viewport-overlays {:ref overlays-ref}
@ -246,6 +246,7 @@
[:& stv/viewport-texts {:key (dm/str "texts-" page-id) [:& stv/viewport-texts {:key (dm/str "texts-" page-id)
:page-id page-id :page-id page-id
:objects base-objects :objects base-objects
:modifiers modifiers
:edition edition}]]] :edition edition}]]]
[:svg.viewport-controls [:svg.viewport-controls
@ -257,6 +258,7 @@
:ref viewport-ref :ref viewport-ref
:class (when drawing-tool "drawing") :class (when drawing-tool "drawing")
:style {:cursor @cursor} :style {:cursor @cursor}
:fill "none"
:on-click on-click :on-click on-click
:on-context-menu on-context-menu :on-context-menu on-context-menu
@ -297,7 +299,8 @@
(when show-text-editor? (when show-text-editor?
[:& text-edition-outline [:& text-edition-outline
{:shape (get base-objects edition)}]) {:shape (get base-objects edition)
:zoom zoom}])
(when show-measures? (when show-measures?
[:& msr/measurement [:& msr/measurement

View file

@ -21,12 +21,14 @@
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.dom.dnd :as dnd] [app.util.dom.dnd :as dnd]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.normalize-wheel :as nw]
[app.util.object :as obj] [app.util.object :as obj]
[app.util.timers :as timers] [app.util.timers :as timers]
[beicon.core :as rx] [beicon.core :as rx]
[cuerdas.core :as str] [cuerdas.core :as str]
[rumext.alpha :as mf]) [rumext.alpha :as mf]))
(:import goog.events.WheelEvent))
(def scale-per-pixel -0.0057)
(defn on-mouse-down (defn on-mouse-down
[{:keys [id blocked hidden type]} selected edition drawing-tool text-editing? [{:keys [id blocked hidden type]} selected edition drawing-tool text-editing?
@ -377,32 +379,24 @@
(dom/stop-propagation event) (dom/stop-propagation event)
(let [pt (->> (dom/get-client-position event) (let [pt (->> (dom/get-client-position event)
(utils/translate-point-to-viewport viewport zoom)) (utils/translate-point-to-viewport viewport zoom))
norm-event ^js (nw/normalize-wheel event)
ctrl? (kbd/ctrl? event) ctrl? (kbd/ctrl? event)
delta-y (.-pixelY norm-event)
delta-x (.-pixelX norm-event)]
delta-mode (.-deltaMode ^js event)
unit (cond
(= delta-mode WheelEvent.DeltaMode.PIXEL) 1
(= delta-mode WheelEvent.DeltaMode.LINE) 16
(= delta-mode WheelEvent.DeltaMode.PAGE) 100)
delta-y (-> (.-deltaY ^js event)
(* unit)
(/ zoom))
delta-x (-> (.-deltaX ^js event)
(* unit)
(/ zoom))]
(if (or ctrl? mod?) (if (or ctrl? mod?)
(let [delta (* -1 (+ delta-y delta-x)) (let [delta-zoom (+ delta-y delta-x)
scale (-> (+ 1 (/ delta 100)) (mth/clamp 0.77 1.3))] scale (+ 1 (mth/abs (* scale-per-pixel delta-zoom)))
scale (if (pos? delta-zoom) (/ 1 scale) scale)]
(st/emit! (dw/set-zoom pt scale))) (st/emit! (dw/set-zoom pt scale)))
(if (and (not (cfg/check-platform? :macos)) (if (and (not (cfg/check-platform? :macos))
;; macos sends delta-x automatically, don't need to do it ;; macos sends delta-x automatically, don't need to do it
(kbd/shift? event)) (kbd/shift? event))
(st/emit! (dw/update-viewport-position {:x #(+ % delta-y)})) (st/emit! (dw/update-viewport-position {:x #(+ % (/ delta-y zoom))}))
(st/emit! (dw/update-viewport-position {:x #(+ % delta-x) (st/emit! (dw/update-viewport-position {:x #(+ % (/ delta-x zoom))
:y #(+ % delta-y)})))))))))) :y #(+ % (/ delta-y zoom))}))))))))))
(defn on-drag-enter [] (defn on-drag-enter []
(mf/use-callback (mf/use-callback

View file

@ -214,37 +214,65 @@
(defn inside-vbox [vbox objects frame-id] (defn inside-vbox [vbox objects frame-id]
(let [frame (get objects frame-id)] (let [frame (get objects frame-id)]
(and (some? frame) (gsh/overlaps? frame vbox))))
(and (some? frame)
(gsh/overlaps? frame vbox))))
(defn setup-active-frames (defn setup-active-frames
[objects vbox hover active-frames zoom] [objects hover-ids selected active-frames zoom transform vbox]
(mf/use-effect (let [frame? #(= :frame (get-in objects [% :type]))
(mf/deps vbox) all-frames (mf/use-memo (mf/deps objects) #(cph/get-frames-ids objects))
selected-frames (mf/use-memo (mf/deps selected) #(->> all-frames (filter selected)))
xf-selected-frame (comp (remove frame?) (map #(get-in objects [% :frame-id])))
selected-shapes-frames (mf/use-memo (mf/deps selected) #(into #{} xf-selected-frame selected))
(fn [] active-selection (when (and (not= transform :move) (= (count selected-frames) 1)) (first selected-frames))
(swap! active-frames hover-frame (last @hover-ids)
(fn [active-frames] last-hover-frame (mf/use-var nil)]
(let [set-active-frames
(fn [active-frames id active?]
(cond-> active-frames
(and active? (inside-vbox vbox objects id))
(assoc id true)))]
(reduce-kv set-active-frames {} active-frames))))))
(mf/use-effect (mf/use-effect
(mf/deps @hover @active-frames zoom) (mf/deps hover-frame)
(fn [] (fn []
(let [frame-id (if (= :frame (:type @hover)) (when (some? hover-frame)
(:id @hover) (reset! last-hover-frame hover-frame))))
(:frame-id @hover))]
(if (< zoom 0.25) (mf/use-effect
(when (some? @active-frames) (mf/deps objects @hover-ids selected zoom transform vbox)
(reset! active-frames nil)) (fn []
(when (and (some? frame-id)(not (contains? @active-frames frame-id)))
(reset! active-frames {frame-id true}))))))) ;; Rules for active frame:
;; - If zoom < 25% displays thumbnail except when selecting a single frame or a child
;; - We always active the current hovering frame for zoom > 25%
;; - When zoom > 130% we activate the frames that are inside the vbox
;; - If no hovering over any frames we keep the previous active one
;; - Check always that the active frames are inside the vbox
(let [is-active-frame?
(fn [id]
(or
;; Zoom > 130% shows every frame
(> zoom 1.3)
;; Zoom >= 25% will show frames hovering
(and
(>= zoom 0.25)
(or (= id hover-frame) (= id @last-hover-frame)))
;; Otherwise, if it's a selected frame
(= id active-selection)
;; Or contains a selected shape
(contains? selected-shapes-frames id)))
new-active-frames
(into #{}
(comp (filter is-active-frame?)
;; We only allow active frames that are contained in the vbox
(filter (partial inside-vbox vbox objects)))
all-frames)]
(when (not= @active-frames new-active-frames)
(reset! active-frames new-active-frames)))))))
;; NOTE: this is executed on each page change, maybe we need to move ;; NOTE: this is executed on each page change, maybe we need to move
;; this shortcuts outside the viewport? ;; this shortcuts outside the viewport?

View file

@ -6,6 +6,7 @@
(ns app.main.ui.workspace.viewport.scroll-bars (ns app.main.ui.workspace.viewport.scroll-bars
(:require (:require
[app.common.colors :as clr]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
@ -188,7 +189,7 @@
[:* [:*
(when show-v-scroll? (when show-v-scroll?
[:g.v-scroll [:g.v-scroll {:fill clr/black}
[:rect {:on-mouse-move #(on-mouse-move % :y) [:rect {:on-mouse-move #(on-mouse-move % :y)
:on-mouse-down #(on-mouse-down % :y) :on-mouse-down #(on-mouse-down % :y)
:on-mouse-up on-mouse-up :on-mouse-up on-mouse-up
@ -202,7 +203,7 @@
:style {:stroke "white" :style {:stroke "white"
:stroke-width (/ 0.15 zoom)}}]]) :stroke-width (/ 0.15 zoom)}}]])
(when show-h-scroll? (when show-h-scroll?
[:g.h-scroll [:g.h-scroll {:fill clr/black}
[:rect {:on-mouse-move #(on-mouse-move % :x) [:rect {:on-mouse-move #(on-mouse-move % :x)
:on-mouse-down #(on-mouse-down % :x) :on-mouse-down #(on-mouse-down % :x)
:on-mouse-up on-mouse-up :on-mouse-up on-mouse-up

View file

@ -8,224 +8,10 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.main.ui.cursors :as cur] [app.main.ui.cursors :as cur]
[app.util.dom :as dom])) [app.util.dom :as dom]))
(defn- text-corrected-transform
"If we apply a scale directly to the texts it will show deformed so we need to create this
correction matrix to \"undo\" the resize but keep the other transformations."
[{:keys [x y width height points transform transform-inverse] :as shape} current-transform modifiers]
(let [corner-pt (first points)
corner-pt (cond-> corner-pt (some? transform-inverse) (gpt/transform transform-inverse))
resize-x? (some? (:resize-vector modifiers))
resize-y? (some? (:resize-vector-2 modifiers))
flip-x? (neg? (get-in modifiers [:resize-vector :x]))
flip-y? (or (neg? (get-in modifiers [:resize-vector :y]))
(neg? (get-in modifiers [:resize-vector-2 :y])))
result (cond-> (gmt/matrix)
(and (some? transform) (or resize-x? resize-y?))
(gmt/multiply transform)
resize-x?
(gmt/scale (gpt/inverse (:resize-vector modifiers)) corner-pt)
resize-y?
(gmt/scale (gpt/inverse (:resize-vector-2 modifiers)) corner-pt)
flip-x?
(gmt/scale (gpt/point -1 1) corner-pt)
flip-y?
(gmt/scale (gpt/point 1 -1) corner-pt)
(and (some? transform) (or resize-x? resize-y?))
(gmt/multiply transform-inverse))
[width height]
(if (or resize-x? resize-y?)
(let [pc (cond-> (gpt/point x y)
(some? transform)
(gpt/transform transform)
(some? current-transform)
(gpt/transform current-transform))
pw (cond-> (gpt/point (+ x width) y)
(some? transform)
(gpt/transform transform)
(some? current-transform)
(gpt/transform current-transform))
ph (cond-> (gpt/point x (+ y height))
(some? transform)
(gpt/transform transform)
(some? current-transform)
(gpt/transform current-transform))]
[(gpt/distance pc pw) (gpt/distance pc ph)])
[width height])]
[result width height]))
(defn get-nodes
"Retrieve the DOM nodes to apply the matrix transformation"
[base-node {:keys [id type masked-group?]}]
(let [shape-node (dom/query base-node (str "#shape-" id))
frame? (= :frame type)
group? (= :group type)
text? (= :text type)
mask? (and group? masked-group?)
;; When the shape is a frame we maybe need to move its thumbnail
thumb-node (when frame? (dom/query (str "#thumbnail-" id)))]
(cond
frame?
[thumb-node
(dom/get-parent (dom/query shape-node ".frame-background"))
(dom/query shape-node ".frame-clip")]
;; For groups we don't want to transform the whole group but only
;; its filters/masks
mask?
[(dom/query shape-node ".mask-clip-path")
(dom/query shape-node ".mask-shape")]
group?
(let [shape-defs (dom/query shape-node "defs")]
(d/concat-vec
(dom/query-all shape-defs ".svg-def")
(dom/query-all shape-defs ".svg-mask-wrapper")))
text?
[shape-node
(dom/query shape-node ".text-shape")]
:else
[shape-node])))
(defn transform-region!
[node modifiers]
(let [{:keys [x y width height]}
(-> (gsh/make-selrect
(-> (dom/get-attribute node "data-old-x") d/parse-double)
(-> (dom/get-attribute node "data-old-y") d/parse-double)
(-> (dom/get-attribute node "data-old-width") d/parse-double)
(-> (dom/get-attribute node "data-old-height") d/parse-double))
(gsh/transform-selrect modifiers))]
(dom/set-attribute! node "x" x)
(dom/set-attribute! node "y" y)
(dom/set-attribute! node "width" width)
(dom/set-attribute! node "height" height)))
(defn start-transform!
[base-node shapes]
(doseq [shape shapes]
(when-let [nodes (get-nodes base-node shape)]
(doseq [node nodes]
(let [old-transform (dom/get-attribute node "transform")]
(when (some? old-transform)
(dom/set-attribute! node "data-old-transform" old-transform))
(when (or (= (dom/get-tag-name node) "linearGradient")
(= (dom/get-tag-name node) "radialGradient"))
(let [gradient-transform (dom/get-attribute node "gradientTransform")]
(when (some? gradient-transform)
(dom/set-attribute! node "data-old-gradientTransform" gradient-transform))))
(when (= (dom/get-tag-name node) "pattern")
(let [pattern-transform (dom/get-attribute node "patternTransform")]
(when (some? pattern-transform)
(dom/set-attribute! node "data-old-patternTransform" pattern-transform))))
(when (or (= (dom/get-tag-name node) "mask")
(= (dom/get-tag-name node) "filter"))
(let [old-x (dom/get-attribute node "x")
old-y (dom/get-attribute node "y")
old-width (dom/get-attribute node "width")
old-height (dom/get-attribute node "height")]
(dom/set-attribute! node "data-old-x" old-x)
(dom/set-attribute! node "data-old-y" old-y)
(dom/set-attribute! node "data-old-width" old-width)
(dom/set-attribute! node "data-old-height" old-height))))))))
(defn set-transform-att!
[node att value]
(let [old-att (dom/get-attribute node (dm/str "data-old-" att))
new-value (if (some? old-att)
(dm/str value " " old-att)
(str value))]
(dom/set-attribute! node att (str new-value))))
(defn update-transform!
[base-node shapes transforms modifiers]
(doseq [{:keys [id type] :as shape} shapes]
(when-let [nodes (get-nodes base-node shape)]
(let [transform (get transforms id)
modifiers (get-in modifiers [id :modifiers])
[text-transform _text-width _text-height]
(when (= :text type)
(text-corrected-transform shape transform modifiers))]
(doseq [node nodes]
(cond
;; Text shapes need special treatment because their resize only change
;; the text area, not the change size/position
(dom/class? node "text-shape")
(when (some? text-transform)
(set-transform-att! node "transform" text-transform))
(or (= (dom/get-tag-name node) "mask")
(= (dom/get-tag-name node) "filter"))
(transform-region! node modifiers)
(or (= (dom/get-tag-name node) "linearGradient")
(= (dom/get-tag-name node) "radialGradient"))
(set-transform-att! node "gradientTransform" transform)
(= (dom/get-tag-name node) "pattern")
(set-transform-att! node "patternTransform" transform)
(and (some? transform) (some? node))
(set-transform-att! node "transform" transform)))))))
(defn remove-transform!
[base-node shapes]
(doseq [shape shapes]
(when-let [nodes (get-nodes base-node shape)]
(doseq [node nodes]
(when (some? node)
(cond
(= (dom/get-tag-name node) "foreignObject")
;; The shape width/height will be automaticaly setup when the modifiers are applied
nil
(or (= (dom/get-tag-name node) "mask")
(= (dom/get-tag-name node) "filter"))
(do
(dom/remove-attribute! node "data-old-x")
(dom/remove-attribute! node "data-old-y")
(dom/remove-attribute! node "data-old-width")
(dom/remove-attribute! node "data-old-height"))
:else
(let [old-transform (dom/get-attribute node "data-old-transform")]
(if (some? old-transform)
(dom/remove-attribute! node "data-old-transform")
(dom/remove-attribute! node "transform")))))))))
(defn format-viewbox [vbox] (defn format-viewbox [vbox]
(dm/str (:x vbox 0) " " (dm/str (:x vbox 0) " "
(:y vbox 0) " " (:y vbox 0) " "

View file

@ -108,19 +108,19 @@
on-double-click on-double-click
(mf/use-callback (mf/use-callback
(mf/deps (:id frame)) (mf/deps (:id frame))
(st/emitf (dw/go-to-layout :layers) (st/emitf (dw/go-to-layout :layers)
(dw/start-rename-shape (:id frame)))) (dw/start-rename-shape (:id frame))))
on-context-menu on-context-menu
(mf/use-callback (mf/use-callback
(mf/deps frame) (mf/deps frame)
(fn [bevent] (fn [bevent]
(let [event (.-nativeEvent bevent) (let [event (.-nativeEvent bevent)
position (dom/get-client-position event)] position (dom/get-client-position event)]
(dom/prevent-default event) (dom/prevent-default event)
(dom/stop-propagation event) (dom/stop-propagation event)
(st/emit! (dw/show-shape-context-menu {:position position :shape frame}))))) (st/emit! (dw/show-shape-context-menu {:position position :shape frame})))))
on-pointer-enter on-pointer-enter
(mf/use-callback (mf/use-callback
@ -132,27 +132,42 @@
(mf/use-callback (mf/use-callback
(mf/deps (:id frame) on-frame-leave) (mf/deps (:id frame) on-frame-leave)
(fn [_] (fn [_]
(on-frame-leave (:id frame))))] (on-frame-leave (:id frame))))
text-pos-x (if (:use-for-thumbnail? frame) 15 0)]
[:text {:x 0 [:*
:y 0 (when (:use-for-thumbnail? frame)
:width width [:g {:transform (str (when (and selected? modifiers)
:height 20 (str (:displacement modifiers) " "))
:class "workspace-frame-label" (text-transform label-pos zoom))}
:transform (str (when (and selected? modifiers) [:svg {:x 0
(str (:displacement modifiers) " " )) :y -9
(text-transform label-pos zoom)) :width 12
:style {:fill (when selected? "var(--color-primary-dark)")} :height 12
:visibility (if show-artboard-names? "visible" "hidden") :class "workspace-frame-icon"
:on-mouse-down on-mouse-down :style {:fill (when selected? "var(--color-primary-dark)")}
:on-double-click on-double-click :visibility (if show-artboard-names? "visible" "hidden")}
:on-context-menu on-context-menu [:use {:href "#icon-set-thumbnail"}]]])
:on-pointer-enter on-pointer-enter [:text {:x text-pos-x
:on-pointer-leave on-pointer-leave} :y 0
(:name frame)])) :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)]]))
(mf/defc frame-titles (mf/defc frame-titles
{::mf/wrap-props false} {::mf/wrap-props false
::mf/wrap [mf/memo]}
[props] [props]
(let [objects (unchecked-get props "objects") (let [objects (unchecked-get props "objects")
zoom (unchecked-get props "zoom") zoom (unchecked-get props "zoom")

View file

@ -114,7 +114,7 @@
(rx/map (fn [objects] (rx/map (fn [objects]
(let [objects (render/adapt-objects-for-shape objects object-id)] (let [objects (render/adapt-objects-for-shape objects object-id)]
{:objects objects {:objects objects
:object object-id})))))) :object (get objects object-id)}))))))
{:keys [objects object]} (use-resource fetch-state)] {:keys [objects object]} (use-resource fetch-state)]
@ -264,7 +264,7 @@
:embed embed} :embed embed}
(when-let [component-id (:component-id @state)] (when-let [component-id (:component-id @state)]
[:use {:x 0 :y 0 :xlinkHref (str "#" component-id)}])]] [:use {:x 0 :y 0 :href (str "#" component-id)}])]]
]))) ])))

View file

@ -8,6 +8,7 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.text :as txt] [app.common.text :as txt]
[app.main.ui.formats :as fmt]
[app.util.color :as uc] [app.util.color :as uc]
[cuerdas.core :as str])) [cuerdas.core :as str]))
@ -108,7 +109,7 @@
(every? #(or (nil? %) (= % 0)) value) (every? #(or (nil? %) (= % 0)) value)
(or (nil? value) (= value 0)))) (or (nil? value) (= value 0))))
default-format (fn [value] (str value "px")) default-format (fn [value] (str (fmt/format-pixels value)))
format-property (fn [prop] format-property (fn [prop]
(let [css-prop (or (prop to-prop) (name prop)) (let [css-prop (or (prop to-prop) (name prop))
format-fn (or (prop format) default-format) format-fn (or (prop format) default-format)

View file

@ -345,8 +345,9 @@
(defn node->xml (defn node->xml
[node] [node]
(-> (js/XMLSerializer.) (when (some? node)
(.serializeToString node))) (-> (js/XMLSerializer.)
(.serializeToString node))))
(defn svg->data-uri (defn svg->data-uri
[svg] [svg]

View file

@ -0,0 +1,179 @@
/**
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
/**
Adapted to google closure from:
https://raw.githubusercontent.com/facebookarchive/fixed-data-table/master/src/vendor_upstream/dom/normalizeWheel.js
*/
'use strict';
goog.provide("app.util.normalize_wheel");
goog.scope(function() {
const self = app.util.normalize_wheel;
// const UserAgent_DEPRECATED = require('UserAgent_DEPRECATED');
// const isEventSupported = require('isEventSupported');
// Reasonable defaults
const PIXEL_STEP = 10;
const LINE_HEIGHT = 40;
const PAGE_HEIGHT = 800;
/**
* Mouse wheel (and 2-finger trackpad) support on the web sucks. It is
* complicated, thus this doc is long and (hopefully) detailed enough to answer
* your questions.
*
* If you need to react to the mouse wheel in a predictable way, this code is
* like your bestest friend. * hugs *
*
* As of today, there are 4 DOM event types you can listen to:
*
* 'wheel' -- Chrome(31+), FF(17+), IE(9+)
* 'mousewheel' -- Chrome, IE(6+), Opera, Safari
* 'MozMousePixelScroll' -- FF(3.5 only!) (2010-2013) -- don't bother!
* 'DOMMouseScroll' -- FF(0.9.7+) since 2003
*
* So what to do? The is the best:
*
* normalizeWheel.getEventType();
*
* In your event callback, use this code to get sane interpretation of the
* deltas. This code will return an object with properties:
*
* spinX -- normalized spin speed (use for zoom) - x plane
* spinY -- " - y plane
* pixelX -- normalized distance (to pixels) - x plane
* pixelY -- " - y plane
*
* Wheel values are provided by the browser assuming you are using the wheel to
* scroll a web page by a number of lines or pixels (or pages). Values can vary
* significantly on different platforms and browsers, forgetting that you can
* scroll at different speeds. Some devices (like trackpads) emit more events
* at smaller increments with fine granularity, and some emit massive jumps with
* linear speed or acceleration.
*
* This code does its best to normalize the deltas for you:
*
* - spin is trying to normalize how far the wheel was spun (or trackpad
* dragged). This is super useful for zoom support where you want to
* throw away the chunky scroll steps on the PC and make those equal to
* the slow and smooth tiny steps on the Mac. Key data: This code tries to
* resolve a single slow step on a wheel to 1.
*
* - pixel is normalizing the desired scroll delta in pixel units. You'll
* get the crazy differences between browsers, but at least it'll be in
* pixels!
*
* - positive value indicates scrolling DOWN/RIGHT, negative UP/LEFT. This
* should translate to positive value zooming IN, negative zooming OUT.
* This matches the newer 'wheel' event.
*
* Why are there spinX, spinY (or pixels)?
*
* - spinX is a 2-finger side drag on the trackpad, and a shift + wheel turn
* with a mouse. It results in side-scrolling in the browser by default.
*
* - spinY is what you expect -- it's the classic axis of a mouse wheel.
*
* - I dropped spinZ/pixelZ. It is supported by the DOM 3 'wheel' event and
* probably is by browsers in conjunction with fancy 3D controllers .. but
* you know.
*
* Implementation info:
*
* Examples of 'wheel' event if you scroll slowly (down) by one step with an
* average mouse:
*
* OS X + Chrome (mouse) - 4 pixel delta (wheelDelta -120)
* OS X + Safari (mouse) - N/A pixel delta (wheelDelta -12)
* OS X + Firefox (mouse) - 0.1 line delta (wheelDelta N/A)
* Win8 + Chrome (mouse) - 100 pixel delta (wheelDelta -120)
* Win8 + Firefox (mouse) - 3 line delta (wheelDelta -120)
*
* On the trackpad:
*
* OS X + Chrome (trackpad) - 2 pixel delta (wheelDelta -6)
* OS X + Firefox (trackpad) - 1 pixel delta (wheelDelta N/A)
*
* On other/older browsers.. it's more complicated as there can be multiple and
* also missing delta values.
*
* The 'wheel' event is more standard:
*
* http://www.w3.org/TR/DOM-Level-3-Events/#events-wheelevents
*
* The basics is that it includes a unit, deltaMode (pixels, lines, pages), and
* deltaX, deltaY and deltaZ. Some browsers provide other values to maintain
* backward compatibility with older events. Those other values help us
* better normalize spin speed. Example of what the browsers provide:
*
* | event.wheelDelta | event.detail
* ------------------+------------------+--------------
* Safari v5/OS X | -120 | 0
* Safari v5/Win7 | -120 | 0
* Chrome v17/OS X | -120 | 0
* Chrome v17/Win7 | -120 | 0
* IE9/Win7 | -120 | undefined
* Firefox v4/OS X | undefined | 1
* Firefox v4/Win7 | undefined | 3
*
*/
function normalizeWheel(/*object*/ event) /*object*/ {
var sX = 0, sY = 0, // spinX, spinY
pX = 0, pY = 0; // pixelX, pixelY
// Legacy
if ('detail' in event) { sY = event.detail; }
if ('wheelDelta' in event) { sY = -event.wheelDelta / 120; }
if ('wheelDeltaY' in event) { sY = -event.wheelDeltaY / 120; }
if ('wheelDeltaX' in event) { sX = -event.wheelDeltaX / 120; }
// side scrolling on FF with DOMMouseScroll
if ( 'axis' in event && event.axis === event.HORIZONTAL_AXIS ) {
sX = sY;
sY = 0;
}
pX = sX * PIXEL_STEP;
pY = sY * PIXEL_STEP;
if ('deltaY' in event) { pY = event.deltaY; }
if ('deltaX' in event) { pX = event.deltaX; }
if ((pX || pY) && event.deltaMode) {
if (event.deltaMode == 1) { // delta in LINE units
pX *= LINE_HEIGHT;
pY *= LINE_HEIGHT;
} else { // delta in PAGE units
pX *= PAGE_HEIGHT;
pY *= PAGE_HEIGHT;
}
}
// Fall-back if spin cannot be determined
if (pX && !sX) { sX = (pX < 1) ? -1 : 1; }
if (pY && !sY) { sY = (pY < 1) ? -1 : 1; }
return { spinX : sX,
spinY : sY,
pixelX : pX,
pixelY : pY };
}
self.normalize_wheel = normalizeWheel;
});

View file

@ -811,7 +811,7 @@
:attrs) :attrs)
image-data (get-svg-data :image node) image-data (get-svg-data :image node)
svg-data (or image-data pattern-data)] svg-data (or image-data pattern-data)]
(:xlink:href svg-data))) (or (:href svg-data) (:xlink:href svg-data))))
(defn get-image-fill (defn get-image-fill
[node] [node]

View file

@ -90,7 +90,6 @@
:polyline :polyline
:radialGradient :radialGradient
:rect :rect
:script
:set :set
:stop :stop
:style :style
@ -495,7 +494,6 @@
:marker :marker
:mask :mask
:pattern :pattern
:script
:style :style
:switch :switch
:text :text
@ -963,5 +961,5 @@
(let [redfn (fn [acc {:keys [tag attrs]}] (let [redfn (fn [acc {:keys [tag attrs]}]
(cond-> acc (cond-> acc
(= :image tag) (= :image tag)
(conj (:xlink:href attrs))))] (conj (or (:href attrs) (:xlink:href attrs)))))]
(reduce-nodes redfn [] svg-data ))) (reduce-nodes redfn [] svg-data )))

View file

@ -0,0 +1,72 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Copyright (c) UXBOX Labs SL
*/
"use strict";
goog.provide("app.util.text_position_data");
goog.scope(function () {
const self = app.util.text_position_data;
const document = goog.global.document;
function getRangeRects(node, start, end) {
const range = document.createRange();
range.setStart(node, start);
range.setEnd(node, end);
return [...range.getClientRects()].filter((r) => r.width > 0);
}
self.parse_text_nodes = function(parent, textNode) {
const content = textNode.textContent;
const textSize = content.length;
let from = 0;
let to = 0;
let current = "";
let result = [];
let prevRect = null;
// This variable is to make sure there are not infinite loops
// when we don't advance `to` we store true and then force to
// advance `to` on the next iteration if the condition is true again
let safeguard = false;
while (to < textSize) {
const rects = getRangeRects(textNode, from, to + 1);
if (rects.length > 1 && safeguard) {
from++;
to++;
safeguard = false;
} else if (rects.length > 1) {
const position = prevRect;
result.push({
node: parent,
position: position,
text: current
});
from = to;
current = "";
safeguard = true;
} else {
prevRect = rects[0];
current += content[to];
to = to + 1;
safeguard = false;
}
}
// to == textSize
const rects = getRangeRects(textNode, from, to);
result.push({node: parent, position: rects[0], text: current});
return result;
};
});

View file

@ -11,45 +11,21 @@
[app.common.transit :as transit] [app.common.transit :as transit]
[app.main.store :as st] [app.main.store :as st]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.globals :as global])) [app.util.text-position-data :as tpd]))
(defn get-range-rects
"Retrieve the rectangles that cover the selection given by a `node` adn
the start and end index `start-i`, `end-i`"
[^js node start-i end-i]
(let [^js range (.createRange global/document)]
(.setStart range node start-i)
(.setEnd range node end-i)
(.getClientRects range)))
;; TODO: Evaluate to change this function to Javascript
(defn parse-text-nodes (defn parse-text-nodes
"Given a text node retrieves the rectangles for everyone of its paragraphs and its text." "Given a text node retrieves the rectangles for everyone of its paragraphs and its text."
[parent-node rtl text-node] [parent-node direction text-node]
(let [content (.-textContent text-node) (letfn [(parse-entry [^js entry]
text-size (.-length content)] {:node (.-node entry)
:position (dom/bounding-rect->rect (.-position entry))
(loop [from-i 0 :text (.-text entry)
to-i 0 :direction direction})]
current "" (into
result []] []
(if (>= to-i text-size) (map parse-entry)
(let [rects (get-range-rects text-node from-i to-i) (tpd/parse-text-nodes parent-node text-node))))
entry {:node parent-node
:position (dom/bounding-rect->rect (first rects))
:text current}]
;; We need to add the last element not closed yet
(conj result entry))
(let [rects (get-range-rects text-node from-i (inc to-i))]
;; If the rects increase means we're in a new paragraph
(if (> (.-length rects) 1)
(let [entry {:node parent-node
:position (dom/bounding-rect->rect (if rtl (second rects) (first rects)))
:text current}]
(recur to-i to-i "" (conj result entry)))
(recur from-i (inc to-i) (str current (nth content to-i)) result)))))))
(defn calc-text-node-positions (defn calc-text-node-positions
@ -87,9 +63,9 @@
(->> text-nodes (->> text-nodes
(mapcat (mapcat
(fn [parent-node] (fn [parent-node]
(let [rtl (= "rtl" (.-dir (.-parentElement parent-node)))] (let [direction (.-direction (js/getComputedStyle parent-node))]
(->> (.-childNodes parent-node) (->> (.-childNodes parent-node)
(mapcat #(parse-text-nodes parent-node rtl %)))))) (mapcat #(parse-text-nodes parent-node direction %))))))
(mapv #(update % :position translate-rect)))))) (mapv #(update % :position translate-rect))))))
(defn calc-position-data (defn calc-position-data
@ -100,25 +76,27 @@
(let [text-data (calc-text-node-positions base-node viewport zoom)] (let [text-data (calc-text-node-positions base-node viewport zoom)]
(when (d/not-empty? text-data) (when (d/not-empty? text-data)
(->> text-data (->> text-data
(mapv (fn [{:keys [node position text]}] (mapv (fn [{:keys [node position text direction]}]
(let [{:keys [x y width height]} position (let [{:keys [x y width height]} position
rtl (= "rtl" (.-dir (.-parentElement ^js node)))
styles (js/getComputedStyle ^js node) styles (js/getComputedStyle ^js node)
get (fn [prop] get (fn [prop]
(let [value (.getPropertyValue styles prop)] (let [value (.getPropertyValue styles prop)]
(when (and value (not= value "")) (when (and value (not= value ""))
value)))] value)))]
(d/without-nils (d/without-nils
{:rtl rtl {:x x
:x (if rtl (+ x width) x)
:y (+ y height) :y (+ y height)
:width width :width width
:height height :height height
:direction direction
:font-family (str (get "font-family")) :font-family (str (get "font-family"))
:font-size (str (get "font-size")) :font-size (str (get "font-size"))
:font-weight (str (get "font-weight")) :font-weight (str (get "font-weight"))
:text-transform (str (get "text-transform")) :text-transform (str (get "text-transform"))
:text-decoration (str (get "text-decoration")) :text-decoration (str (get "text-decoration"))
:letter-spacing (str (get "letter-spacing"))
:font-style (str (get "font-style")) :font-style (str (get "font-style"))
:fills (transit/decode-str (get "--fills")) :fills (transit/decode-str (get "--fills"))
:text text})))))))))) :text text}))))))))))

View file

@ -14,6 +14,7 @@
[app.util.http :as http] [app.util.http :as http]
[app.worker.impl :as impl] [app.worker.impl :as impl]
[beicon.core :as rx] [beicon.core :as rx]
[debug :refer [debug?]]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
(defn- handle-response (defn- handle-response
@ -115,6 +116,9 @@
(rx/map render-thumbnail) (rx/map render-thumbnail)
(rx/mapcat persist-thumbnail)))] (rx/mapcat persist-thumbnail)))]
(->> (request-thumbnail file-id revn) (if (debug? :disable-thumbnail-cache)
(rx/catch not-found? on-cache-miss) (->> (request-data-for-thumbnail file-id revn)
(rx/map on-result)))) (rx/map render-thumbnail))
(->> (request-thumbnail file-id revn)
(rx/catch not-found? on-cache-miss)
(rx/map on-result)))))

View file

@ -61,6 +61,9 @@
;; Show text fragments outlines ;; Show text fragments outlines
:text-outline :text-outline
;; Disable thumbnail cache
:disable-thumbnail-cache
}) })
;; These events are excluded when we activate the :events flag ;; These events are excluded when we activate the :events flag

View file

@ -539,9 +539,6 @@ msgstr "التسجيل معطل حاليا."
msgid "errors.terms-privacy-agreement-invalid" msgid "errors.terms-privacy-agreement-invalid"
msgstr "يجب أن تقبل شروط الخدمة وسياسة الخصوصية الخاصة بنا." msgstr "يجب أن تقبل شروط الخدمة وسياسة الخصوصية الخاصة بنا."
#: src/app/main/ui/auth/verify_token.cljs
msgid "errors.token-expired"
msgstr "انتهت صلاحية الرمز"
#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs #: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
msgid "errors.unexpected-error" msgid "errors.unexpected-error"

View file

@ -701,10 +701,6 @@ msgstr ""
"Heu d'acceptar les nostres condicions del servei i la política de " "Heu d'acceptar les nostres condicions del servei i la política de "
"privacitat." "privacitat."
#: src/app/main/ui/auth/verify_token.cljs
msgid "errors.token-expired"
msgstr "El codi ha caducat"
#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs #: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
msgid "errors.unexpected-error" msgid "errors.unexpected-error"
msgstr "S'ha produït un error inesperat." msgstr "S'ha produït un error inesperat."

View file

@ -700,10 +700,6 @@ msgstr ""
"Sie müssen unsere Nutzungsbedingungen und Datenschutzrichtlinien " "Sie müssen unsere Nutzungsbedingungen und Datenschutzrichtlinien "
"akzeptieren." "akzeptieren."
#: src/app/main/ui/auth/verify_token.cljs
msgid "errors.token-expired"
msgstr "Token abgelaufen"
#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs #: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
msgid "errors.unexpected-error" msgid "errors.unexpected-error"
msgstr "Ein unerwarteter Fehler ist aufgetreten." msgstr "Ein unerwarteter Fehler ist aufgetreten."

Some files were not shown because too many files have changed in this diff Show more