mirror of
https://github.com/penpot/penpot.git
synced 2025-02-12 18:18:24 -05:00
Merge remote-tracking branch 'origin/main' into develop
This commit is contained in:
commit
9eba666c31
15 changed files with 317 additions and 207 deletions
11
CHANGES.md
11
CHANGES.md
|
@ -19,6 +19,17 @@
|
|||
### :arrow_up: Deps updates
|
||||
### :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
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
|
|
@ -23,7 +23,41 @@
|
|||
[expound.alpha :as expound]
|
||||
[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
|
||||
"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 save?]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
|
@ -40,85 +74,115 @@
|
|||
{:id (:id file)}))
|
||||
(update file :data blob/decode)))))
|
||||
|
||||
(defn reset-file-data
|
||||
[system id data]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(db/update! conn :file
|
||||
{:data data}
|
||||
{:id id})))
|
||||
(defn analyze-files
|
||||
"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)))))
|
||||
|
||||
(defn get-file
|
||||
[system id]
|
||||
(-> (:app.db/pool system)
|
||||
(db/get-by-id :file id)
|
||||
(update :data app.util.blob/decode)
|
||||
(update :data pmg/migrate-data)))
|
||||
(process-chunk [chunk]
|
||||
(loop [files chunk]
|
||||
(when-let [file (first files)]
|
||||
(on-file file stats)
|
||||
(recur (rest files)))))]
|
||||
|
||||
(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))))))
|
||||
(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))))
|
||||
|
||||
(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.
|
||||
(defn update-pages
|
||||
"Apply a function to all pages of one file. The function receives a page and returns an updated page."
|
||||
[data f]
|
||||
(update data :pages-index d/update-vals f))
|
||||
|
||||
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))))
|
||||
(defn update-shapes
|
||||
"Apply a function to all shapes of one page The function receives a shape and returns an updated shape"
|
||||
[page f]
|
||||
(update page :objects d/update-vals f))
|
||||
|
||||
(is-instance? [object]
|
||||
(some? (:shape-ref object)))
|
||||
|
||||
(get-parent [object]
|
||||
(get (:objects page) (:parent-id object)))
|
||||
;; ==== Specific fixes
|
||||
|
||||
(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))]
|
||||
(defn repair-orphaned-shapes
|
||||
"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)))
|
||||
|
||||
(update page :objects d/update-vals update-object)))]
|
||||
([data]
|
||||
(let [is-orphan? (fn [shape objects]
|
||||
(and (some? (:parent-id shape))
|
||||
(nil? (get objects (:parent-id shape)))))
|
||||
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
update-page (fn [page]
|
||||
(let [objects (:objects page)
|
||||
orphans (set (filter #(is-orphan? % objects) (vals objects)))]
|
||||
(if (seq orphans)
|
||||
(do
|
||||
(prn (:id data) "file has" (count orphans) "broken shapes")
|
||||
(-> page
|
||||
(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)))]
|
||||
|
||||
(defn repair-idless-components
|
||||
"There are some files that contains components with no :id attribute.
|
||||
This function detects them and repairs it.
|
||||
(update-pages data update-page))))
|
||||
|
||||
Use it with the update-file function above."
|
||||
[data]
|
||||
(letfn [(update-component [id component]
|
||||
(if (nil? (:id component))
|
||||
(do
|
||||
(prn (:id data) "Broken component" (:name component) id)
|
||||
(assoc component :id id))
|
||||
component))]
|
||||
|
||||
(update data :components #(d/mapm update-component %))))
|
||||
;; DO NOT DELETE already used scripts, could be taken as templates for easyly writing new ones
|
||||
;; -------------------------------------------------------------------------------------------
|
||||
|
||||
(defn analyze-idless-components
|
||||
"Scan all files to check if there are any one with idless components.
|
||||
(Does not save the changes, only used to detect affected files)."
|
||||
[file _]
|
||||
(repair-idless-components (:data 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]
|
||||
|
@ -138,32 +202,3 @@
|
|||
;; (when @affected?
|
||||
;; (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))))
|
||||
|
||||
|
|
|
@ -44,6 +44,8 @@ services:
|
|||
|
||||
penpot-exporter:
|
||||
image: "penpotapp/exporter:latest"
|
||||
env_file:
|
||||
- config.env
|
||||
environment:
|
||||
# Don't touch it; this uses internal docker network to
|
||||
# communicate with the frontend.
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
:http-server-port 6061
|
||||
:http-server-host "localhost"
|
||||
: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-host ::us/string)
|
||||
|
@ -45,7 +45,7 @@
|
|||
::http-server-host
|
||||
::browser-pool-max
|
||||
::browser-pool-min
|
||||
::domain-whitelist]))
|
||||
::domain-white-list]))
|
||||
|
||||
(defn- read-env
|
||||
[prefix]
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded) {
|
||||
centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded;
|
||||
|
||||
var parent = this.parentNode,
|
||||
parentComputedStyle = window.getComputedStyle(parent, null),
|
||||
var parent = this.parentNode;
|
||||
if (parent) {
|
||||
var parentComputedStyle = window.getComputedStyle(parent, null),
|
||||
parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')),
|
||||
parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')),
|
||||
overTop = this.offsetTop - parent.offsetTop < parent.scrollTop,
|
||||
|
@ -13,14 +14,15 @@
|
|||
overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth),
|
||||
alignWithTop = overTop && !overBottom;
|
||||
|
||||
if ((overTop || overBottom) && centerIfNeeded) {
|
||||
parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2;
|
||||
}
|
||||
if ((overLeft || overRight) && centerIfNeeded) {
|
||||
parent.scrollLeft = this.offsetLeft - parent.offsetLeft - parent.clientWidth / 2 - parentBorderLeftWidth + this.clientWidth / 2;
|
||||
}
|
||||
if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) {
|
||||
this.scrollIntoView(alignWithTop);
|
||||
if ((overTop || overBottom) && centerIfNeeded) {
|
||||
parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2;
|
||||
}
|
||||
if ((overLeft || overRight) && centerIfNeeded) {
|
||||
parent.scrollLeft = this.offsetLeft - parent.offsetLeft - parent.clientWidth / 2 - parentBorderLeftWidth + this.clientWidth / 2;
|
||||
}
|
||||
if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) {
|
||||
this.scrollIntoView(alignWithTop);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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) (and (not-empty drawing) (nil? (:object drawing))))
|
||||
(when (and (nil? edition) (nil? (:object drawing)))
|
||||
(let [undo (:workspace-undo state)
|
||||
items (:items undo)
|
||||
index (or (:index undo) (dec (count items)))]
|
||||
|
@ -420,19 +420,26 @@
|
|||
(reverse)
|
||||
(into (d/ordered-set)))
|
||||
|
||||
empty-parents-xform
|
||||
(comp
|
||||
(map (fn [id] (get objects id)))
|
||||
(map (fn [{:keys [shapes type] :as obj}]
|
||||
(when (and (= :group type)
|
||||
(zero? (count (remove #(contains? ids %) shapes))))
|
||||
obj)))
|
||||
(take-while some?)
|
||||
(map :id))
|
||||
find-all-empty-parents (fn recursive-find-empty-parents [empty-parents]
|
||||
(let [all-ids (into empty-parents ids)
|
||||
empty-parents-xform
|
||||
(comp
|
||||
(map (fn [id] (get objects id)))
|
||||
(map (fn [{:keys [shapes type] :as obj}]
|
||||
(when (and (= :group type)
|
||||
(zero? (count (remove #(contains? all-ids %) shapes))))
|
||||
obj)))
|
||||
(take-while some?)
|
||||
(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
|
||||
;; 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)
|
||||
(pcb/with-page page)
|
||||
|
@ -448,13 +455,13 @@
|
|||
(pcb/update-shapes (map :id interacting-shapes)
|
||||
(fn [shape]
|
||||
(update shape :interactions
|
||||
(fn [interactions]
|
||||
(when interactions
|
||||
(d/removev #(and (csi/has-destination %)
|
||||
(contains? ids (:destination %)))
|
||||
interactions))))))
|
||||
(fn [interactions]
|
||||
(when interactions
|
||||
(d/removev #(and (csi/has-destination %)
|
||||
(contains? ids (:destination %)))
|
||||
interactions))))))
|
||||
(cond->
|
||||
(seq starting-flows)
|
||||
(seq starting-flows)
|
||||
(pcb/update-page-option :flows (fn [flows]
|
||||
(reduce #(csp/remove-flow %1 (:id %2))
|
||||
flows
|
||||
|
|
|
@ -189,10 +189,14 @@
|
|||
(s/def ::file-change-event
|
||||
(s/keys :req-un [::type ::profile-id ::file-id ::session-id ::revn ::changes]))
|
||||
|
||||
|
||||
(defn handle-file-change
|
||||
[{:keys [file-id changes] :as msg}]
|
||||
(us/assert ::file-change-event msg)
|
||||
(ptk/reify ::handle-file-change
|
||||
IDeref
|
||||
(-deref [_] {:changes changes})
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [position-data-operation?
|
||||
|
|
|
@ -18,6 +18,10 @@
|
|||
([state 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
|
||||
([state]
|
||||
(lookup-page-objects state (:current-page-id state)))
|
||||
|
|
|
@ -355,7 +355,7 @@
|
|||
(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
|
||||
(let [{:keys [tag attrs hidden]} element-data
|
||||
attrs (usvg/format-styles attrs)
|
||||
element-data (cond-> element-data (map? element-data) (assoc :attrs attrs))
|
||||
name (dwc/generate-unique-name unames (or (:id attrs) (tag->name tag)))
|
||||
|
@ -402,6 +402,9 @@
|
|||
(setup-fill)
|
||||
(setup-stroke))
|
||||
|
||||
shape (cond-> shape
|
||||
hidden (assoc :hidden true))
|
||||
|
||||
children (cond->> (:content element-data)
|
||||
(or (= tag :g) (= tag :svg))
|
||||
(mapv #(usvg/inherit-attributes attrs %)))]
|
||||
|
@ -471,6 +474,7 @@
|
|||
:height (str (:height root-shape))
|
||||
:fill "none"
|
||||
:id "base-background"}
|
||||
:hidden true
|
||||
:content []}
|
||||
|
||||
svg-data (-> svg-data
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
[app.common.pages.helpers :as cph]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
|
@ -31,7 +32,9 @@
|
|||
[object-id]
|
||||
(rx/create
|
||||
(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)
|
||||
(-> node
|
||||
(.toBlob (fn [blob]
|
||||
|
@ -43,6 +46,14 @@
|
|||
(do (rx/push! subs nil)
|
||||
(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
|
||||
"Updates the thumbnail information for the given frame `id`"
|
||||
[page-id frame-id]
|
||||
|
@ -71,50 +82,39 @@
|
|||
|
||||
(defn- extract-frame-changes
|
||||
"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)
|
||||
|
||||
extract-ids
|
||||
(fn [{type :type :as change}]
|
||||
(fn [{:keys [page-id type] :as change}]
|
||||
(case type
|
||||
:add-obj [(:id change)]
|
||||
:mod-obj [(:id change)]
|
||||
:del-obj [(:id change)]
|
||||
:reg-objects (:shapes change)
|
||||
:mov-objects (:shapes change)
|
||||
:add-obj [[page-id (:id change)]]
|
||||
:mod-obj [[page-id (:id change)]]
|
||||
:del-obj [[page-id (:id change)]]
|
||||
:mov-objects (->> (:shapes change) (map #(vector page-id %)))
|
||||
[]))
|
||||
|
||||
get-frame-id
|
||||
(fn [id]
|
||||
(let [shape (or (get new-objects id)
|
||||
(get old-objects id))]
|
||||
(or (and (cph/frame-shape? shape) id) (:frame-id shape))))
|
||||
(fn [[page-id id]]
|
||||
(let [old-objects (wsh/lookup-data-objects old-data page-id)
|
||||
new-objects (wsh/lookup-data-objects new-data page-id)
|
||||
|
||||
;; Extracts the frames and then removes nils and the root frame
|
||||
xform (comp (mapcat extract-ids)
|
||||
(map get-frame-id)
|
||||
(remove nil?)
|
||||
(filter #(not= uuid/zero %))
|
||||
(filter #(contains? new-objects %)))]
|
||||
new-shape (get new-objects id)
|
||||
old-shape (get old-objects id)
|
||||
|
||||
(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?
|
||||
"Checks if a event is only updating thumbnails to ignore in the thumbnail generation process"
|
||||
[event]
|
||||
(let [changes (-> event deref :changes)
|
||||
(cond-> #{}
|
||||
(and old-frame-id (not= uuid/zero old-frame-id))
|
||||
(conj [page-id old-frame-id])
|
||||
|
||||
is-thumbnail-op?
|
||||
(fn [{type :type attr :attr}]
|
||||
(and (= type :set)
|
||||
(= attr :thumbnail)))
|
||||
|
||||
is-thumbnail-change?
|
||||
(fn [change]
|
||||
(and (= (:type change) :mod-obj)
|
||||
(->> change :operations (every? is-thumbnail-op?))))]
|
||||
|
||||
(->> changes (every? is-thumbnail-change?))))
|
||||
(and new-frame-id (not= uuid/zero new-frame-id))
|
||||
(conj [page-id new-frame-id]))))]
|
||||
(into #{}
|
||||
(comp (mapcat extract-ids)
|
||||
(mapcat get-frame-id))
|
||||
changes)))
|
||||
|
||||
(defn watch-state-changes
|
||||
"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/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(let [stopper (->> stream
|
||||
(rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %))
|
||||
(= ::watch-state-changes (ptk/type %)))))
|
||||
(let [stopper
|
||||
(->> stream
|
||||
(rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %))
|
||||
(= ::watch-state-changes (ptk/type %)))))
|
||||
|
||||
objects-stream (->> (rx/concat
|
||||
(rx/of nil)
|
||||
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true}))
|
||||
;; We need to keep the old-objects so we can check the frame for the
|
||||
;; deleted objects
|
||||
(rx/buffer 2 1))
|
||||
workspace-data-str
|
||||
(->> (rx/concat
|
||||
(rx/of nil)
|
||||
(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
|
||||
;; deleted objects
|
||||
(rx/buffer 2 1))
|
||||
|
||||
frame-changes (->> stream
|
||||
(rx/filter dch/commit-changes?)
|
||||
change-str
|
||||
(->> 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
|
||||
(rx/observe-on :async)
|
||||
(rx/filter (complement thumbnail-change?))
|
||||
(rx/with-latest-from objects-stream)
|
||||
(rx/map extract-frame-changes)
|
||||
(rx/share))]
|
||||
frame-changes-str
|
||||
(->> change-str
|
||||
(rx/with-latest-from workspace-data-str)
|
||||
(rx/flat-map extract-frame-changes)
|
||||
(rx/share))]
|
||||
|
||||
(->> frame-changes
|
||||
(rx/flat-map
|
||||
(fn [ids]
|
||||
(->> (rx/from ids)
|
||||
(rx/map #(ptk/data-event ::force-render %)))))
|
||||
(->> (rx/merge
|
||||
(->> frame-changes-str
|
||||
(rx/filter (fn [[page-id _]] (not= page-id (:current-page-id @st/state))))
|
||||
(rx/map (fn [[page-id frame-id]] (clear-thumbnail page-id frame-id))))
|
||||
|
||||
(->> 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))))))
|
||||
|
||||
(defn duplicate-thumbnail
|
||||
|
|
|
@ -324,7 +324,8 @@
|
|||
props (obj/merge! #js {} props
|
||||
#js {:childs childs
|
||||
:objects objects})]
|
||||
[:> group-wrapper props]))))
|
||||
(when (not-empty childs)
|
||||
[:> group-wrapper props])))))
|
||||
|
||||
(defn bool-container-factory
|
||||
[objects]
|
||||
|
|
|
@ -82,6 +82,7 @@
|
|||
|
||||
frame? (= :frame type)
|
||||
group? (= :group type)
|
||||
text? (= :text type)
|
||||
mask? (and group? masked-group?)]
|
||||
|
||||
(cond
|
||||
|
@ -103,6 +104,10 @@
|
|||
(dom/query-all shape-defs ".svg-def")
|
||||
(dom/query-all shape-defs ".svg-mask-wrapper")))
|
||||
|
||||
text?
|
||||
[shape-node
|
||||
(dom/query shape-node ".text-container")]
|
||||
|
||||
:else
|
||||
[shape-node])))
|
||||
|
||||
|
@ -185,6 +190,15 @@
|
|||
(dom/class? node "frame-children")
|
||||
(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")
|
||||
(= (dom/get-tag-name node) "filter"))
|
||||
(transform-region! node modifiers)
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
|
||||
(.clearRect canvas-context 0 0 canvas-width canvas-height)
|
||||
(.drawImage canvas-context img-node 0 0 canvas-width canvas-height)
|
||||
(.removeAttribute canvas-node "data-empty")
|
||||
true))
|
||||
(catch :default 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 (mf/deref thumbnail-data-ref)
|
||||
|
||||
prev-thumbnail-data (hooks/use-previous thumbnail-data)
|
||||
|
||||
render-frame? (mf/use-state (not thumbnail-data))
|
||||
|
||||
on-image-load
|
||||
|
@ -141,6 +144,12 @@
|
|||
(.observe observer node #js {:childList true :attributes true :attributeOldValue true :characterData true :subtree true})
|
||||
(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/deps @render-frame? thumbnail-data)
|
||||
(fn []
|
||||
|
@ -198,8 +207,10 @@
|
|||
|
||||
[:foreignObject {:x x :y y :width width :height height}
|
||||
[: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-empty true
|
||||
:width fixed-width
|
||||
:height fixed-height
|
||||
;; DEBUG
|
||||
|
|
|
@ -7,9 +7,10 @@
|
|||
(ns app.main.ui.workspace.shapes.text.editor
|
||||
(:require
|
||||
["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.shapes :as gsh]
|
||||
[app.common.geom.shapes.text :as gsht]
|
||||
[app.common.text :as txt]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
|
@ -255,30 +256,37 @@
|
|||
(-> (gpt/subtract pt box)
|
||||
(gpt/multiply zoom)))))
|
||||
|
||||
(mf/defc text-editor-viewport
|
||||
(mf/defc text-editor-svg
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (obj/get props "shape")
|
||||
viewport-ref (obj/get props "viewport-ref")
|
||||
zoom (obj/get props "zoom")
|
||||
|
||||
position
|
||||
(-> (gpt/point (-> shape :selrect :x)
|
||||
(-> shape :selrect :y))
|
||||
(translate-point-from-viewport (mf/ref-val viewport-ref) zoom))
|
||||
clip-id
|
||||
(dm/str "text-edition-clip" (:id shape))
|
||||
|
||||
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
|
||||
(-> (gmt/matrix)
|
||||
(gmt/scale (gpt/point zoom))
|
||||
(gmt/multiply (gsh/transform-matrix shape nil top-left-corner)))]
|
||||
text-modifier
|
||||
(mf/deref text-modifier-ref)
|
||||
|
||||
[:div {:style {:position "absolute"
|
||||
:left (str (:x position) "px")
|
||||
:top (str (:y position) "px")
|
||||
:pointer-events "all"
|
||||
:transform (str transform)
|
||||
:transform-origin "left top"}}
|
||||
bounding-box
|
||||
(gsht/position-data-bounding-box text-modifier)]
|
||||
|
||||
[:& text-shape-edit-html {:shape shape :key (str (:id shape))}]]))
|
||||
[: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"
|
||||
:left 0
|
||||
:top 0
|
||||
:pointer-events "all"}}
|
||||
[:& text-shape-edit-html {:shape shape :key (str (:id shape))}]]]]))
|
||||
|
|
|
@ -187,10 +187,7 @@
|
|||
|
||||
[:div.viewport
|
||||
[: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?
|
||||
[:& comments/comments-layer {:vbox vbox
|
||||
:vport vport
|
||||
|
@ -275,6 +272,9 @@
|
|||
:on-pointer-up on-pointer-up}
|
||||
|
||||
[:g {:style {:pointer-events (if disable-events? "none" "auto")}}
|
||||
(when show-text-editor?
|
||||
[:& editor/text-editor-svg {:shape editing-shape}])
|
||||
|
||||
(when show-outlines?
|
||||
[:& outline/shape-outlines
|
||||
{:objects base-objects
|
||||
|
|
Loading…
Add table
Reference in a new issue