Compare commits

...

25 Commits

Author SHA1 Message Date
alonso.torres
1764d965c1 📚 Upgrade to version 1.13.4-beta 2022-06-06 15:22:23 +02:00
Alejandro Alonso
a120630a7f 🐛 Fix environment import for exporter at docker 2022-06-06 13:23:40 +02:00
Alejandro
f33ad5e8fa Merge pull request #1972 from penpot/hiru-fix-orphaned-shapes
 Add script to fix broken objects
2022-06-06 13:18:07 +02:00
Andrés Moya
f04859f8a6 Add script to fix broken objects 2022-06-06 12:56:37 +02:00
Alejandro Alonso
31aed2aaa4 🐛 Fix base background not visible for imported svg 2022-06-06 12:34:19 +02:00
Alejandro
18109b2387 Merge pull request #1976 from penpot/hiru-fix-scrollintoview
🐛 Fix auto scroll layers panel in firefox
2022-06-06 11:05:35 +02:00
Andrés Moya
a0cc8a06b6 🐛 Fix auto scroll layers panel in firefox 2022-06-06 10:21:32 +02:00
Pablo Alba
462ec0c12a Merge pull request #1973 from penpot/alotor-more-hotfixes
Hotfixes
2022-06-03 10:59:26 +02:00
Alejandro Alonso
2b61b1768f 🐛 Fix exporter white list domains configuration 2022-06-03 07:43:19 +02:00
alonso.torres
424630a67f 📚 Update changelog 2022-06-02 22:53:50 +02:00
alonso.torres
14b1970a8a 🐛 Fix concurrent thumbnail modification 2022-06-02 22:37:33 +02:00
alonso.torres
541168aee4 🐛 Fix problem with some data and text input 2022-06-02 22:35:59 +02:00
alonso.torres
6e9a77edcd 🐛 Fix undo for drawing curves 2022-06-02 22:31:27 +02:00
Pablo Alba
8d1cd2f56d 🐛 Empty groups were not deleted 2022-06-02 16:53:01 +02:00
alonso.torres
0b149dd302 ⬆️ Update to 1.13.3-beta 2022-06-01 10:41:08 +02:00
Pablo Alba
662fc073df Merge pull request #1966 from penpot/alotor-fix-font-loading
🐛 Fix problem with font loading
2022-06-01 10:37:30 +02:00
alonso.torres
46be4ca6d1 🐛 Fix problem with font loading 2022-06-01 09:38:27 +02:00
Alejandro Alonso
c356ae6de8 🐛 Fix github auth without name 2022-05-31 10:26:13 +02:00
Pablo Alba
c5e872b81d 🐛 Remove default font on team change 2022-05-30 14:19:34 +02:00
Alejandro Alonso
0307e58fbe 🐛 Fix old texts with opacity and no fill 2022-05-30 12:40:07 +02:00
Alejandro
5c14c3fafc Merge pull request #1960 from penpot/alotor-fixes
🐛 Fix thumbnails. Add safety text position
2022-05-30 12:21:40 +02:00
alonso.torres
321c3fb34b 🐛 Fix problem with missplaced texts 2022-05-30 12:09:04 +02:00
alonso.torres
4764674374 🐛 Fix thumbnails. Add safety text position 2022-05-30 12:09:04 +02:00
Pablo Alba
0416988913 Set invitations expiration to 7 days 2022-05-30 10:41:23 +02:00
Andrés Moya
ec884787f1 🔧 Fix docker dependencies 2022-05-27 13:57:33 +02:00
30 changed files with 463 additions and 280 deletions

View File

@@ -8,6 +8,29 @@
### :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
- Fix docker dependencies
- Sets invitations expirations to 7 days
- Add safety measure for text positions
- Fix old texts with opacity and no fill
- Remove default font on team change
- Fix github auth without name
- Fix problems with font loading in Firefox 95
## 1.13.2-beta
### :bug: Bugs fixed

View File

@@ -119,7 +119,7 @@
(get-email info))]
{:backend (:name provider)
:email email
:fullname (get-name info)
:fullname (or (get-name info) email)
:props (->> (dissoc info :name :email)
(qualify-props provider))}))

View File

@@ -400,7 +400,7 @@
(defn- create-team-invitation
[{:keys [conn tokens team profile role email] :as cfg}]
(let [member (profile/retrieve-profile-data-by-email conn email)
token-exp (dt/in-future "48h")
token-exp (dt/in-future "168h") ;; 7 days
itoken (tokens :generate
{:iss :team-invitation
:exp token-exp

View File

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

View File

@@ -28,3 +28,11 @@
[shape]
(gpr/points->selrect (position-data-points shape)))
(defn overlaps-position-data?
"Checks if the given position data is inside the shape"
[{:keys [points]} position-data]
(let [bounding-box (gpr/points->selrect points)
fix-rect #(assoc % :y (- (:y %) (:height %)))]
(->> position-data
(some #(gpr/overlaps-rects? bounding-box (fix-rect %)))
(boolean))))

View File

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

View File

@@ -10,6 +10,7 @@
[app.common.geom.matrix :as gmt]
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.path :as gsp]
[app.common.geom.shapes.text :as gsht]
[app.common.logging :as l]
[app.common.math :as mth]
[app.common.pages :as cp]
@@ -415,5 +416,21 @@
(update :pages-index d/update-vals update-container)
(update :components d/update-vals update-container))))
(defmethod migrate 19
[data]
(letfn [(update-object [object]
(cond-> object
(and (cph/text-shape? object)
(d/not-empty? (:position-data object))
(not (gsht/overlaps-position-data? object (:position-data object))))
(dissoc :position-data)))
(update-container [container]
(update container :objects d/update-vals update-object))]
(-> data
(update :pages-index d/update-vals update-container)
(update :components d/update-vals update-container))))
;; TODO: pending to do a migration for delete already not used fill
;; and stroke props. This should be done for >1.14.x version.

View File

@@ -28,6 +28,7 @@ RUN set -ex; \
libasound2 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libatomic1 \
libcairo2 \
libcups2 \
libdbus-1-3 \

View File

@@ -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.

View File

@@ -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]

View File

@@ -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);
}
}
};
}

View File

@@ -79,7 +79,8 @@
(dissoc :dashboard-shared-files)
(dissoc :dashboard-recent-files)
(dissoc :dashboard-team-members)
(dissoc :dashboard-team-stats)))))
(dissoc :dashboard-team-stats)
(update :workspace-global dissoc :default-font)))))
ptk/WatchEvent
(watch [_ state stream]

View File

@@ -150,7 +150,11 @@
:workspace-undo {}
:workspace-project project
:workspace-file (assoc file :initialized true)
:workspace-data (:data file)
:workspace-data (-> (:data file)
;; DEBUG: Uncomment this to try out migrations in local without changing
;; the version number
#_(assoc :version 17)
#_(app.common.pages.migrations/migrate-data 19))
:workspace-libraries (d/index-by :id libraries)))
ptk/WatchEvent

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

View File

@@ -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?

View File

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

View File

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

View File

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

View File

@@ -56,49 +56,53 @@
attrs))
(defn add-fill
([attrs shape render-id]
(add-fill attrs shape render-id nil))
([attrs fill-data render-id type]
(add-fill attrs fill-data render-id nil type))
([attrs shape render-id index]
([attrs fill-data render-id index type]
(let [fill-attrs
(cond
(contains? shape :fill-image)
(contains? fill-data :fill-image)
(let [fill-image-id (str "fill-image-" render-id)]
{:fill (str "url(#" fill-image-id ")")})
(and (contains? shape :fill-color-gradient) (some? (:fill-color-gradient shape)))
(and (contains? fill-data :fill-color-gradient) (some? (:fill-color-gradient fill-data)))
(let [fill-color-gradient-id (str "fill-color-gradient_" render-id (if index (str "_" index) ""))]
{:fill (str "url(#" fill-color-gradient-id ")")})
(contains? shape :fill-color)
{:fill (:fill-color shape)}
(contains? fill-data :fill-color)
{:fill (:fill-color fill-data)}
:else
{:fill "none"})
fill-attrs (cond-> fill-attrs
(contains? shape :fill-opacity)
(assoc :fillOpacity (:fill-opacity shape)))]
(contains? fill-data :fill-opacity)
(assoc :fillOpacity (:fill-opacity fill-data))
;; Old texts with only an opacity set are black by default
(and (= type :text) (nil? (:fill-color-gradient fill-data)) (nil? (:fill-color fill-data)))
(assoc :fill "black"))]
(obj/merge! attrs (clj->js fill-attrs)))))
(defn add-stroke [attrs shape render-id index]
(let [stroke-style (:stroke-style shape :none)
(defn add-stroke [attrs stroke-data render-id index]
(let [stroke-style (:stroke-style stroke-data :none)
stroke-color-gradient-id (str "stroke-color-gradient_" render-id "_" index)
stroke-width (:stroke-width shape 1)]
stroke-width (:stroke-width stroke-data 1)]
(if (not= stroke-style :none)
(let [stroke-attrs
(cond-> {:strokeWidth stroke-width}
(:stroke-color-gradient shape)
(:stroke-color-gradient stroke-data)
(assoc :stroke (str/format "url(#%s)" stroke-color-gradient-id))
(and (not (:stroke-color-gradient shape))
(:stroke-color shape nil))
(assoc :stroke (:stroke-color shape nil))
(and (not (:stroke-color-gradient stroke-data))
(:stroke-color stroke-data nil))
(assoc :stroke (:stroke-color stroke-data nil))
(and (not (:stroke-color-gradient shape))
(:stroke-opacity shape nil))
(assoc :strokeOpacity (:stroke-opacity shape nil))
(and (not (:stroke-color-gradient stroke-data))
(:stroke-opacity stroke-data nil))
(assoc :strokeOpacity (:stroke-opacity stroke-data nil))
(not= stroke-style :svg)
(assoc :strokeDasharray (stroke-type->dasharray stroke-width stroke-style))
@@ -106,29 +110,29 @@
;; For simple line caps we use svg stroke-line-cap attribute. This
;; only works if all caps are the same and we are not using the tricks
;; for inner or outer strokes.
(and (stroke-caps-line (:stroke-cap-start shape))
(= (:stroke-cap-start shape) (:stroke-cap-end shape))
(not (#{:inner :outer} (:stroke-alignment shape)))
(and (stroke-caps-line (:stroke-cap-start stroke-data))
(= (:stroke-cap-start stroke-data) (:stroke-cap-end stroke-data))
(not (#{:inner :outer} (:stroke-alignment stroke-data)))
(not= :dotted stroke-style))
(assoc :strokeLinecap (:stroke-cap-start shape))
(assoc :strokeLinecap (:stroke-cap-start stroke-data))
(= :dotted stroke-style)
(assoc :strokeLinecap "round")
;; For other cap types we use markers.
(and (or (stroke-caps-marker (:stroke-cap-start shape))
(and (stroke-caps-line (:stroke-cap-start shape))
(not= (:stroke-cap-start shape) (:stroke-cap-end shape))))
(not (#{:inner :outer} (:stroke-alignment shape))))
(and (or (stroke-caps-marker (:stroke-cap-start stroke-data))
(and (stroke-caps-line (:stroke-cap-start stroke-data))
(not= (:stroke-cap-start stroke-data) (:stroke-cap-end stroke-data))))
(not (#{:inner :outer} (:stroke-alignment stroke-data))))
(assoc :markerStart
(str/format "url(#marker-%s-%s)" render-id (name (:stroke-cap-start shape))))
(str/format "url(#marker-%s-%s)" render-id (name (:stroke-cap-start stroke-data))))
(and (or (stroke-caps-marker (:stroke-cap-end shape))
(and (stroke-caps-line (:stroke-cap-end shape))
(not= (:stroke-cap-start shape) (:stroke-cap-end shape))))
(not (#{:inner :outer} (:stroke-alignment shape))))
(and (or (stroke-caps-marker (:stroke-cap-end stroke-data))
(and (stroke-caps-line (:stroke-cap-end stroke-data))
(not= (:stroke-cap-start stroke-data) (:stroke-cap-end stroke-data))))
(not (#{:inner :outer} (:stroke-alignment stroke-data))))
(assoc :markerEnd
(str/format "url(#marker-%s-%s)" render-id (name (:stroke-cap-end shape)))))]
(str/format "url(#marker-%s-%s)" render-id (name (:stroke-cap-end stroke-data)))))]
(obj/merge! attrs (clj->js stroke-attrs)))
attrs)))
@@ -195,7 +199,7 @@
(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)
(add-fill styles (d/without-nils (get-in shape [:fills 0])) render-id 0 (:type shape))
:else
styles)]
@@ -211,16 +215,16 @@
(add-style-attrs shape)))
(defn extract-fill-attrs
[shape render-id index]
(let [fill-styles (-> (obj/get shape "style" (obj/new))
(add-fill shape render-id index))]
[fill-data render-id index type]
(let [fill-styles (-> (obj/get fill-data "style" (obj/new))
(add-fill fill-data render-id index type))]
(-> (obj/new)
(obj/set! "style" fill-styles))))
(defn extract-stroke-attrs
[shape index render-id]
(let [stroke-styles (-> (obj/get shape "style" (obj/new))
(add-stroke shape render-id index))]
[stroke-data index render-id]
(let [stroke-styles (-> (obj/get stroke-data "style" (obj/new))
(add-stroke stroke-data render-id index))]
(-> (obj/new)
(obj/set! "style" stroke-styles))))

View File

@@ -375,7 +375,7 @@
(d/not-empty? (:fills shape))
(let [fill-props
(attrs/extract-fill-attrs (get-in shape [:fills 0]) render-id 0)
(attrs/extract-fill-attrs (get-in shape [:fills 0]) render-id 0 (:type shape))
style (-> (obj/get props "style")
(obj/clone)

View File

@@ -53,7 +53,8 @@
:width width
:data-loading loading?}
(= :path (:type shape))
(obj/set! "patternTransform" transform))]
(obj/set! "patternTransform" transform))
type (:type shape)]
(for [[shape-index shape] (d/enumerate (or (:position-data shape) [shape]))]
[:* {:key (dm/str shape-index)}
@@ -73,7 +74,7 @@
(obj/set! "id" fill-id))
[:g
(for [[fill-index value] (-> (d/enumerate (:fills shape [])) reverse)]
[:> :rect (-> (attrs/extract-fill-attrs value render-id fill-index)
[:> :rect (-> (attrs/extract-fill-attrs value render-id fill-index type)
(obj/set! "key" (dm/str fill-index))
(obj/set! "width" width)
(obj/set! "height" height))])

View File

@@ -318,7 +318,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]

View File

@@ -84,7 +84,7 @@
disable-thumbnail? (d/not-empty? (dm/get-in modifiers [(:id shape) :modifiers]))
[on-load-frame-dom render-frame? thumbnail-renderer]
(ftr/use-render-thumbnail page-id shape node-ref rendered? disable-thumbnail?)
(ftr/use-render-thumbnail page-id shape node-ref rendered? disable-thumbnail? @force-render)
on-frame-load
(fns/use-node-store thumbnail? node-ref rendered? render-frame?)]
@@ -129,5 +129,5 @@
[:g.frame-thumbnail-wrapper
{:id (dm/str "thumbnail-container-" (:id shape))
;; Hide the thumbnail when not displaying
:opacity (when (and @rendered? (not thumbnail?)) 0)}
:opacity (when (and @rendered? (not thumbnail?) (not render-frame?)) 0)}
thumbnail-renderer]]]))))

View File

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

View File

@@ -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)
@@ -53,7 +54,7 @@
(defn use-render-thumbnail
"Hook that will create the thumbnail thata"
[page-id {:keys [id x y width height] :as shape} node-ref rendered? disable?]
[page-id {:keys [id x y width height] :as shape} node-ref rendered? disable? force-render]
(let [frame-canvas-ref (mf/use-ref nil)
frame-image-ref (mf/use-ref nil)
@@ -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
@@ -85,12 +88,13 @@
img-node (mf/ref-val frame-image-ref)]
(when (draw-thumbnail-canvas! canvas-node img-node)
(reset! image-url nil)
(reset! render-frame? false))
;; If we don't have the thumbnail data saved (normaly the first load) we update the data
;; when available
(when (not @thumbnail-data-ref)
(st/emit! (dwt/update-thumbnail page-id id) ))))))
;; If we don't have the thumbnail data saved (normaly the first load) we update the data
;; when available
(when (not @thumbnail-data-ref)
(st/emit! (dwt/update-thumbnail page-id id) ))
(reset! render-frame? false))))))
generate-thumbnail
(mf/use-callback
@@ -140,6 +144,24 @@
(.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 []
(when (and (some? thumbnail-data) @render-frame?)
(reset! render-frame? false))))
(mf/use-effect
(mf/deps force-render)
(fn []
(when force-render
(rx/push! updates-str :update))))
(mf/use-effect
(fn []
(let [subid (->> updates-str
@@ -185,12 +207,16 @@
[: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
:style {:filter (when (debug? :thumbnails) "invert(1)")}}]]
:style {:filter (when (debug? :thumbnails) "invert(1)")
:width "100%"
:height "100%"}}]]
(when (some? @image-url)
[:image {:ref frame-image-ref

View File

@@ -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))}]]]]))

View File

@@ -9,6 +9,7 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.text :as gsht]
[app.common.math :as mth]
[app.common.pages.helpers :as cph]
[app.common.text :as txt]
@@ -21,7 +22,7 @@
[app.util.dom :as dom]
[app.util.object :as obj]
[app.util.text-editor :as ted]
[app.util.text-svg-position :as utp]
[app.util.text-svg-position :as tsp]
[app.util.timers :as ts]
[promesa.core :as p]
[rumext.alpha :as mf]))
@@ -62,11 +63,13 @@
(assoc :content (d/txt-merge content editor-content)))))
(defn- update-text-shape
[{:keys [grow-type id migrate]} node]
[{:keys [grow-type id migrate] :as shape} node]
;; Check if we need to update the size because it's auto-width or auto-height
;; Update the position-data of every text fragment
(p/let [position-data (utp/calc-position-data node)]
(st/emit! (dwt/update-position-data id position-data))
(p/let [position-data (tsp/calc-position-data node)]
;; At least one paragraph needs to be inside the bounding box
(when (gsht/overlaps-position-data? shape position-data)
(st/emit! (dwt/update-position-data id position-data)))
(when (contains? #{:auto-height :auto-width} grow-type)
(let [{:keys [width height]}
@@ -77,13 +80,12 @@
(when (and (not (mth/almost-zero? width))
(not (mth/almost-zero? height))
(not migrate))
(st/emit! (dwt/resize-text id width height))))))
(st/emit! (dwt/clean-text-modifier id)))
(st/emit! (dwt/resize-text id width height)))))
(st/emit! (dwt/clean-text-modifier id))))
(defn- update-text-modifier
[{:keys [grow-type id]} node]
(p/let [position-data (utp/calc-position-data node)
(p/let [position-data (tsp/calc-position-data node)
props {:position-data position-data}
props

View File

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

View File

@@ -7,6 +7,7 @@
(ns app.util.text-svg-position
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]
[app.common.transit :as transit]
[app.main.fonts :as fonts]
@@ -43,13 +44,20 @@
[^js node]
(let [styles (js/getComputedStyle node)
font (.getPropertyValue styles "font")]
(if (dom/check-font? font)
(p/resolved font)
(let [font-id (.getPropertyValue styles "--font-id")]
(-> (fonts/ensure-loaded! font-id)
(p/then #(when (not (dom/check-font? font))
(load-font font))))))))
font (.getPropertyValue styles "font")
font (if (or (not font) (empty? font))
;; Firefox 95 won't return the font correctly.
;; We can get the font shorthand with the font-size + font-family
(dm/str (.getPropertyValue styles "font-size")
" "
(.getPropertyValue styles "font-family"))
font)
font-id (.getPropertyValue styles "--font-id")]
(-> (fonts/ensure-loaded! font-id)
(p/then #(when (not (dom/check-font? font))
(load-font font))))))
(defn calc-text-node-positions
[base-node viewport zoom]

View File

@@ -1 +1 @@
1.13.2-beta
1.13.4-beta