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

This commit is contained in:
alonso.torres 2022-06-06 15:23:22 +02:00
commit 9eba666c31
15 changed files with 317 additions and 207 deletions

View file

@ -19,6 +19,17 @@
### :arrow_up: Deps updates ### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!) ### :heart: Community contributions by (Thank you!)
## 1.13.4-beta
### :bug: Bugs fixed
- Fix undo when drawing curves [Taiga #3523](https://tree.taiga.io/project/penpot/issue/3523)
- Fix issue with text edition and certain fonts (WorkSans, Raleway, ...) and foreign objects [Taiga #3521](https://tree.taiga.io/project/penpot/issue/3521)
- Fix thumbnail generation when concurrent edition [Taiga #3522](https://tree.taiga.io/project/penpot/issue/3522)
- Fix environment imporot for exporter in Docker
- Fix auto scroll layers in Firefox [Taiga #3531](https://tree.taiga.io/project/penpot/issue/3531)
- Fix base background not visible for imported SVG
## 1.13.3-beta ## 1.13.3-beta
### :bug: Bugs fixed ### :bug: Bugs fixed

View file

@ -23,7 +23,41 @@
[expound.alpha :as expound] [expound.alpha :as expound]
[fipp.edn :refer [pprint]])) [fipp.edn :refer [pprint]]))
;; ==== Utility functions
(defn reset-file-data
"Hardcode replace of the data of one file."
[system id data]
(db/with-atomic [conn (:app.db/pool system)]
(db/update! conn :file
{:data data}
{:id id})))
(defn get-file
"Get the migrated data of one file."
[system id]
(-> (:app.db/pool system)
(db/get-by-id :file id)
(update :data app.util.blob/decode)
(update :data pmg/migrate-data)))
(defn duplicate-file
"This is a raw version of duplication of file just only for forensic analysis."
[system file-id email]
(db/with-atomic [conn (:app.db/pool system)]
(when-let [profile (some->> (prof/retrieve-profile-data-by-email conn (str/lower email))
(prof/populate-additional-data conn))]
(when-let [file (db/exec-one! conn (sql/select :file {:id file-id}))]
(let [params (assoc file
:id (uuid/next)
:project-id (:default-project-id profile))]
(db/insert! conn :file params)
(:id file))))))
(defn update-file (defn update-file
"Apply a function to the data of one file. Optionally save the changes or not.
The function receives the decoded and migrated file data."
([system id f] (update-file system id f false)) ([system id f] (update-file system id f false))
([system id f save?] ([system id f save?]
(db/with-atomic [conn (:app.db/pool system)] (db/with-atomic [conn (:app.db/pool system)]
@ -40,85 +74,115 @@
{:id (:id file)})) {:id (:id file)}))
(update file :data blob/decode))))) (update file :data blob/decode)))))
(defn reset-file-data (defn analyze-files
[system id data] "Apply a function to all files in the database, reading them in batches. Do not change data.
The function receives an object with some properties of the file and the decoded data, and
an empty atom where it may accumulate statistics, if desired."
[system {:keys [sleep chunk-size max-chunks on-file]
:or {sleep 1000 chunk-size 10 max-chunks ##Inf}}]
(let [stats (atom {})]
(letfn [(retrieve-chunk [conn cursor]
(let [sql (str "select id, name, modified_at, data from file "
" where modified_at < ? and deleted_at is null "
" order by modified_at desc limit ?")]
(->> (db/exec! conn [sql cursor chunk-size])
(map #(update % :data blob/decode)))))
(process-chunk [chunk]
(loop [files chunk]
(when-let [file (first files)]
(on-file file stats)
(recur (rest files)))))]
(db/with-atomic [conn (:app.db/pool system)] (db/with-atomic [conn (:app.db/pool system)]
(db/update! conn :file (loop [cursor (dt/now)
{:data data} chunks 0]
{:id id}))) (when (< chunks max-chunks)
(let [chunk (retrieve-chunk conn cursor)]
(when-not (empty? chunk)
(let [cursor (-> chunk last :modified-at)]
(process-chunk chunk)
(Thread/sleep (inst-ms (dt/duration sleep)))
(recur cursor (inc chunks)))))))
@stats))))
(defn get-file (defn update-pages
[system id] "Apply a function to all pages of one file. The function receives a page and returns an updated page."
(-> (:app.db/pool system) [data f]
(db/get-by-id :file id) (update data :pages-index d/update-vals f))
(update :data app.util.blob/decode)
(update :data pmg/migrate-data)))
(defn duplicate-file (defn update-shapes
"This is a raw version of duplication of file just only for forensic analysis" "Apply a function to all shapes of one page The function receives a shape and returns an updated shape"
[system file-id email] [page f]
(db/with-atomic [conn (:app.db/pool system)] (update page :objects d/update-vals f))
(when-let [profile (some->> (prof/retrieve-profile-data-by-email conn (str/lower email))
(prof/populate-additional-data conn))]
(when-let [file (db/exec-one! conn (sql/select :file {:id file-id}))]
(let [params (assoc file
:id (uuid/next)
:project-id (:default-project-id profile))]
(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." ;; ==== Specific fixes
[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] (defn repair-orphaned-shapes
(some? (:shape-ref object))) "There are some shapes whose parent has been deleted. This
function detects them and puts them as children of the root node."
([file _] ; to be called from analyze-files to search for files with the problem
(repair-orphaned-shapes (:data file)))
(get-parent [object] ([data]
(get (:objects page) (:parent-id object))) (let [is-orphan? (fn [shape objects]
(and (some? (:parent-id shape))
(nil? (get objects (:parent-id shape)))))
(update-object [object] update-page (fn [page]
(if (and (is-nested? object) (let [objects (:objects page)
(not (is-instance? (get-parent object)))) orphans (set (filter #(is-orphan? % objects) (vals objects)))]
(if (seq orphans)
(do (do
(prn "Orphan:" (:name object)) (prn (:id data) "file has" (count orphans) "broken shapes")
(assoc object :component-root? true)) (-> page
object))] (update-shapes (fn [shape]
(if (orphans shape)
(assoc shape :parent-id uuid/zero)
shape)))
(update-in [:objects uuid/zero :shapes]
(fn [shapes] (into shapes (map :id orphans))))))
page)))]
(update page :objects d/update-vals update-object)))] (update-pages data update-page))))
(update data :pages-index d/update-vals update-page)))
(defn repair-idless-components ;; DO NOT DELETE already used scripts, could be taken as templates for easyly writing new ones
"There are some files that contains components with no :id attribute. ;; -------------------------------------------------------------------------------------------
This function detects them and repairs it.
Use it with the update-file function above." ;; (defn repair-orphaned-components
[data] ;; "We have detected some cases of component instances that are not nested, but
(letfn [(update-component [id component] ;; however they have not the :component-root? attribute (so the system considers
(if (nil? (:id component)) ;; them nested). This script fixes this adding them the attribute.
(do ;;
(prn (:id data) "Broken component" (:name component) id) ;; Use it with the update-file function above."
(assoc component :id id)) ;; [data]
component))] ;; (let [update-page
;; (fn [page]
(update data :components #(d/mapm update-component %)))) ;; (prn "================= Page:" (:name page))
;; (letfn [(is-nested? [object]
(defn analyze-idless-components ;; (and (some? (:component-id object))
"Scan all files to check if there are any one with idless components. ;; (nil? (:component-root? object))))
(Does not save the changes, only used to detect affected files)." ;;
[file _] ;; (is-instance? [object]
(repair-idless-components (:data file))) ;; (some? (:shape-ref object)))
;;
;; (get-parent [object]
;; (get (:objects page) (:parent-id object)))
;;
;; (update-object [object]
;; (if (and (is-nested? object)
;; (not (is-instance? (get-parent object))))
;; (do
;; (prn "Orphan:" (:name object))
;; (assoc object :component-root? true))
;; object))]
;;
;; (update page :objects d/update-vals update-object)))]
;;
;; (update data :pages-index d/update-vals update-page)))
;; (defn check-image-shapes ;; (defn check-image-shapes
;; [{:keys [data] :as file} stats] ;; [{:keys [data] :as file} stats]
@ -138,32 +202,3 @@
;; (when @affected? ;; (when @affected?
;; (swap! stats update :affected-files (fnil inc 0))))) ;; (swap! stats update :affected-files (fnil inc 0)))))
(defn analyze-files
[system {:keys [sleep chunk-size max-chunks on-file]
:or {sleep 1000 chunk-size 10 max-chunks ##Inf}}]
(let [stats (atom {})]
(letfn [(retrieve-chunk [conn cursor]
(let [sql (str "select id, name, modified_at, data from file "
" where modified_at < ? and deleted_at is null "
" order by modified_at desc limit ?")]
(->> (db/exec! conn [sql cursor chunk-size])
(map #(update % :data blob/decode)))))
(process-chunk [chunk]
(loop [items chunk]
(when-let [item (first items)]
(on-file item stats)
(recur (rest items)))))]
(db/with-atomic [conn (:app.db/pool system)]
(loop [cursor (dt/now)
chunks 0]
(when (< chunks max-chunks)
(let [chunk (retrieve-chunk conn cursor)]
(when-not (empty? chunk)
(let [cursor (-> chunk last :modified-at)]
(process-chunk chunk)
(Thread/sleep (inst-ms (dt/duration sleep)))
(recur cursor (inc chunks)))))))
@stats))))

View file

@ -44,6 +44,8 @@ services:
penpot-exporter: penpot-exporter:
image: "penpotapp/exporter:latest" image: "penpotapp/exporter:latest"
env_file:
- config.env
environment: environment:
# Don't touch it; this uses internal docker network to # Don't touch it; this uses internal docker network to
# communicate with the frontend. # communicate with the frontend.

View file

@ -26,7 +26,7 @@
:http-server-port 6061 :http-server-port 6061
:http-server-host "localhost" :http-server-host "localhost"
:redis-uri "redis://redis/0" :redis-uri "redis://redis/0"
:exporter-domain-whitelist #{"localhost:3449"}}) :domain-white-list #{"localhost:3449"}})
(s/def ::http-server-port ::us/integer) (s/def ::http-server-port ::us/integer)
(s/def ::http-server-host ::us/string) (s/def ::http-server-host ::us/string)
@ -45,7 +45,7 @@
::http-server-host ::http-server-host
::browser-pool-max ::browser-pool-max
::browser-pool-min ::browser-pool-min
::domain-whitelist])) ::domain-white-list]))
(defn- read-env (defn- read-env
[prefix] [prefix]

View file

@ -3,8 +3,9 @@
Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded) { Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded) {
centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded; centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded;
var parent = this.parentNode, var parent = this.parentNode;
parentComputedStyle = window.getComputedStyle(parent, null), if (parent) {
var parentComputedStyle = window.getComputedStyle(parent, null),
parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')), parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')),
parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')), parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')),
overTop = this.offsetTop - parent.offsetTop < parent.scrollTop, overTop = this.offsetTop - parent.offsetTop < parent.scrollTop,
@ -22,6 +23,7 @@
if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) { if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) {
this.scrollIntoView(alignWithTop); this.scrollIntoView(alignWithTop);
} }
}
}; };
} }
})() })()

View file

@ -124,7 +124,7 @@
(let [edition (get-in state [:workspace-local :edition]) (let [edition (get-in state [:workspace-local :edition])
drawing (get state :workspace-drawing)] drawing (get state :workspace-drawing)]
;; Editors handle their own undo's ;; Editors handle their own undo's
(when-not (or (some? edition) (and (not-empty drawing) (nil? (:object drawing)))) (when (and (nil? edition) (nil? (:object drawing)))
(let [undo (:workspace-undo state) (let [undo (:workspace-undo state)
items (:items undo) items (:items undo)
index (or (:index undo) (dec (count items)))] index (or (:index undo) (dec (count items)))]
@ -420,19 +420,26 @@
(reverse) (reverse)
(into (d/ordered-set))) (into (d/ordered-set)))
find-all-empty-parents (fn recursive-find-empty-parents [empty-parents]
(let [all-ids (into empty-parents ids)
empty-parents-xform empty-parents-xform
(comp (comp
(map (fn [id] (get objects id))) (map (fn [id] (get objects id)))
(map (fn [{:keys [shapes type] :as obj}] (map (fn [{:keys [shapes type] :as obj}]
(when (and (= :group type) (when (and (= :group type)
(zero? (count (remove #(contains? ids %) shapes)))) (zero? (count (remove #(contains? all-ids %) shapes))))
obj))) obj)))
(take-while some?) (take-while some?)
(map :id)) (map :id))
calculated-empty-parents (into #{} empty-parents-xform all-parents)]
(if (= empty-parents calculated-empty-parents)
empty-parents
(recursive-find-empty-parents calculated-empty-parents))))
empty-parents empty-parents
;; Any parent whose children are all deleted, must be deleted too. ;; Any parent whose children are all deleted, must be deleted too.
(into (d/ordered-set) empty-parents-xform all-parents) (into (d/ordered-set) (find-all-empty-parents #{}))
changes (-> (pcb/empty-changes it page-id) changes (-> (pcb/empty-changes it page-id)
(pcb/with-page page) (pcb/with-page page)

View file

@ -189,10 +189,14 @@
(s/def ::file-change-event (s/def ::file-change-event
(s/keys :req-un [::type ::profile-id ::file-id ::session-id ::revn ::changes])) (s/keys :req-un [::type ::profile-id ::file-id ::session-id ::revn ::changes]))
(defn handle-file-change (defn handle-file-change
[{:keys [file-id changes] :as msg}] [{:keys [file-id changes] :as msg}]
(us/assert ::file-change-event msg) (us/assert ::file-change-event msg)
(ptk/reify ::handle-file-change (ptk/reify ::handle-file-change
IDeref
(-deref [_] {:changes changes})
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(let [position-data-operation? (let [position-data-operation?

View file

@ -18,6 +18,10 @@
([state page-id] ([state page-id]
(get-in state [:workspace-data :pages-index page-id]))) (get-in state [:workspace-data :pages-index page-id])))
(defn lookup-data-objects
[data page-id]
(dm/get-in data [:pages-index page-id :objects]))
(defn lookup-page-objects (defn lookup-page-objects
([state] ([state]
(lookup-page-objects state (:current-page-id state))) (lookup-page-objects state (:current-page-id state)))

View file

@ -355,7 +355,7 @@
(assoc :svg-attrs (dissoc attrs :x :y :width :height :href :xlink:href)))))) (assoc :svg-attrs (dissoc attrs :x :y :width :height :href :xlink:href))))))
(defn parse-svg-element [frame-id svg-data element-data unames] (defn parse-svg-element [frame-id svg-data element-data unames]
(let [{:keys [tag attrs]} element-data (let [{:keys [tag attrs hidden]} element-data
attrs (usvg/format-styles attrs) attrs (usvg/format-styles attrs)
element-data (cond-> element-data (map? element-data) (assoc :attrs attrs)) element-data (cond-> element-data (map? element-data) (assoc :attrs attrs))
name (dwc/generate-unique-name unames (or (:id attrs) (tag->name tag))) name (dwc/generate-unique-name unames (or (:id attrs) (tag->name tag)))
@ -402,6 +402,9 @@
(setup-fill) (setup-fill)
(setup-stroke)) (setup-stroke))
shape (cond-> shape
hidden (assoc :hidden true))
children (cond->> (:content element-data) children (cond->> (:content element-data)
(or (= tag :g) (= tag :svg)) (or (= tag :g) (= tag :svg))
(mapv #(usvg/inherit-attributes attrs %)))] (mapv #(usvg/inherit-attributes attrs %)))]
@ -471,6 +474,7 @@
:height (str (:height root-shape)) :height (str (:height root-shape))
:fill "none" :fill "none"
:id "base-background"} :id "base-background"}
:hidden true
:content []} :content []}
svg-data (-> svg-data svg-data (-> svg-data

View file

@ -10,6 +10,7 @@
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.data.workspace.changes :as dch] [app.main.data.workspace.changes :as dch]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.store :as st] [app.main.store :as st]
@ -31,7 +32,9 @@
[object-id] [object-id]
(rx/create (rx/create
(fn [subs] (fn [subs]
(let [node (dom/query (dm/fmt "canvas.thumbnail-canvas[data-object-id='%'" object-id))] ;; We look in the DOM a canvas that 1) matches the id and 2) that it's not empty
;; will be empty on first rendering before drawing the thumbnail and we don't want to store that
(let [node (dom/query (dm/fmt "canvas.thumbnail-canvas[data-object-id='%']:not([data-empty])" object-id))]
(if (some? node) (if (some? node)
(-> node (-> node
(.toBlob (fn [blob] (.toBlob (fn [blob]
@ -43,6 +46,14 @@
(do (rx/push! subs nil) (do (rx/push! subs nil)
(rx/end! subs))))))) (rx/end! subs)))))))
(defn clear-thumbnail
[page-id frame-id]
(ptk/reify ::clear-thumbnail
ptk/UpdateEvent
(update [_ state]
(let [object-id (dm/str page-id frame-id)]
(assoc-in state [:workspace-file :thumbnails object-id] nil)))))
(defn update-thumbnail (defn update-thumbnail
"Updates the thumbnail information for the given frame `id`" "Updates the thumbnail information for the given frame `id`"
[page-id frame-id] [page-id frame-id]
@ -71,50 +82,39 @@
(defn- extract-frame-changes (defn- extract-frame-changes
"Process a changes set in a commit to extract the frames that are changing" "Process a changes set in a commit to extract the frames that are changing"
[[event [old-objects new-objects]]] [[event [old-data new-data]]]
(let [changes (-> event deref :changes) (let [changes (-> event deref :changes)
extract-ids extract-ids
(fn [{type :type :as change}] (fn [{:keys [page-id type] :as change}]
(case type (case type
:add-obj [(:id change)] :add-obj [[page-id (:id change)]]
:mod-obj [(:id change)] :mod-obj [[page-id (:id change)]]
:del-obj [(:id change)] :del-obj [[page-id (:id change)]]
:reg-objects (:shapes change) :mov-objects (->> (:shapes change) (map #(vector page-id %)))
:mov-objects (:shapes change)
[])) []))
get-frame-id get-frame-id
(fn [id] (fn [[page-id id]]
(let [shape (or (get new-objects id) (let [old-objects (wsh/lookup-data-objects old-data page-id)
(get old-objects id))] new-objects (wsh/lookup-data-objects new-data page-id)
(or (and (cph/frame-shape? shape) id) (:frame-id shape))))
;; Extracts the frames and then removes nils and the root frame new-shape (get new-objects id)
xform (comp (mapcat extract-ids) old-shape (get old-objects id)
(map get-frame-id)
(remove nil?)
(filter #(not= uuid/zero %))
(filter #(contains? new-objects %)))]
(into #{} xform changes))) old-frame-id (if (cph/frame-shape? old-shape) id (:frame-id old-shape))
new-frame-id (if (cph/frame-shape? new-shape) id (:frame-id new-shape))]
(defn thumbnail-change? (cond-> #{}
"Checks if a event is only updating thumbnails to ignore in the thumbnail generation process" (and old-frame-id (not= uuid/zero old-frame-id))
[event] (conj [page-id old-frame-id])
(let [changes (-> event deref :changes)
is-thumbnail-op? (and new-frame-id (not= uuid/zero new-frame-id))
(fn [{type :type attr :attr}] (conj [page-id new-frame-id]))))]
(and (= type :set) (into #{}
(= attr :thumbnail))) (comp (mapcat extract-ids)
(mapcat get-frame-id))
is-thumbnail-change? changes)))
(fn [change]
(and (= (:type change) :mod-obj)
(->> change :operations (every? is-thumbnail-op?))))]
(->> changes (every? is-thumbnail-change?))))
(defn watch-state-changes (defn watch-state-changes
"Watch the state for changes inside frames. If a change is detected will force a rendering "Watch the state for changes inside frames. If a change is detected will force a rendering
@ -123,32 +123,39 @@
(ptk/reify ::watch-state-changes (ptk/reify ::watch-state-changes
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ stream] (watch [_ _ stream]
(let [stopper (->> stream (let [stopper
(->> stream
(rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %)) (rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %))
(= ::watch-state-changes (ptk/type %))))) (= ::watch-state-changes (ptk/type %)))))
objects-stream (->> (rx/concat workspace-data-str
(->> (rx/concat
(rx/of nil) (rx/of nil)
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true})) (rx/from-atom refs/workspace-data {:emit-current-value? true}))
;; We need to keep the old-objects so we can check the frame for the ;; We need to keep the old-objects so we can check the frame for the
;; deleted objects ;; deleted objects
(rx/buffer 2 1)) (rx/buffer 2 1))
frame-changes (->> stream change-str
(rx/filter dch/commit-changes?) (->> stream
(rx/filter #(or (dch/commit-changes? %)
(= (ptk/type %) :app.main.data.workspace.notifications/handle-file-change)))
(rx/observe-on :async))
;; Async so we wait for additional side-effects of commit-changes frame-changes-str
(rx/observe-on :async) (->> change-str
(rx/filter (complement thumbnail-change?)) (rx/with-latest-from workspace-data-str)
(rx/with-latest-from objects-stream) (rx/flat-map extract-frame-changes)
(rx/map extract-frame-changes)
(rx/share))] (rx/share))]
(->> frame-changes (->> (rx/merge
(rx/flat-map (->> frame-changes-str
(fn [ids] (rx/filter (fn [[page-id _]] (not= page-id (:current-page-id @st/state))))
(->> (rx/from ids) (rx/map (fn [[page-id frame-id]] (clear-thumbnail page-id frame-id))))
(rx/map #(ptk/data-event ::force-render %)))))
(->> frame-changes-str
(rx/filter (fn [[page-id _]] (= page-id (:current-page-id @st/state))))
(rx/map (fn [[_ frame-id]] (ptk/data-event ::force-render frame-id)))))
(rx/take-until stopper)))))) (rx/take-until stopper))))))
(defn duplicate-thumbnail (defn duplicate-thumbnail

View file

@ -324,7 +324,8 @@
props (obj/merge! #js {} props props (obj/merge! #js {} props
#js {:childs childs #js {:childs childs
:objects objects})] :objects objects})]
[:> group-wrapper props])))) (when (not-empty childs)
[:> group-wrapper props])))))
(defn bool-container-factory (defn bool-container-factory
[objects] [objects]

View file

@ -82,6 +82,7 @@
frame? (= :frame type) frame? (= :frame type)
group? (= :group type) group? (= :group type)
text? (= :text type)
mask? (and group? masked-group?)] mask? (and group? masked-group?)]
(cond (cond
@ -103,6 +104,10 @@
(dom/query-all shape-defs ".svg-def") (dom/query-all shape-defs ".svg-def")
(dom/query-all shape-defs ".svg-mask-wrapper"))) (dom/query-all shape-defs ".svg-mask-wrapper")))
text?
[shape-node
(dom/query shape-node ".text-container")]
:else :else
[shape-node]))) [shape-node])))
@ -185,6 +190,15 @@
(dom/class? node "frame-children") (dom/class? node "frame-children")
(set-transform-att! node "transform" (gmt/inverse transform)) (set-transform-att! node "transform" (gmt/inverse transform))
(dom/class? node "text-container")
(let [modifiers (dissoc modifiers :displacement :rotation)]
(when (not (gsh/empty-modifiers? modifiers))
(let [mtx (-> shape
(assoc :modifiers modifiers)
(gsh/transform-shape)
(gsh/transform-matrix {:no-flip true}))]
(override-transform-att! node "transform" mtx))))
(or (= (dom/get-tag-name node) "mask") (or (= (dom/get-tag-name node) "mask")
(= (dom/get-tag-name node) "filter")) (= (dom/get-tag-name node) "filter"))
(transform-region! node modifiers) (transform-region! node modifiers)

View file

@ -32,6 +32,7 @@
(.clearRect canvas-context 0 0 canvas-width canvas-height) (.clearRect canvas-context 0 0 canvas-width canvas-height)
(.drawImage canvas-context img-node 0 0 canvas-width canvas-height) (.drawImage canvas-context img-node 0 0 canvas-width canvas-height)
(.removeAttribute canvas-node "data-empty")
true)) true))
(catch :default err (catch :default err
(.error js/console err) (.error js/console err)
@ -75,6 +76,8 @@
thumbnail-data-ref (mf/use-memo (mf/deps page-id id) #(refs/thumbnail-frame-data page-id id)) thumbnail-data-ref (mf/use-memo (mf/deps page-id id) #(refs/thumbnail-frame-data page-id id))
thumbnail-data (mf/deref thumbnail-data-ref) thumbnail-data (mf/deref thumbnail-data-ref)
prev-thumbnail-data (hooks/use-previous thumbnail-data)
render-frame? (mf/use-state (not thumbnail-data)) render-frame? (mf/use-state (not thumbnail-data))
on-image-load on-image-load
@ -141,6 +144,12 @@
(.observe observer node #js {:childList true :attributes true :attributeOldValue true :characterData true :subtree true}) (.observe observer node #js {:childList true :attributes true :attributeOldValue true :characterData true :subtree true})
(reset! observer-ref observer)))))] (reset! observer-ref observer)))))]
(mf/use-effect
(mf/deps thumbnail-data)
(fn []
(when (and (some? prev-thumbnail-data) (nil? thumbnail-data))
(rx/push! updates-str :update))))
(mf/use-effect (mf/use-effect
(mf/deps @render-frame? thumbnail-data) (mf/deps @render-frame? thumbnail-data)
(fn [] (fn []
@ -198,8 +207,10 @@
[:foreignObject {:x x :y y :width width :height height} [:foreignObject {:x x :y y :width width :height height}
[:canvas.thumbnail-canvas [:canvas.thumbnail-canvas
{:ref frame-canvas-ref {:key (dm/str "thumbnail-canvas-" (:id shape))
:ref frame-canvas-ref
:data-object-id (dm/str page-id (:id shape)) :data-object-id (dm/str page-id (:id shape))
:data-empty true
:width fixed-width :width fixed-width
:height fixed-height :height fixed-height
;; DEBUG ;; DEBUG

View file

@ -7,9 +7,10 @@
(ns app.main.ui.workspace.shapes.text.editor (ns app.main.ui.workspace.shapes.text.editor
(:require (:require
["draft-js" :as draft] ["draft-js" :as draft]
[app.common.geom.matrix :as gmt] [app.common.data.macros :as dm]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.geom.shapes.text :as gsht]
[app.common.text :as txt] [app.common.text :as txt]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
[app.main.data.workspace.texts :as dwt] [app.main.data.workspace.texts :as dwt]
@ -255,30 +256,37 @@
(-> (gpt/subtract pt box) (-> (gpt/subtract pt box)
(gpt/multiply zoom))))) (gpt/multiply zoom)))))
(mf/defc text-editor-viewport (mf/defc text-editor-svg
{::mf/wrap-props false} {::mf/wrap-props false}
[props] [props]
(let [shape (obj/get props "shape") (let [shape (obj/get props "shape")
viewport-ref (obj/get props "viewport-ref")
zoom (obj/get props "zoom")
position clip-id
(-> (gpt/point (-> shape :selrect :x) (dm/str "text-edition-clip" (:id shape))
(-> shape :selrect :y))
(translate-point-from-viewport (mf/ref-val viewport-ref) zoom))
top-left-corner (gpt/point (/ (:width shape) 2) (/ (:height shape) 2)) text-modifier-ref
(mf/use-memo (mf/deps (:id shape)) #(refs/workspace-text-modifier-by-id (:id shape)))
transform text-modifier
(-> (gmt/matrix) (mf/deref text-modifier-ref)
(gmt/scale (gpt/point zoom))
(gmt/multiply (gsh/transform-matrix shape nil top-left-corner)))]
bounding-box
(gsht/position-data-bounding-box text-modifier)]
[:g.text-editor {:clip-path (dm/fmt "url(#%)" clip-id)
:transform (dm/str (gsh/transform-matrix shape))}
[:defs
[:clipPath {:id clip-id}
[:rect {:x (min (:x bounding-box) (:x shape))
:y (min (:y bounding-box) (:y shape))
:width (max (:width bounding-box) (:width shape))
:height (max (:height bounding-box) (:height shape))
:fill "red"}]]]
[:foreignObject {:x (:x shape) :y (:y shape) :width "100%" :height "100%"
:externalResourcesRequired true}
[:div {:style {:position "absolute" [:div {:style {:position "absolute"
:left (str (:x position) "px") :left 0
:top (str (:y position) "px") :top 0
:pointer-events "all" :pointer-events "all"}}
:transform (str transform) [:& text-shape-edit-html {:shape shape :key (str (:id shape))}]]]]))
:transform-origin "left top"}}
[:& text-shape-edit-html {:shape shape :key (str (:id shape))}]]))

View file

@ -187,10 +187,7 @@
[:div.viewport [:div.viewport
[:div.viewport-overlays {:ref overlays-ref} [:div.viewport-overlays {:ref overlays-ref}
(when show-text-editor?
[:& editor/text-editor-viewport {:shape editing-shape
:viewport-ref viewport-ref
:zoom zoom}])
(when show-comments? (when show-comments?
[:& comments/comments-layer {:vbox vbox [:& comments/comments-layer {:vbox vbox
:vport vport :vport vport
@ -275,6 +272,9 @@
:on-pointer-up on-pointer-up} :on-pointer-up on-pointer-up}
[:g {:style {:pointer-events (if disable-events? "none" "auto")}} [:g {:style {:pointer-events (if disable-events? "none" "auto")}}
(when show-text-editor?
[:& editor/text-editor-svg {:shape editing-shape}])
(when show-outlines? (when show-outlines?
[:& outline/shape-outlines [:& outline/shape-outlines
{:objects base-objects {:objects base-objects