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
- 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)
- 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)
- 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)

View file

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

View file

@ -223,6 +223,9 @@
{:name "0071-add-file-object-thumbnail-table"
: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 = ?;")
(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/keys :req-un [::profile-id ::file-id ::object-id ::data]))

View file

@ -12,7 +12,7 @@
[app.loggers.audit :as audit]
[app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile]
[app.util.services :as sv]
[app.util.services :as sv]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
@ -114,15 +114,25 @@
{:is-active true}
{:id member-id}))
(assoc member :is-active true)
;; Delete the invitation
(db/delete! conn :team-invitation
{:team-id team-id :email-to (str/lower member-email)})))
(defmethod process-token :team-invitation
[cfg {:keys [profile-id token]} {:keys [member-id] :as 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
;; This happens when token is filled with member-id and current
;; user is already logged in with exactly invited account.

View file

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

View file

@ -67,6 +67,38 @@
(db/insert! conn :file params)
(: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
;; [{:keys [data] :as file} stats]
;; (println "=> analizing file:" (:name file) (:id file))

View file

@ -12,6 +12,7 @@
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.common.pages.helpers :as cph]
[app.common.pages.migrations :as pmg]
[app.db :as db]
[app.util.blob :as blob]
@ -125,10 +126,14 @@
{:columns [:object-id]})
(into #{} (map :object-id)))
using (->> (concat (vals (:pages-index data))
(vals (:components data)))
(into #{} (comp (map :objects)
(mapcat keys))))
get-objects-ids
(fn [{:keys [id objects]}]
(->> (cph/get-frames objects)
(map #(str id (:id %)))))
using (into #{}
(mapcat get-objects-ids)
(vals (:pages-index data)))
unused (set/difference stored using)]
@ -136,7 +141,7 @@
(let [sql (str/concat
"delete from file_object_thumbnail "
" 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))))))
(defn- clean-file-thumbnails!

View file

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

View file

@ -5,7 +5,49 @@
;; Copyright (c) UXBOX Labs SL
(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.
;; For each attribute, if the value is the same in all shapes,
@ -36,13 +78,11 @@
;; :rx nil
;; :ry nil}
;;
(defn get-attrs-multi
([objs attrs]
(get-attrs-multi objs attrs = identity))
(get-attrs-multi objs attrs default-equal identity))
([objs attrs eqfn sel]
(loop [attr (first attrs)
attrs (rest attrs)
result (transient {})]
@ -50,34 +90,25 @@
(let [value
(loop [curr (first objs)
objs (rest objs)
value ::undefined]
value ::unset]
(if (and curr (not= value :multiple))
;;
(let [new-val (get curr attr ::undefined)
(let [new-val (get-attr curr attr)
value (cond
(= new-val ::undefined) value
(= new-val :multiple) :multiple
(= value ::undefined) (sel new-val)
(eqfn new-val value) value
:else :multiple)]
(= new-val ::unset) value
(= new-val :multiple) :multiple
(= value ::unset) (sel new-val)
(eqfn new-val value) value
:else :multiple)]
(recur (first objs) (rest objs) value))
;;
value))]
(recur (first attrs)
(rest attrs)
(cond-> result
(not= value ::undefined)
(not= value ::unset)
(assoc! attr value))))
(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]
(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
[f]
(fn [rf]
@ -574,17 +584,20 @@
(assert (string? basename))
(assert (set? used))
(let [[prefix initial] (extract-numeric-suffix basename)]
(if (and (not prefix-first?)
(not (contains? used basename)))
basename
(loop [counter initial]
(let [candidate (if (and (= 1 counter) prefix-first?)
(str prefix)
(str prefix "-" counter))]
(if (contains? used candidate)
(recur (inc counter))
candidate)))))))
(if (> (count basename) 1000)
;; We skip generating names for long strings. If the name is too long the regex can hang
basename
(let [[prefix initial] (extract-numeric-suffix basename)]
(if (and (not prefix-first?)
(not (contains? used basename)))
basename
(loop [counter initial]
(let [candidate (if (and (= 1 counter) prefix-first?)
(str prefix)
(str prefix "-" counter))]
(if (contains? used candidate)
(recur (inc counter))
candidate))))))))
(defn deep-mapm
"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)
f' (/ (- (* b e) (* a f)) det)]
(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))
(Point. (/ x ox) (/ y oy)))
(defn min
([] (min nil nil))
([p1] (min p1 nil))
@ -139,6 +138,15 @@
(mth/sqrt (+ (mth/pow dx 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
[{x :x y :y :as p}]
(assert (point? p))

View file

@ -6,6 +6,7 @@
(ns app.common.geom.shapes
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.bool :as gsb]
@ -38,6 +39,14 @@
;; --- 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?
"Checks if one rect is fully inside the other"
[rect other]
@ -96,6 +105,37 @@
(mth/sqrt (* 2 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
(dm/export gco/center-shape)

View file

@ -152,45 +152,74 @@
:top
: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
[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-not ignore-constraints
(:constraints-v child (default-constraints-v child))
:scale)
(if (and (nil? (:resize-vector modifiers))
(nil? (:resize-vector-2 modifiers)))
;; If we don't have a resize modifier we return the same modifiers
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)
modifiers-v (constraint-modifier (constraints-v const->type+axis) :y parent child modifiers transformed-parent-rect)]
constraints-v
(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
;; 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))
modifiers-h (constraint-modifier (constraints-h const->type+axis) :x parent child modifiers transformed-parent-rect)
modifiers-v (constraint-modifier (constraints-v const->type+axis) :y parent child modifiers transformed-parent-rect)]
:always
(gmt/translate-matrix)))
;; Build final child modifiers. Apply transform again to the result, to get the
;; 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)
(assoc :resize-origin (:resize-origin modifiers-h)
:resize-vector (gpt/point (get-in modifiers-h [:resize-vector :x] 1)
(get-in modifiers-h [:resize-vector :y] 1)))
:always
(gmt/translate-matrix)))
(:resize-vector modifiers-v)
(assoc :resize-origin-2 (:resize-origin modifiers-v)
:resize-vector-2 (gpt/point (get-in modifiers-v [:resize-vector :x] 1)
(get-in modifiers-v [:resize-vector :y] 1)))
(:resize-vector modifiers-h)
(assoc :resize-origin (:resize-origin modifiers-h)
:resize-vector (gpt/point (get-in modifiers-h [:resize-vector :x] 1)
(get-in modifiers-h [:resize-vector :y] 1)))
(:resize-transform modifiers)
(assoc :resize-transform (:resize-transform modifiers)
:resize-transform-inverse (:resize-transform-inverse modifiers)))))
(:resize-vector modifiers-v)
(assoc :resize-origin-2 (:resize-origin modifiers-v)
: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]
(gpr/points->selrect (position-data-points shape)))

View file

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

View file

@ -11,6 +11,7 @@
[app.common.exceptions :as ex]
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.bool :as gshb]
[app.common.math :as mth]
[app.common.pages.common :refer [component-sync-attrs]]
[app.common.pages.helpers :as cph]
[app.common.pages.init :as init]
@ -433,25 +434,35 @@
(defmethod process-operation :set
[shape op]
(let [attr (:attr op)
val (:val op)
ignore (:ignore-touched op)
(let [attr (:attr op)
group (get component-sync-attrs attr)
val (:val op)
shape-val (get shape attr)
ignore (:ignore-touched op)
ignore-geometry (:ignore-geometry op)
shape-ref (:shape-ref shape)
group (get component-sync-attrs attr)
root-name? (and (= group :name-group)
(:component-root? shape))]
is-geometry? (and (or (= group :geometry-group)
(and (= group :content-group) (= (:type shape) :path)))
(not (#{:width :height} attr))) ;; :content in paths are also considered geometric
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
;; Depending on the origin of the attribute change, we need or not 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
;; 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 (and ignore-geometry
(and (= group :geometry-group)
(not (#{:width :height} attr))))))
(not (and ignore-geometry is-geometry?)))
(->
(update :touched cph/set-touched-group group)
(dissoc :remote-synced?))

View file

@ -25,6 +25,7 @@
:content :content-group
:hidden :visibility-group
:blocked :modifiable-group
:grow-type :text-font-group
:font-family :text-font-group
:font-size :text-font-group
:font-style :text-font-group
@ -58,8 +59,10 @@
:y :geometry-group
:width :geometry-group
:height :geometry-group
:rotation :geometry-group
:transform :geometry-group
:transform-inverse :geometry-group
:position-data :geometry-group
:opacity :layer-effects-group
:blend-mode :layer-effects-group
:shadow :shadow-group
@ -78,6 +81,7 @@
:rx :ry
:r1 :r2 :r3 :r4
:selrect
:points
:opacity
:blend-mode
@ -111,6 +115,7 @@
:x :y
:rotation
:selrect
:points
:constraints-h
:constraints-v
@ -136,6 +141,7 @@
:rx :ry
:r1 :r2 :r3 :r4
:selrect
:points
:constraints-h
:constraints-v
@ -178,6 +184,7 @@
:x :y
:rotation
:selrect
:points
:constraints-h
:constraints-v
@ -220,6 +227,7 @@
:x :y
:rotation
:selrect
:points
:constraints-h
:constraints-v
@ -262,6 +270,7 @@
:x :y
:rotation
:selrect
:points
:constraints-h
:constraints-v
@ -329,6 +338,7 @@
:rx :ry
:r1 :r2 :r3 :r4
:selrect
:points
:constraints-h
:constraints-v
@ -354,6 +364,7 @@
:rx :ry
:r1 :r2 :r3 :r4
:selrect
:points
:constraints-h
:constraints-v
@ -398,6 +409,7 @@
:rx :ry
:r1 :r2 :r3 :r4
:selrect
:points
:constraints-h
: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;
box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25);
z-index: 12;
top: 40px;
top: 30px;
left: 6px;
width: 155px;

View file

@ -144,7 +144,7 @@
.color-bullet {
width: 24px;
height: 24px;
border-radius: $br-small;
border-radius: 50%;
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 {
fill: $color-gray-40;
font-size: $fs12;

View file

@ -277,7 +277,10 @@
page (get-in state [:workspace-data :pages-index page-id])
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)
(pcb/add-page id page))]
@ -1428,7 +1431,12 @@
wrapper (gsh/selection-rect selected-objs)
orig-pos (gpt/point (:x1 wrapper) (:y1 wrapper))]
(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)
frame-object (get page-objects frame-id)

View file

@ -45,7 +45,7 @@
ptk/WatchEvent
(watch [it 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)
changes (reduce

View file

@ -124,7 +124,7 @@
(let [edition (get-in state [:workspace-local :edition])
drawing (get state :workspace-drawing)]
;; 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)
items (:items undo)
index (or (:index undo) (dec (count items)))]

View file

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

View file

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

View file

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

View file

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

View file

@ -23,9 +23,22 @@
(us/verify ::spec/content new-content)
(let [shape-id (:id shape)
[old-points old-selrect]
(helpers/content->points+selrect shape old-content)
[new-points new-selrect]
(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)
(pcb/with-objects objects))]

View file

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

View file

@ -8,7 +8,9 @@
(:require
[app.common.data :as d]
[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
([state]
@ -50,6 +52,10 @@
(filter selectable?)
selected)))))
(defn lookup-selected-raw
[state]
(dm/get-in state [:workspace-local :selected]))
(defn lookup-selected
([state]
(lookup-selected state nil))
@ -94,3 +100,26 @@
(-> (:workspace-libraries state)
(assoc id {:id id
: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
(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])
rect (->> (select-keys attrs [:x :y :width :height])
@ -352,7 +352,7 @@
(merge rect-metadata)
(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]
(let [{:keys [tag attrs]} element-data
@ -414,7 +414,6 @@
new-shape (dwc/make-new-shape shape objects selected)
changes (-> changes
(pcb/with-objects objects)
(pcb/add-object new-shape)
(pcb/change-parent parent-id [new-shape] index))
@ -464,9 +463,25 @@
root-shape (create-svg-root frame-id svg-data)
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
new-shape (dwc/make-new-shape root-shape objects selected)
changes (-> (pcb/empty-changes it page-id)
(pcb/with-objects objects)
(pcb/add-object new-shape))
root-attrs (-> (:attrs svg-data)
@ -478,7 +493,6 @@
[unames changes]
(d/enumerate (->> (:content svg-data)
(mapv #(usvg/inherit-attributes root-attrs %)))))
changes (pcb/resize-parents changes
(->> changes
:redo-changes

View file

@ -187,8 +187,8 @@
update-fn
(fn [shape]
(if (some? (:content shape))
(update-text-content shape txt/is-root-node? attrs/merge attrs)
(assoc shape :content (attrs/merge {:type "root"} attrs))))
(update-text-content shape txt/is-root-node? d/txt-merge attrs)
(assoc shape :content (d/txt-merge {:type "root"} attrs))))
shape-ids (cond (cph/text-shape? shape) [id]
(cph/group-shape? shape) (cph/get-children-ids objects id))]
@ -240,18 +240,19 @@
shape-ids (cond
(cph/text-shape? shape) [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
[node]
(let [color-attrs (select-keys node [:fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient])]
(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))
(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
[content]
@ -374,6 +375,15 @@
(update [_ state]
(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
[id]
(ptk/reify ::remove-text-modifier

View file

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

View file

@ -252,15 +252,23 @@
shape-delta
(when root
(gpt/point (- (:x shape) (:x root))
(- (:y shape) (:y root))))
(gpt/point (- (gsh/left-bound shape) (gsh/left-bound root))
(- (gsh/top-bound shape) (gsh/top-bound root))))
transformed-shape-delta
(when transformed-root
(gpt/point (- (:x transformed-shape) (:x transformed-root))
(- (:y transformed-shape) (:y transformed-root))))
(gpt/point (- (gsh/left-bound transformed-shape) (gsh/left-bound 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?]))
@ -356,22 +364,27 @@
[modif-tree shape modifiers]
(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)
set-child
(fn [modif-tree child]
(let [child-modifiers (gsh/calc-child-modifiers shape child modifiers ignore-constraints transformed-rect)]
(fn [snap-pixel? modif-tree child]
(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
(not (gsh/empty-modifiers? child-modifiers))
(set-modifiers-rec child child-modifiers))))
modif-tree
(-> modif-tree
(assoc-in [(:id shape) :modifiers] modifiers))]
(assoc-in [(:id shape) :modifiers] modifiers))
(reduce set-child modif-tree children)))]
(set-modifiers-rec modif-tree shape modifiers)))
resize-modif?
(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
"Retrieves a map with the flag `ignore-geometry?` given a tree of modifiers"

View file

@ -9,9 +9,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.shapes :as gsh]
[app.common.pages.helpers :as cph]
[app.common.path.commands :as upc]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.store :as st]
[okulary.core :as l]))
@ -193,28 +191,29 @@
(assoc :pages (:pages data)))))
st/state =))
(def workspace-data
(l/derived :workspace-data st/state))
(def workspace-file-colors
(l/derived (fn [state]
(when-let [file (:workspace-data state)]
(->> (:colors file)
(d/mapm #(assoc %2 :file-id (:id file))))))
st/state))
(l/derived (fn [data]
(when data
(->> (:colors data)
(d/mapm #(assoc %2 :file-id (:id data))))))
workspace-data
=))
(def workspace-recent-colors
(l/derived (fn [state]
(dm/get-in state [:workspace-data :recent-colors] []))
st/state))
(l/derived (fn [data]
(get data :recent-colors []))
workspace-data))
(def workspace-recent-fonts
(l/derived (fn [state]
(dm/get-in state [:workspace-data :recent-fonts] []))
st/state))
(l/derived (fn [data]
(get data :workspace-data []))
workspace-data))
(def workspace-file-typography
(l/derived (fn [state]
(when-let [file (:workspace-data state)]
(:typographies file)))
st/state))
(l/derived :typographies workspace-data))
(def workspace-project
(l/derived :workspace-project st/state))
@ -313,24 +312,8 @@
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]
(let [selector
(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 =)))
(l/derived (partial wsh/select-bool-children id) st/state =))
(def selected-data
(l/derived #(let [selected (wsh/lookup-selected %)
@ -399,11 +382,14 @@
(l/derived #(dm/get-in % [:workspace-file :thumbnails] {}) st/state))
(defn thumbnail-frame-data
[frame-id]
(l/derived #(get % frame-id) thumbnail-data))
[page-id frame-id]
(l/derived
(fn [thumbnails]
(get thumbnails (dm/str page-id frame-id)))
thumbnail-data))
(def workspace-text-modifier
(l/derived :workspace-text-modifier st/state))
(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
[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)
(update :x - (:horizontal padding))
(update :y - (:vertical padding))
@ -402,7 +402,7 @@
:style {:-webkit-print-color-adjust :exact}
:fill "none"}
(let [fonts (ff/frame->fonts object objects)]
(let [fonts (ff/shape->fonts object objects)]
[:& ff/fontfaces-style {:fonts fonts}])
(case (:type object)

View file

@ -11,6 +11,7 @@
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.main.ui.static :as static]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
@ -59,7 +60,8 @@
(mf/defc verify-token
[{: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
(fn []
(dom/set-html-title (tr "title.default"))
@ -69,13 +71,10 @@
(handle-token tdata))
(fn [{:keys [type code] :as error}]
(cond
(and (= :validation type)
(or (= :validation type)
(= :invalid-token code)
(= :token-expired (:reason error)))
(let [msg (tr "errors.token-expired")]
(ts/schedule 100 #(st/emit! (dm/error msg)))
(st/emit! (rt/nav :auth-login)))
(reset! bad-token true)
(= :email-already-exists code)
(let [msg (tr "errors.email-already-exists")]
(ts/schedule 100 #(st/emit! (dm/error msg)))
@ -91,5 +90,10 @@
(ts/schedule 100 #(st/emit! (dm/error msg)))
(st/emit! (rt/nav :auth-login)))))))))
[:div.verify-token
i/loader-pencil]))
(if @bad-token
[:> 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.uuid :as uuid]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.numeric-input :refer [numeric-input]]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.keyboard :as kbd]
@ -141,13 +142,19 @@
[:div.editable-select {:class class
:ref on-node-load}
[: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}]
(if (= type "number")
[:> numeric-input {:value (or (some-> @state :current-value value->label) "")
:on-change set-value
:on-focus handle-focus
:on-blur handle-blur
:placeholder placeholder}]
[: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]
[:& dropdown {:show (get @state :is-open? false)

View file

@ -79,6 +79,7 @@
on-focus #(reset! focus? true)
on-change (fn [event]
(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)))
on-blur

View file

@ -13,5 +13,5 @@
class (str "icon-" (name id))]
`(rumext.alpha/html
[: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-tool (icon-xref :ruler-tool))
(def search (icon-xref :search))
(def set-thumbnail (icon-xref :set-thumbnail))
(def shape-halign-center (icon-xref :shape-halign-center))
(def shape-halign-left (icon-xref :shape-halign-left))
(def shape-halign-right (icon-xref :shape-halign-right))

View file

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

View file

@ -24,7 +24,7 @@
[:h2 "What's new?"]]
[:span.release "Beta version " version]
[: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."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]]]
@ -41,8 +41,8 @@
[:div.modal-title
[:h2 "Multiple exports"]]
[:div.modal-content
[:p "Speed 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 "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 keep designing in the meantime ;)"]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
[:& c/navigation-bullets
@ -61,7 +61,7 @@
[:h2 "Multiple fills and strokes"]]
[:div.modal-content
[: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
[:button.btn-secondary {:on-click next} "Continue"]
[:& c/navigation-bullets
@ -80,7 +80,7 @@
[:h2 "Members area redesign"]]
[:div.modal-content
[: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
[:button.btn-secondary {:on-click next} "Continue"]
[:& c/navigation-bullets
@ -98,8 +98,8 @@
[:div.modal-title
[:h2 "Focus mode"]]
[: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 "This option is also useful to improve the performance in cases where the page has a large number of elements."]]
[:p "Enjoy a distraction-less design mode by selecting the elements of a page that matter to you and temporarily hiding the rest."]
[:p "As a side effect, this can give you a performance boost in massive designs."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click finish} "Start!"]
[:& c/navigation-bullets

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -19,6 +19,31 @@
[app.util.object :as obj]
[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/forward-ref true
::mf/wrap-props false}
@ -56,7 +81,13 @@
wrapper-props
(cond-> wrapper-props
(= :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}
[:> :g wrapper-props

View file

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

View file

@ -64,21 +64,27 @@
(mf/deps fonts-css)
#(fonts/extract-fontface-urls fonts-css))
;; Calculate the data-uris for these fonts
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
style (replace-embeds fonts-css fonts-urls fonts-embed)]
(when (d/not-empty? style)
[:style style])))
[:style {:data-loading loading?} style])))
(defn frame->fonts
[frame objects]
(->> (cph/get-children objects (:id frame))
(filter cph/text-shape?)
(map (comp fonts/get-content-fonts :content))
(reduce set/union #{})))
(defn shape->fonts
[shape objects]
(let [initial (cond-> #{}
(cph/text-shape? shape)
(into (fonts/get-content-fonts (:content shape))))]
(->> (cph/get-children objects (:id shape))
(filter cph/text-shape?)
(map (comp fonts/get-content-fonts :content))
(reduce set/union initial))))
(defn shapes->fonts
[shapes]

View file

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

View file

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

View file

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

View file

@ -27,7 +27,7 @@
:circle [:layout :fill :stroke :shadow :blur :svg]
:path [: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
[{:keys [page-id file-id shapes frame]}]

View file

@ -58,7 +58,7 @@
(for [shape shapes]
(if (seq (: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}])
[:& fill-block {:key (str "fill-block-" (:id shape))
[:& fill-block {:key (str "fill-block-only" (:id shape))
:shape shape}]))])))

View file

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

View file

@ -84,7 +84,7 @@
(for [shape shapes]
(if (seq (: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}])
[:& stroke-block {:key (str "stroke-color-" (:id shape))
[:& stroke-block {:key (str "stroke-color-only" (:id shape))
:shape shape}]))])))

View file

@ -101,12 +101,13 @@
[:div.attributes-content-row
[:pre.attributes-content (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 (shape->color style)
:copy-data (copy-style-data style :fill-color :fill-color-gradient)
:on-change-format #(reset! color-format %)}])
[:& color-row {:format @color-format
:color (shape->color fill)
:copy-data (copy-style-data fill :fill-color :fill-color-gradient)
:on-change-format #(reset! color-format %)}]))
(when (:font-id style)
[:div.attributes-unit-row
@ -186,4 +187,5 @@
[:div.attributes-block-title-text (tr "handoff.attributes.typography")]]
(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
:key (: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
:key (:id item)}]))]))

View file

@ -8,8 +8,11 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.uuid :as uuid]
[app.main.data.workspace.thumbnails :as dwt]
[app.main.fonts :as fonts]
[app.main.refs :as refs]
[app.main.ui.context :as ctx]
[app.main.ui.hooks :as hooks]
[app.main.ui.shapes.embed :as embed]
[app.main.ui.shapes.frame :as frame]
@ -25,19 +28,17 @@
[shape-wrapper]
(let [frame-shape (frame/frame-shape shape-wrapper)]
(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/forward-ref true}
[props ref]
(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 (mf/deref childs-ref)]
[:& (mf/provider embed/context) {:value true}
[:& shape-container {:shape shape :ref ref}
[:& ff/fontfaces-style {:fonts fonts}]
[:& frame-shape {:shape shape :childs childs} ]]]))))
(defn check-props
@ -60,16 +61,20 @@
thumbnail? (unchecked-get props "thumbnail?")
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))
force-render (mf/use-state false)
;; Thumbnail data
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? (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
node-ref (mf/use-var nil)
@ -84,13 +89,20 @@
disable-thumbnail? (d/not-empty? (dm/get-in modifiers [(:id shape) :modifiers]))
[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
(fns/use-node-store thumbnail? node-ref rendered?)]
(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
(fn []
;; When a change in the data is received a "force-render" event is emited
@ -113,11 +125,15 @@
@node-ref)
(when (not @rendered?) (reset! rendered? true)))))
[:g.frame-container {:key "frame-container" :ref on-frame-load}
thumb-renderer
[:& (mf/provider ctx/render-ctx) {:value render-id}
[:g.frame-container {:id (dm/str "frame-container-" (:id shape))
:key "frame-container"
:ref on-frame-load}
[:& 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
[:> frame/frame-thumbnail {:key (dm/str (:id shape))
:shape (cond-> shape
(some? thumbnail-data)
(assoc :thumbnail thumbnail-data))}]]]))))
thumb-renderer]]]))))

View file

@ -7,10 +7,223 @@
(ns app.main.ui.workspace.shapes.frame.dynamic-modifiers
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.main.ui.workspace.viewport.utils :as utils]
[app.util.dom :as dom]
[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
[objects node modifiers]
@ -20,7 +233,13 @@
(fn []
(when (some? 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)))
modifiers))))
@ -42,13 +261,13 @@
is-cur-val? (d/not-empty? modifiers)]
(when (and (not is-prev-val?) is-cur-val?)
(utils/start-transform! node shapes))
(start-transform! node shapes))
(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?))
(utils/remove-transform! node @prev-shapes))
(remove-transform! node @prev-shapes))
(reset! prev-modifiers modifiers)
(reset! prev-transforms transforms)

View file

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

View file

@ -6,6 +6,7 @@
(ns app.main.ui.workspace.shapes.frame.thumbnail-render
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.math :as mth]
[app.main.data.workspace :as dw]
@ -14,26 +15,59 @@
[app.util.dom :as dom]
[app.util.object :as obj]
[app.util.timers :as ts]
[beicon.core :as rx]
[cuerdas.core :as str]
[rumext.alpha :as mf]))
;; (def thumbnail-scale-factor 2)
(defn- draw-thumbnail-canvas
[canvas-node img-node]
(let [canvas-context (.getContext canvas-node "2d")
canvas-width (.-width canvas-node)
canvas-height (.-height canvas-node)]
(.clearRect canvas-context 0 0 canvas-width canvas-height)
(.drawImage canvas-context img-node 0 0 canvas-width canvas-height)
(.toDataURL canvas-node "image/jpeg" 0.8)))
(try
(when (and (some? canvas-node) (some? img-node))
(let [canvas-context (.getContext canvas-node "2d")
canvas-width (.-width canvas-node)
canvas-height (.-height canvas-node)]
;; TODO: Expermient with different scale factors
;; (set! (.-width canvas-node) (* thumbnail-scale-factor canvas-width))
;; (set! (.-height canvas-node) (* thumbnail-scale-factor canvas-height))
;; (.setTransform canvas-context thumbnail-scale-factor 0 0 thumbnail-scale-factor 0 0)
;; (set! (.-imageSmoothingEnabled canvas-context) true)
;; (set! (.-imageSmoothingQuality canvas-context) "high")
(.clearRect canvas-context 0 0 canvas-width canvas-height)
(.drawImage canvas-context img-node 0 0 canvas-width canvas-height)
(.toDataURL canvas-node "image/png" 1.0)))
(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
"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)
frame-image-ref (mf/use-ref nil)
disable-ref? (mf/use-var disable?)
regenerate-thumbnail (mf/use-var false)
fixed-width (mth/clamp (:width shape) 250 2000)
fixed-height (/ (* (:height shape) fixed-width) (:width shape))
@ -42,55 +76,91 @@
shape-ref (hooks/use-update-var shape)
thumbnail-ref? (mf/use-var thumbnail?)
updates-str (mf/use-memo #(rx/subject))
on-image-load
(mf/use-callback
(fn []
(let [canvas-node (mf/ref-val frame-canvas-ref)
img-node (mf/ref-val frame-image-ref)
thumb-data (draw-thumbnail-canvas canvas-node img-node)]
(st/emit! (dw/update-thumbnail id thumb-data))
(reset! image-url nil))))
(ts/raf
#(let [canvas-node (mf/ref-val frame-canvas-ref)
img-node (mf/ref-val frame-image-ref)
thumb-data (draw-thumbnail-canvas canvas-node img-node)]
(when (some? thumb-data)
(st/emit! (dw/update-thumbnail page-id id thumb-data))
(reset! image-url nil))))))
on-change
generate-thumbnail
(mf/use-callback
(fn []
(when (and (some? @node-ref) (not @disable-ref?))
(let [node @node-ref]
(ts/schedule-on-idle
#(let [frame-html (dom/node->xml node)
{:keys [x y width height]} @shape-ref
svg-node
(-> (dom/make-node "http://www.w3.org/2000/svg" "svg")
(dom/set-property! "version" "1.1")
(dom/set-property! "viewBox" (dm/str x " " y " " width " " height))
(dom/set-property! "width" width)
(dom/set-property! "height" height)
(dom/set-property! "fill" "none")
(obj/set! "innerHTML" frame-html))
img-src (-> svg-node dom/node->xml dom/svg->data-uri)]
(reset! image-url img-src)))))))
(let [node @node-ref
frame-html (dom/node->xml node)
{:keys [x y width height]} @shape-ref
style-node (dom/query (dm/str "#frame-container-" (:id shape) " style"))
style-str (or (-> style-node dom/node->xml) "")
svg-node
(-> (dom/make-node "http://www.w3.org/2000/svg" "svg")
(dom/set-property! "version" "1.1")
(dom/set-property! "viewBox" (dm/str x " " y " " width " " height))
(dom/set-property! "width" width)
(dom/set-property! "height" height)
(dom/set-property! "fill" "none")
(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
(mf/use-callback
(fn [node]
(when (and (some? node) (nil? @observer-ref))
(on-change [])
(let [observer (js/MutationObserver. on-change)]
(.observe observer node #js {:childList true :attributes true :characterData true :subtree true})
(when-not (some? @thumbnail-data-ref)
(rx/push! updates-str :update))
(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)))))]
(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/deps disable?)
(fn []
(reset! disable-ref? disable?)))
(mf/use-effect
(mf/deps thumbnail?)
(fn []
(reset! thumbnail-ref? thumbnail?)))
(mf/use-effect
(fn []
#(when (and (some? @node-ref) @rendered?)
@ -104,7 +174,7 @@
[on-load-frame-dom
(when (some? @image-url)
(mf/html
[:g.thumbnail-rendering {:opacity 0}
[:g.thumbnail-rendering
[:foreignObject {:x x :y y :width width :height height}
[:canvas {:ref frame-canvas-ref
:width fixed-width
@ -113,7 +183,7 @@
[:image {:ref frame-image-ref
:x (:x shape)
:y (:y shape)
:xlinkHref @image-url
:href @image-url
:width (:width shape)
:height (:height shape)
:on-load on-image-load}]]))]))

View file

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

View file

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

View file

@ -152,7 +152,10 @@
(let [old-state (mf/ref-val prev-value)]
(if (and (some? state) (some? old-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-add-styles (get-blocks-to-add-styles block-changes)]
(-> state

View file

@ -6,9 +6,9 @@
(ns app.main.ui.workspace.shapes.text.viewport-texts
(:require
[app.common.attrs :as attrs]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.pages.helpers :as cph]
[app.common.text :as txt]
@ -25,6 +25,24 @@
[app.util.timers :as ts]
[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
"Updates the shape with the current state in the editor"
[shape editor-state]
@ -37,7 +55,7 @@
(cond-> shape
(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
[{:keys [grow-type id]} node]
@ -53,11 +71,12 @@
;; Update the position-data of every text fragment
(let [position-data (utp/calc-position-data node)]
(st/emit! (dwt/update-position-data id position-data))))
(st/emit! (dwt/update-position-data id position-data)))
(st/emit! (dwt/clean-text-modifier id)))
(defn- update-text-modifier
[{:keys [grow-type id]} node]
(let [position-data (utp/calc-position-data node)
props {:position-data position-data}
@ -74,30 +93,18 @@
(st/emit! (dwt/update-text-modifier id props))))
(mf/defc text-container
{::mf/wrap-props false}
{::mf/wrap-props false
::mf/wrap [mf/memo]}
[props]
(let [shape (obj/get props "shape")
on-update (obj/get props "on-update")
watch-edits (obj/get props "watch-edits")
handle-update
(mf/use-callback
(mf/deps shape on-update)
(fn [node]
(when (some? 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))]
(on-update shape node))))]
[:& fo/text-shape {:key (str "shape-" (:id shape))
:ref handle-update
@ -109,36 +116,39 @@
::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]}
[props]
(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)
;; A change in position-data won't be a "real" change
text-change?
(fn [id]
(let [old-shape (get prev-text-shapes id)
new-shape (get text-shapes id)]
(and (not (identical? old-shape new-shape))
(not= old-shape new-shape))))
new-shape (get text-shapes id)
old-modifiers (-> (get prev-modifiers id) strip-modifier)
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
(mf/use-memo
(mf/deps text-shapes)
(mf/deps text-shapes modifiers)
#(->> (keys text-shapes)
(filter text-change?)
(map (d/getf text-shapes))))
handle-update-modifier (mf/use-callback update-text-modifier)
handle-update-shape (mf/use-callback update-text-shape)]
[:*
(for [{:keys [id] :as shape} changed-texts]
[:& text-container {:shape shape
:on-update handle-update-shape
[:& text-container {:shape (gsh/transform-shape shape)
:on-update (if (some? (get modifiers (:id shape)))
handle-update-modifier
handle-update-shape)
: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/wrap-props false}
[props]
@ -150,10 +160,29 @@
(-> (mf/deref refs/workspace-editor-state)
(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
(some? 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)]
(mf/use-effect
@ -162,28 +191,34 @@
#(st/emit! (dwt/remove-text-modifier (:id shape)))))
[:& text-container {:shape shape
:watch-edits true
:on-update handle-update-shape}]))
(defn check-props
[new-props old-props]
(and (identical? (unchecked-get new-props "objects") (unchecked-get old-props "objects"))
(= (unchecked-get new-props "edition") (unchecked-get old-props "edition"))))
(and (identical? (unchecked-get new-props "objects")
(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/wrap-props false
::mf/wrap [#(mf/memo' % check-props)]}
[props]
(let [objects (obj/get props "objects")
edition (obj/get props "edition")
xf-texts (comp (filter (comp cph/text-shape? second))
(map strip-position-data))
(let [objects (obj/get props "objects")
edition (obj/get props "edition")
modifiers (obj/get props "modifiers")
text-shapes
(mf/use-memo
(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)]
@ -198,4 +233,6 @@
[:*
(when 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.icons :as i]
[app.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry]]
[app.util.color :as uc]
[app.util.dom :as dom]
[app.util.dom.dnd :as dnd]
[app.util.i18n :as i18n :refer [tr]]
@ -1089,10 +1090,21 @@
;; TODO: looks like the first argument is not necessary
apply-color
(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)
(st/emit! (dc/change-stroke ids color 0))
(st/emit! (dc/change-fill ids color 0)))))
(st/emit! (dc/change-stroke ids (merge uc/empty-color color) 0))
(st/emit! (dc/change-fill ids (merge uc/empty-color color) 0)))))
rename-color
(fn [name]

View file

@ -32,10 +32,9 @@
(l/derived (l/in [:workspace-local :shape-for-rename]) st/state))
(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 {})
shape-for-rename (mf/deref shape-for-rename-ref)
name-ref (mf/use-ref)
start-edit (fn []
(on-start-edit)
@ -96,7 +95,8 @@
container? (or (cph/frame-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
(mf/deps id)
@ -129,6 +129,7 @@
select-shape
(fn [event]
(dom/prevent-default event)
(reset! scroll-to-middle? false)
(let [id (:id item)]
(cond
(kbd/shift? event)
@ -177,19 +178,26 @@
:detect-center? container?
:data {:id (:id item)
:index index
:name (:name item)})]
:name (:name item)})
ref (mf/use-ref)]
(mf/use-effect
(mf/deps selected? selected)
(fn []
(let [single? (= (count selected) 1)
node (mf/ref-val dref)
node (mf/ref-val ref)
subid
(when (and single? selected?)
(ts/schedule
100
#(dom/scroll-into-view! node #js {:block "center", :behavior "smooth"})))]
(let [scroll-to @scroll-to-middle?]
(ts/schedule
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)
(rx/dispose! subid)))))
@ -211,6 +219,7 @@
:on-double-click #(dom/stop-propagation %)}
[:& si/element-icon {:shape item}]
[:& layer-name {:shape item
:name-ref ref
:on-start-edit #(reset! disable-drag true)
:on-stop-edit #(reset! disable-drag false)}]
@ -368,7 +377,10 @@
search-and-filters
(fn [[id shape]]
(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
(= uuid/zero id)
(and
@ -431,8 +443,17 @@
[:div.active-filters
(for [f (:active-filters @filter-state)]
[:span {:on-click (remove-filter f)}
(tr f) i/cross])]
(let [name (case f
: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)
[:div.filters-container

View file

@ -25,10 +25,12 @@
[:proportion-lock
:width :height
:x :y
:ox :oy
:rotation
:rx :ry
:r1 :r2 :r3 :r4
:selrect])
:selrect
:points])
(def ^:private type->options
{:bool #{:size :position :rotation}
@ -46,7 +48,7 @@
;; -- User/drawing coords
(mf/defc measures-menu
[{:keys [ids ids-with-children values type all-types shape] :as props}]
(let [options (if (= type :multiple)
(reduce #(union %1 %2) (map #(get type->options %) all-types))
(get type->options type))
@ -58,21 +60,37 @@
[shape])
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 $
(map gsh/transform-shape $)
(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
(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)]
(cond-> values
(not= (:width values) :multiple) (assoc :width width)
(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
(not= (:rotation values) :multiple) (assoc :rotation rotation)))
@ -85,7 +103,6 @@
radius-multi? (mf/use-state nil)
radius-input-ref (mf/use-ref nil)
on-preset-selected
(fn [width height]
(st/emit! (udw/update-dimensions ids :width width)
@ -279,7 +296,6 @@
{:no-validate true
:min 0
:max 359
:default 0
:data-wrap true
:placeholder "--"
:on-click select-all

View file

@ -34,7 +34,7 @@
(mf/defc stroke-menu
{::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
:multiple (tr "workspace.options.selection-stroke")
:group (tr "workspace.options.group-stroke")
@ -191,4 +191,5 @@
:on-reorder (handle-reorder index)
:disable-drag disable-drag
: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)
on-change-ref (mf/use-ref nil)
name-ref (mf/use-ref (:name typography))
on-name-blur
(mf/use-callback
(mf/deps on-change)
(fn [event]
(let [content (dom/get-target-val event)]
(when-not (str/blank? content)
(let [[path name] (cph/parse-path-name content)]
(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))))]
(let [name (dom/get-target-val event)]
(when-not (str/blank? name)
(on-change {:name name })))))]
(mf/use-effect
(mf/deps editing?)
@ -498,16 +490,6 @@
(fn []
(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
{:class (when selected? "selected")
@ -582,8 +564,7 @@
{:type "text"
:ref name-input-ref
:default-value (cph/merge-path-item (:path typography) (:name typography))
:on-blur on-name-blur
:on-change on-name-change}]
:on-blur on-name-blur}]
[:div.element-set-actions-button
{:on-click #(reset! open? false)}

View file

@ -47,7 +47,7 @@
(second))))
(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
:top 0
:left 0})
@ -110,14 +110,15 @@
[:option {:value ":inner"} (tr "workspace.options.stroke.inner")]
[:option {:value ":outer"} (tr "workspace.options.stroke.outer")]]
[:select#style.input-select {:value (enum->string (:stroke-style stroke))
:on-change (on-stroke-style-change index)}
(when (= (:stroke-style stroke) :multiple)
[:option {:value ""} "--"])
[:option {:value ":solid"} (tr "workspace.options.stroke.solid")]
[:option {:value ":dotted"} (tr "workspace.options.stroke.dotted")]
[:option {:value ":dashed"} (tr "workspace.options.stroke.dashed")]
[:option {:value ":mixed"} (tr "workspace.options.stroke.mixed")]]]
(when-not disable-stroke-style
[:select#style.input-select {:value (enum->string (:stroke-style stroke))
:on-change (on-stroke-style-change index)}
(when (= (:stroke-style stroke) :multiple)
[:option {:value ""} "--"])
[:option {:value ":solid"} (tr "workspace.options.stroke.solid")]
[:option {:value ":dotted"} (tr "workspace.options.stroke.dotted")]
[:option {:value ":dashed"} (tr "workspace.options.stroke.dashed")]
[:option {:value ":mixed"} (tr "workspace.options.stroke.mixed")]])]
;; Stroke Caps
(when show-caps

View file

@ -241,6 +241,8 @@
type :multiple
all-types (into #{} (map :type shapes))
has-text? (contains? all-types :text)
[measure-ids measure-values] (get-attrs shapes objects :measure)
[layer-ids layer-values
@ -280,7 +282,8 @@
[:& fill-menu {:type type :ids fill-ids :values fill-values}])
(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)
[:& color-selection-menu {:type type :shapes (vals objects-no-measures)}])

View file

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

View file

@ -90,7 +90,7 @@
hover (mf/use-state nil)
hover-disabled? (mf/use-state false)
frame-hover (mf/use-state nil)
active-frames (mf/use-state {})
active-frames (mf/use-state #{})
;; REFS
viewport-ref (mf/use-ref nil)
@ -168,7 +168,7 @@
show-snap-points? (and (or (contains? layout :dynamic-alignment)
(contains? layout :snap-grid))
(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-artboard-names? (contains? layout :display-artboard-names)
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-viewport-modifiers modifiers base-objects)
(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-overlays {:ref overlays-ref}
@ -246,6 +246,7 @@
[:& stv/viewport-texts {:key (dm/str "texts-" page-id)
:page-id page-id
:objects base-objects
:modifiers modifiers
:edition edition}]]]
[:svg.viewport-controls
@ -257,6 +258,7 @@
:ref viewport-ref
:class (when drawing-tool "drawing")
:style {:cursor @cursor}
:fill "none"
:on-click on-click
:on-context-menu on-context-menu
@ -297,7 +299,8 @@
(when show-text-editor?
[:& text-edition-outline
{:shape (get base-objects edition)}])
{:shape (get base-objects edition)
:zoom zoom}])
(when show-measures?
[:& msr/measurement

View file

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

View file

@ -214,37 +214,65 @@
(defn inside-vbox [vbox 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
[objects vbox hover active-frames zoom]
[objects hover-ids selected active-frames zoom transform vbox]
(mf/use-effect
(mf/deps vbox)
(let [frame? #(= :frame (get-in objects [% :type]))
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 []
(swap! active-frames
(fn [active-frames]
(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))))))
active-selection (when (and (not= transform :move) (= (count selected-frames) 1)) (first selected-frames))
hover-frame (last @hover-ids)
last-hover-frame (mf/use-var nil)]
(mf/use-effect
(mf/deps @hover @active-frames zoom)
(fn []
(let [frame-id (if (= :frame (:type @hover))
(:id @hover)
(:frame-id @hover))]
(if (< zoom 0.25)
(when (some? @active-frames)
(reset! active-frames nil))
(when (and (some? frame-id)(not (contains? @active-frames frame-id)))
(reset! active-frames {frame-id true})))))))
(mf/use-effect
(mf/deps hover-frame)
(fn []
(when (some? hover-frame)
(reset! last-hover-frame hover-frame))))
(mf/use-effect
(mf/deps objects @hover-ids selected zoom transform vbox)
(fn []
;; 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
;; this shortcuts outside the viewport?

View file

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

View file

@ -8,224 +8,10 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.main.ui.cursors :as cur]
[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]
(dm/str (:x vbox 0) " "
(:y vbox 0) " "

View file

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

View file

@ -114,7 +114,7 @@
(rx/map (fn [objects]
(let [objects (render/adapt-objects-for-shape objects object-id)]
{:objects objects
:object object-id}))))))
:object (get objects object-id)}))))))
{:keys [objects object]} (use-resource fetch-state)]
@ -264,7 +264,7 @@
:embed embed}
(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
[app.common.data :as d]
[app.common.text :as txt]
[app.main.ui.formats :as fmt]
[app.util.color :as uc]
[cuerdas.core :as str]))
@ -108,7 +109,7 @@
(every? #(or (nil? %) (= % 0)) value)
(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]
(let [css-prop (or (prop to-prop) (name prop))
format-fn (or (prop format) default-format)

View file

@ -345,8 +345,9 @@
(defn node->xml
[node]
(-> (js/XMLSerializer.)
(.serializeToString node)))
(when (some? node)
(-> (js/XMLSerializer.)
(.serializeToString node))))
(defn svg->data-uri
[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)
image-data (get-svg-data :image node)
svg-data (or image-data pattern-data)]
(:xlink:href svg-data)))
(or (:href svg-data) (:xlink:href svg-data))))
(defn get-image-fill
[node]

View file

@ -90,7 +90,6 @@
:polyline
:radialGradient
:rect
:script
:set
:stop
:style
@ -495,7 +494,6 @@
:marker
:mask
:pattern
:script
:style
:switch
:text
@ -963,5 +961,5 @@
(let [redfn (fn [acc {:keys [tag attrs]}]
(cond-> acc
(= :image tag)
(conj (:xlink:href attrs))))]
(conj (or (:href attrs) (:xlink:href attrs)))))]
(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.main.store :as st]
[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
"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)
text-size (.-length content)]
(loop [from-i 0
to-i 0
current ""
result []]
(if (>= to-i text-size)
(let [rects (get-range-rects text-node from-i to-i)
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)))))))
(letfn [(parse-entry [^js entry]
{:node (.-node entry)
:position (dom/bounding-rect->rect (.-position entry))
:text (.-text entry)
:direction direction})]
(into
[]
(map parse-entry)
(tpd/parse-text-nodes parent-node text-node))))
(defn calc-text-node-positions
@ -87,9 +63,9 @@
(->> text-nodes
(mapcat
(fn [parent-node]
(let [rtl (= "rtl" (.-dir (.-parentElement parent-node)))]
(let [direction (.-direction (js/getComputedStyle parent-node))]
(->> (.-childNodes parent-node)
(mapcat #(parse-text-nodes parent-node rtl %))))))
(mapcat #(parse-text-nodes parent-node direction %))))))
(mapv #(update % :position translate-rect))))))
(defn calc-position-data
@ -100,25 +76,27 @@
(let [text-data (calc-text-node-positions base-node viewport zoom)]
(when (d/not-empty? text-data)
(->> text-data
(mapv (fn [{:keys [node position text]}]
(mapv (fn [{:keys [node position text direction]}]
(let [{:keys [x y width height]} position
rtl (= "rtl" (.-dir (.-parentElement ^js node)))
styles (js/getComputedStyle ^js node)
get (fn [prop]
(let [value (.getPropertyValue styles prop)]
(when (and value (not= value ""))
value)))]
(d/without-nils
{:rtl rtl
:x (if rtl (+ x width) x)
{:x x
:y (+ y height)
:width width
:height height
:direction direction
:font-family (str (get "font-family"))
:font-size (str (get "font-size"))
:font-weight (str (get "font-weight"))
:text-transform (str (get "text-transform"))
:text-decoration (str (get "text-decoration"))
:letter-spacing (str (get "letter-spacing"))
:font-style (str (get "font-style"))
:fills (transit/decode-str (get "--fills"))
:text text}))))))))))

View file

@ -14,6 +14,7 @@
[app.util.http :as http]
[app.worker.impl :as impl]
[beicon.core :as rx]
[debug :refer [debug?]]
[rumext.alpha :as mf]))
(defn- handle-response
@ -115,6 +116,9 @@
(rx/map render-thumbnail)
(rx/mapcat persist-thumbnail)))]
(->> (request-thumbnail file-id revn)
(rx/catch not-found? on-cache-miss)
(rx/map on-result))))
(if (debug? :disable-thumbnail-cache)
(->> (request-data-for-thumbnail file-id revn)
(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
:text-outline
;; Disable thumbnail cache
:disable-thumbnail-cache
})
;; These events are excluded when we activate the :events flag

View file

@ -539,9 +539,6 @@ msgstr "التسجيل معطل حاليا."
msgid "errors.terms-privacy-agreement-invalid"
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
msgid "errors.unexpected-error"

View file

@ -701,10 +701,6 @@ msgstr ""
"Heu d'acceptar les nostres condicions del servei i la política de "
"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
msgid "errors.unexpected-error"
msgstr "S'ha produït un error inesperat."

View file

@ -700,10 +700,6 @@ msgstr ""
"Sie müssen unsere Nutzungsbedingungen und Datenschutzrichtlinien "
"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
msgid "errors.unexpected-error"
msgstr "Ein unerwarteter Fehler ist aufgetreten."

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