mirror of
https://github.com/penpot/penpot.git
synced 2026-02-18 15:19:00 -05:00
Compare commits
42 Commits
2.3.0-RC3
...
renderer-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32fe91398a | ||
|
|
b36c8cd52a | ||
|
|
f5acfd0787 | ||
|
|
4939bc06ac | ||
|
|
cd63fb78d2 | ||
|
|
3298785436 | ||
|
|
eeb0d21013 | ||
|
|
a11c2af542 | ||
|
|
6d5b0204e9 | ||
|
|
dfe5d861f2 | ||
|
|
445691430b | ||
|
|
88722bcf4f | ||
|
|
43903014c6 | ||
|
|
cf8b62f1a8 | ||
|
|
39b627cb1a | ||
|
|
81680cffe9 | ||
|
|
dc014bd4eb | ||
|
|
0027e77861 | ||
|
|
fa9004d12c | ||
|
|
c7f801dd44 | ||
|
|
0f0b23e38b | ||
|
|
1f8fe2dc4c | ||
|
|
e84622061d | ||
|
|
305de33200 | ||
|
|
80bbfe7a6f | ||
|
|
26ab39a45d | ||
|
|
739b8d7c02 | ||
|
|
e0a9f63015 | ||
|
|
928709a0f2 | ||
|
|
579b157ab7 | ||
|
|
0bf442e626 | ||
|
|
2184af6602 | ||
|
|
78fb938d16 | ||
|
|
dd9185e058 | ||
|
|
5f8d56b366 | ||
|
|
bc0fde68c7 | ||
|
|
024a2ae848 | ||
|
|
4d56bf66f4 | ||
|
|
c83ef201a1 | ||
|
|
6d26abb9e3 | ||
|
|
1b1f08388f | ||
|
|
472c769c9a |
@@ -111,7 +111,7 @@ jobs:
|
||||
yarn run build:app:assets
|
||||
clojure -M:dev:shadow-cljs release main
|
||||
yarn playwright install --with-deps chromium
|
||||
yarn test:e2e
|
||||
yarn e2e:test
|
||||
|
||||
- run:
|
||||
name: "backend tests"
|
||||
|
||||
45
CHANGES.md
45
CHANGES.md
@@ -4,57 +4,14 @@
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
- **New plugin system.**
|
||||
|
||||
Penpot now supports custom plugins. Read everything about developing your plugins [HERE](https://help.penpot.app/plugins/)
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
- All our plugins beta testers :heart:.
|
||||
- Fix problem when translating multiple path points by @eeropic [#4459](https://github.com/penpot/penpot/issues/4459)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- **Replace Draft.js completely with a custom editor** [Taiga #7706](https://tree.taiga.io/project/penpot/us/7706)
|
||||
|
||||
This refactor adds better IME support, more performant text editing
|
||||
experience and a better clipboard support while keeping full
|
||||
retrocompatibility with previous editor.
|
||||
|
||||
You can enable it with the `enable-feature-text-editor-v2` configuration flag.
|
||||
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem with constraints buttons [Taiga #8465](https://tree.taiga.io/project/penpot/issue/8465)
|
||||
- Fix problem with go back button on error page [Taiga #8887](https://tree.taiga.io/project/penpot/issue/8887)
|
||||
- Fix problem with shadows in text for Safari [Taiga #8770](https://tree.taiga.io/project/penpot/issue/8770)
|
||||
- Fix a regression with feedback form subject and content limits [Taiga #8908](https://tree.taiga.io/project/penpot/issue/8908)
|
||||
- Fix problem with stroke and filter ordering in frames [Github #5058](https://github.com/penpot/penpot/issues/5058)
|
||||
- Fix problem with hover layers when hidden/blocked [Github #5074](https://github.com/penpot/penpot/issues/5074)
|
||||
- Fix problem with precision on boolean calculation [Taiga #8482](https://tree.taiga.io/project/penpot/issue/8482)
|
||||
- Fix problem when translating multiple path points [Github #4459](https://github.com/penpot/penpot/issues/4459)
|
||||
- Fix problem on importing (and exporting) files with flows [Taiga #8914](https://tree.taiga.io/project/penpot/issue/8914)
|
||||
- Fix Internal Error page: "go to your penpot" wrong design [Taiga #8922](https://tree.taiga.io/project/penpot/issue/8922)
|
||||
- Fix problem updating layout when toggle visibility in component copy [Github #5143](https://github.com/penpot/penpot/issues/5143)
|
||||
- Fix "Done" button on toolbar on inspect mode should go to design mode [Taiga #8933](https://tree.taiga.io/project/penpot/issue/8933)
|
||||
- Fix problem with shortcuts in text editor [Github #5078](https://github.com/penpot/penpot/issues/5078)
|
||||
- Fix problems with show in viewer and interactions [Github #4868](https://github.com/penpot/penpot/issues/4868)
|
||||
- Add visual feedback when moving an element into a board [Github #3210](https://github.com/penpot/penpot/issues/3210)
|
||||
- Fix percent calculation on grid layout tracks [Github #4688](https://github.com/penpot/penpot/issues/4688)
|
||||
- Fix problem with caps and inner shadows [Github #4517](https://github.com/penpot/penpot/issues/4517)
|
||||
- Fix problem with horizontal/vertical lines and shadows [Github #4516](https://github.com/penpot/penpot/issues/4516)
|
||||
|
||||
## 2.2.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem with Ctrl+F shortcut on the dashboard [Taiga #8876](https://tree.taiga.io/project/penpot/issue/8876)
|
||||
- Fix visual problem with the font-size dropdown in assets [Taiga #8872](https://tree.taiga.io/project/penpot/issue/8872)
|
||||
- Add limits for invitation RPC methods (hard limit 25 emails per request)
|
||||
|
||||
## 2.2.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
@@ -191,7 +148,7 @@ time being.
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :heart: Communityq contributions (Thank you!)
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ export PENPOT_FLAGS="\
|
||||
enable-urepl-server \
|
||||
enable-rpc-climit \
|
||||
enable-rpc-rlimit \
|
||||
enable-quotes \
|
||||
enable-soft-rpc-rlimit \
|
||||
enable-auto-file-snapshot \
|
||||
enable-webhooks \
|
||||
|
||||
@@ -17,7 +17,6 @@ export PENPOT_FLAGS="\
|
||||
disable-secure-session-cookies \
|
||||
enable-rpc-climit \
|
||||
enable-smtp \
|
||||
enable-quotes \
|
||||
enable-file-snapshot \
|
||||
enable-access-tokens \
|
||||
enable-tiered-file-data-storage \
|
||||
|
||||
@@ -315,13 +315,15 @@
|
||||
(l/dbg :hint "sendmail"
|
||||
:id (:id params)
|
||||
:to (:to params)
|
||||
:subject (str/trim (:subject params)))
|
||||
:subject (str/trim (:subject params))
|
||||
:body (str/join "," (map :type (:body params))))
|
||||
|
||||
(.sendMessage ^Transport transport
|
||||
^MimeMessage message
|
||||
(.getAllRecipients message))))))
|
||||
|
||||
(when (contains? cf/flags :log-emails)
|
||||
(when (or (contains? cf/flags :log-emails)
|
||||
(not (contains? cf/flags :smtp)))
|
||||
(send-to-logger! cfg params))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
|
||||
@@ -60,9 +60,6 @@
|
||||
(try
|
||||
(let [result (handler)]
|
||||
(events/tap :end result))
|
||||
|
||||
(catch java.io.EOFException cause
|
||||
(events/tap :error (errors/handle' cause request)))
|
||||
(catch Throwable cause
|
||||
(l/err :hint "unexpected error on processing sse response"
|
||||
:cause cause)
|
||||
|
||||
@@ -278,18 +278,25 @@
|
||||
:inc 1)
|
||||
message)
|
||||
|
||||
(def ^:private schema:params
|
||||
[:map {:title "params"}
|
||||
[:session-id ::sm/uuid]])
|
||||
|
||||
(def ^:private decode-params
|
||||
(sm/decoder schema:params sm/json-transformer))
|
||||
|
||||
(def ^:private validate-params!
|
||||
(sm/validate-fn schema:params))
|
||||
|
||||
(defn- http-handler
|
||||
[cfg {:keys [params ::session/profile-id] :as request}]
|
||||
(let [session-id (some-> params :session-id sm/parse-uuid)]
|
||||
(when-not (uuid? session-id)
|
||||
(ex/raise :type :validation
|
||||
:code :missing-session-id
|
||||
:hint "missing or invalid session-id found"))
|
||||
|
||||
(let [{:keys [session-id]} (-> params
|
||||
decode-params
|
||||
validate-params!)]
|
||||
(cond
|
||||
(not profile-id)
|
||||
(ex/raise :type :authentication
|
||||
:hint "authentication required")
|
||||
:hint "Authentication required.")
|
||||
|
||||
;; WORKAROUND: we use the adapter specific predicate for
|
||||
;; performance reasons; for now, the ring default impl for
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
|
||||
(l/dbg :hint "run webhook"
|
||||
:event-name (:name event)
|
||||
:webhook-id (str (:id whook))
|
||||
:webhook-id (:id whook)
|
||||
:webhook-uri (:uri whook)
|
||||
:webhook-mtype (:mtype whook))
|
||||
|
||||
|
||||
@@ -149,13 +149,6 @@
|
||||
:hint "authentication required for this endpoint")
|
||||
(f cfg params)))))
|
||||
|
||||
(defn- wrap-db-transaction
|
||||
[_ f mdata]
|
||||
(if (::db/transaction mdata)
|
||||
(fn [cfg params]
|
||||
(db/tx-run! cfg f params))
|
||||
f))
|
||||
|
||||
(defn- wrap-audit
|
||||
[_ f mdata]
|
||||
(if (or (contains? cf/flags :webhooks)
|
||||
@@ -203,7 +196,6 @@
|
||||
(defn- wrap-all
|
||||
[cfg f mdata]
|
||||
(as-> f $
|
||||
(wrap-db-transaction cfg $ mdata)
|
||||
(cond/wrap cfg $ mdata)
|
||||
(retry/wrap-retry cfg $ mdata)
|
||||
(climit/wrap cfg $ mdata)
|
||||
|
||||
@@ -30,17 +30,18 @@
|
||||
:tid token-id
|
||||
:iat created-at})
|
||||
|
||||
expires-at (some-> expiration dt/in-future)
|
||||
token (db/insert! conn :access-token
|
||||
{:id token-id
|
||||
:name name
|
||||
:token token
|
||||
:profile-id profile-id
|
||||
:created-at created-at
|
||||
:updated-at created-at
|
||||
:expires-at expires-at
|
||||
:perms (db/create-array conn "text" [])})]
|
||||
(decode-row token)))
|
||||
expires-at (some-> expiration dt/in-future)]
|
||||
|
||||
(db/insert! conn :access-token
|
||||
{:id token-id
|
||||
:name name
|
||||
:token token
|
||||
:profile-id profile-id
|
||||
:created-at created-at
|
||||
:updated-at created-at
|
||||
:expires-at expires-at
|
||||
:perms (db/create-array conn "text" [])})))
|
||||
|
||||
|
||||
(defn repl:create-access-token
|
||||
[{:keys [::db/pool] :as system} profile-id name expiration]
|
||||
@@ -59,12 +60,14 @@
|
||||
(sv/defmethod ::create-access-token
|
||||
{::doc/added "1.18"
|
||||
::sm/params schema:create-access-token}
|
||||
[cfg {:keys [::rpc/profile-id name expiration]}]
|
||||
|
||||
(quotes/check! cfg {::quotes/id ::quotes/access-tokens-per-profile
|
||||
::quotes/profile-id profile-id})
|
||||
|
||||
(db/tx-run! cfg create-access-token profile-id name expiration))
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name expiration]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [cfg (assoc cfg ::db/conn conn)]
|
||||
(quotes/check-quote! conn
|
||||
{::quotes/id ::quotes/access-tokens-per-profile
|
||||
::quotes/profile-id profile-id})
|
||||
(-> (create-access-token cfg profile-id name expiration)
|
||||
(decode-row)))))
|
||||
|
||||
(def ^:private schema:delete-access-token
|
||||
[:map {:title "delete-access-token"}
|
||||
|
||||
@@ -71,15 +71,10 @@
|
||||
[conn comment-id & {:as opts}]
|
||||
(db/get-by-id conn :comment comment-id opts))
|
||||
|
||||
(def ^:private sql:get-next-seqn
|
||||
"SELECT (f.comment_thread_seqn + 1) AS next_seqn
|
||||
FROM file AS f
|
||||
WHERE f.id = ?
|
||||
FOR UPDATE")
|
||||
|
||||
(defn- get-next-seqn
|
||||
[conn file-id]
|
||||
(let [res (db/exec-one! conn [sql:get-next-seqn file-id])]
|
||||
(let [sql "select (f.comment_thread_seqn + 1) as next_seqn from file as f where f.id = ?"
|
||||
res (db/exec-one! conn [sql file-id])]
|
||||
(:next-seqn res)))
|
||||
|
||||
(def sql:upsert-comment-thread-status
|
||||
@@ -309,43 +304,38 @@
|
||||
::rtry/when rtry/conflict-exception?
|
||||
::sm/params schema:create-comment-thread}
|
||||
[cfg {:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id position content frame-id]}]
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
|
||||
(let [{:keys [team-id project-id page-name]} (get-file cfg file-id page-id)]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
(let [{:keys [team-id project-id page-name]} (get-file conn file-id page-id)]
|
||||
|
||||
(-> cfg
|
||||
(assoc ::quotes/profile-id profile-id)
|
||||
(assoc ::quotes/team-id team-id)
|
||||
(assoc ::quotes/project-id project-id)
|
||||
(assoc ::quotes/file-id file-id)
|
||||
(quotes/check! {::quotes/id ::quotes/comment-threads-per-file}
|
||||
{::quotes/id ::quotes/comments-per-file}))
|
||||
(run! (partial quotes/check-quote! cfg)
|
||||
(list {::quotes/id ::quotes/comment-threads-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id file-id}
|
||||
{::quotes/id ::quotes/comments-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id file-id}))
|
||||
|
||||
(let [params {:created-at request-at
|
||||
:profile-id profile-id
|
||||
:file-id file-id
|
||||
:page-id page-id
|
||||
:page-name page-name
|
||||
:position position
|
||||
:content content
|
||||
:frame-id frame-id}
|
||||
thread (db/tx-run! cfg create-comment-thread params)]
|
||||
|
||||
(vary-meta thread assoc ::audit/props thread))))
|
||||
(create-comment-thread conn {:created-at request-at
|
||||
:profile-id profile-id
|
||||
:file-id file-id
|
||||
:page-id page-id
|
||||
:page-name page-name
|
||||
:position position
|
||||
:content content
|
||||
:frame-id frame-id})))))
|
||||
|
||||
(defn- create-comment-thread
|
||||
[{:keys [::db/conn] :as cfg}
|
||||
{:keys [profile-id file-id page-id page-name created-at position content frame-id]}]
|
||||
|
||||
(let [;; NOTE: we take the next seq number from a separate query
|
||||
;; because we need to lock the file for avoid race conditions
|
||||
|
||||
;; FIXME: this method touches and locks the file table,which
|
||||
;; is already heavy-update tablel; we need to think on move
|
||||
;; the sequence state management to a different table or
|
||||
;; different storage (example: redis) for alivate the update
|
||||
;; pression on the file table
|
||||
[conn {:keys [profile-id file-id page-id page-name created-at position content frame-id]}]
|
||||
|
||||
(let [;; NOTE: we take the next seq number from a separate query because the whole
|
||||
;; operation can be retried on conflict, and in this case the new seq shold be
|
||||
;; retrieved from the database.
|
||||
seqn (get-next-seqn conn file-id)
|
||||
thread-id (uuid/next)
|
||||
thread (db/insert! conn :comment-thread
|
||||
@@ -374,8 +364,7 @@
|
||||
;; Optimistic update of current seq number on file.
|
||||
(db/update! conn :file
|
||||
{:comment-thread-seqn seqn}
|
||||
{:id file-id}
|
||||
{::db/return-keys false})
|
||||
{:id file-id})
|
||||
|
||||
(-> thread
|
||||
(select-keys [:id :file-id :page-id])
|
||||
@@ -398,6 +387,7 @@
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(upsert-comment-thread-status! conn profile-id id)))))
|
||||
|
||||
|
||||
;; --- COMMAND: Update Comment Thread
|
||||
|
||||
(def ^:private
|
||||
@@ -442,11 +432,12 @@
|
||||
{:keys [team-id project-id page-name] :as file} (get-file cfg file-id page-id)]
|
||||
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(quotes/check! cfg {::quotes/id ::quotes/comments-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id file-id})
|
||||
(quotes/check-quote! conn
|
||||
{::quotes/id ::quotes/comments-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id file-id})
|
||||
|
||||
;; Update the page-name cached attribute on comment thread table.
|
||||
(when (not= page-name (:page-name thread))
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
|
||||
(def ^:private schema:send-user-feedback
|
||||
[:map {:title "send-user-feedback"}
|
||||
[:subject [:string {:max 400}]]
|
||||
[:content [:string {:max 2500}]]])
|
||||
[:subject [:string {:max 250}]]
|
||||
[:content [:string {:max 250}]]])
|
||||
|
||||
(sv/defmethod ::send-user-feedback
|
||||
{::doc/added "1.18"
|
||||
|
||||
@@ -98,49 +98,46 @@
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files
|
||||
::webhooks/event? true
|
||||
::sm/params schema:create-file
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
|
||||
(projects/check-edition-permissions! conn profile-id project-id)
|
||||
(let [team (teams/get-team conn
|
||||
:profile-id profile-id
|
||||
:project-id project-id)
|
||||
team-id (:id team)
|
||||
::sm/params schema:create-file}
|
||||
[cfg {:keys [::rpc/profile-id project-id] :as params}]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(projects/check-edition-permissions! conn profile-id project-id)
|
||||
(let [team (teams/get-team conn
|
||||
:profile-id profile-id
|
||||
:project-id project-id)
|
||||
team-id (:id team)
|
||||
|
||||
;; When we create files, we only need to respect the team
|
||||
;; features, because some features can be enabled
|
||||
;; globally, but the team is still not migrated properly.
|
||||
features (-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params)))
|
||||
;; When we create files, we only need to respect the team
|
||||
;; features, because some features can be enabled
|
||||
;; globally, but the team is still not migrated properly.
|
||||
features (-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params)))
|
||||
|
||||
;; We also include all no migration features declared by
|
||||
;; client; that enables the ability to enable a runtime
|
||||
;; feature on frontend and make it permanent on file
|
||||
features (-> (:features params #{})
|
||||
(set/intersection cfeat/no-migration-features)
|
||||
(set/union features))
|
||||
;; We also include all no migration features declared by
|
||||
;; client; that enables the ability to enable a runtime
|
||||
;; feature on frontend and make it permanent on file
|
||||
features (-> (:features params #{})
|
||||
(set/intersection cfeat/no-migration-features)
|
||||
(set/union features))
|
||||
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features features))]
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features features))]
|
||||
|
||||
(quotes/check! cfg {::quotes/id ::quotes/files-per-project
|
||||
::quotes/team-id team-id
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/project-id project-id})
|
||||
(run! (partial quotes/check-quote! conn)
|
||||
(list {::quotes/id ::quotes/files-per-project
|
||||
::quotes/team-id team-id
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/project-id project-id}))
|
||||
|
||||
;; FIXME: IMPORTANT: this code can have race
|
||||
;; conditions, because we have no locks for updating
|
||||
;; team so, creating two files concurrently can lead
|
||||
;; to lost team features updating
|
||||
;; When newly computed features does not match exactly with
|
||||
;; the features defined on team row, we update it.
|
||||
(when (not= features (:features team))
|
||||
(let [features (db/create-array conn "text" features)]
|
||||
(db/update! conn :team
|
||||
{:features features}
|
||||
{:id team-id})))
|
||||
|
||||
;; When newly computed features does not match exactly with
|
||||
;; the features defined on team row, we update it.
|
||||
(when (not= features (:features team))
|
||||
(let [features (db/create-array conn "text" features)]
|
||||
(db/update! conn :team
|
||||
{:features features}
|
||||
{:id team-id})))
|
||||
|
||||
(-> (create-file cfg params)
|
||||
(vary-meta assoc ::audit/props {:team-id team-id}))))
|
||||
(-> (create-file cfg params)
|
||||
(vary-meta assoc ::audit/props {:team-id team-id}))))))
|
||||
|
||||
@@ -45,38 +45,37 @@
|
||||
(sv/defmethod ::create-temp-file
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files
|
||||
::sm/params schema:create-temp-file
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
|
||||
(projects/check-edition-permissions! conn profile-id project-id)
|
||||
(let [team (teams/get-team conn :profile-id profile-id :project-id project-id)
|
||||
;; When we create files, we only need to respect the team
|
||||
;; features, because some features can be enabled
|
||||
;; globally, but the team is still not migrated properly.
|
||||
input-features
|
||||
(:features params #{})
|
||||
::sm/params schema:create-temp-file}
|
||||
[cfg {:keys [::rpc/profile-id project-id] :as params}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(projects/check-edition-permissions! conn profile-id project-id)
|
||||
(let [team (teams/get-team conn :profile-id profile-id :project-id project-id)
|
||||
|
||||
;; If the imported project doesn't contain v2 we need to remove it
|
||||
team-features
|
||||
(cond-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(not (contains? input-features "components/v2"))
|
||||
(disj "components/v2"))
|
||||
;; When we create files, we only need to respect the team
|
||||
;; features, because some features can be enabled
|
||||
;; globally, but the team is still not migrated properly.
|
||||
input-features (:features params #{})
|
||||
|
||||
;; We also include all no migration features declared by
|
||||
;; client; that enables the ability to enable a runtime
|
||||
;; feature on frontend and make it permanent on file
|
||||
features
|
||||
(-> input-features
|
||||
(set/intersection cfeat/no-migration-features)
|
||||
(set/union team-features))
|
||||
;; If the imported project doesn't contain v2 we need to remove it
|
||||
team-features
|
||||
(cond-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(not (contains? input-features "components/v2"))
|
||||
(disj "components/v2"))
|
||||
|
||||
params
|
||||
(-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :deleted-at (dt/in-future {:days 1}))
|
||||
(assoc :features features))]
|
||||
|
||||
(files.create/create-file cfg params)))
|
||||
;; We also include all no migration features declared by
|
||||
;; client; that enables the ability to enable a runtime
|
||||
;; feature on frontend and make it permanent on file
|
||||
features (-> input-features
|
||||
(set/intersection cfeat/no-migration-features)
|
||||
(set/union team-features))
|
||||
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :deleted-at (dt/in-future {:days 1}))
|
||||
(assoc :features features))]
|
||||
|
||||
(files.create/create-file cfg params)))))
|
||||
|
||||
;; --- MUTATION COMMAND: update-temp-file
|
||||
|
||||
|
||||
@@ -86,9 +86,6 @@
|
||||
[:font-weight [::sm/one-of {:format "number"} valid-weight]]
|
||||
[:font-style [::sm/one-of {:format "string"} valid-style]]])
|
||||
|
||||
;; FIXME: IMPORTANT: refactor this, we should not hold a whole db
|
||||
;; connection around the font creation
|
||||
|
||||
(sv/defmethod ::create-font-variant
|
||||
{::doc/added "1.18"
|
||||
::climit/id [[:process-font/by-profile ::rpc/profile-id]
|
||||
@@ -99,9 +96,9 @@
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(teams/check-edition-permissions! conn profile-id team-id)
|
||||
(quotes/check! cfg {::quotes/id ::quotes/font-variants-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id})
|
||||
(quotes/check-quote! conn {::quotes/id ::quotes/font-variants-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id})
|
||||
(create-font-variant cfg (assoc params :profile-id profile-id)))))
|
||||
|
||||
(defn create-font-variant
|
||||
|
||||
@@ -168,17 +168,6 @@
|
||||
|
||||
;; --- MUTATION: Create Project
|
||||
|
||||
(defn- create-project
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id team-id] :as params}]
|
||||
(let [project (teams/create-project conn params)]
|
||||
(teams/create-project-role conn profile-id (:id project) :owner)
|
||||
(db/insert! conn :team-project-profile-rel
|
||||
{:project-id (:id project)
|
||||
:profile-id profile-id
|
||||
:team-id team-id
|
||||
:is-pinned false})
|
||||
(assoc project :is-pinned false)))
|
||||
|
||||
(def ^:private schema:create-project
|
||||
[:map {:title "create-project"}
|
||||
[:team-id ::sm/uuid]
|
||||
@@ -189,15 +178,23 @@
|
||||
{::doc/added "1.18"
|
||||
::webhooks/event? true
|
||||
::sm/params schema:create-project}
|
||||
[cfg {:keys [::rpc/profile-id team-id] :as params}]
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(teams/check-edition-permissions! conn profile-id team-id)
|
||||
(quotes/check-quote! conn {::quotes/id ::quotes/projects-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id})
|
||||
|
||||
(teams/check-edition-permissions! cfg profile-id team-id)
|
||||
(quotes/check! cfg {::quotes/id ::quotes/projects-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id})
|
||||
(let [params (assoc params :profile-id profile-id)
|
||||
project (teams/create-project conn params)]
|
||||
(teams/create-project-role conn profile-id (:id project) :owner)
|
||||
(db/insert! conn :team-project-profile-rel
|
||||
{:project-id (:id project)
|
||||
:profile-id profile-id
|
||||
:team-id team-id
|
||||
:is-pinned false})
|
||||
(assoc project :is-pinned false))))
|
||||
|
||||
(let [params (assoc params :profile-id profile-id)]
|
||||
(db/tx-run! cfg create-project params)))
|
||||
|
||||
;; --- MUTATION: Toggle Project Pin
|
||||
|
||||
|
||||
@@ -82,17 +82,19 @@
|
||||
(cond-> row
|
||||
(some? features) (assoc :features (db/decode-pgarray features #{}))))
|
||||
|
||||
(defn- check-profile-muted
|
||||
"Check if the member's email is part of the global bounce report"
|
||||
|
||||
|
||||
(defn- check-valid-email-muted
|
||||
"Check if the member's email is part of the global bounce report."
|
||||
[conn member]
|
||||
(let [email (profile/clean-email (:email member))]
|
||||
(let [email (profile/clean-email (:email member))]
|
||||
(when (and member (not (eml/allow-send-emails? conn member)))
|
||||
(ex/raise :type :validation
|
||||
:code :member-is-muted
|
||||
:email email
|
||||
:hint "the profile has reported repeatedly as spam or has bounces"))))
|
||||
|
||||
(defn- check-email-bounce
|
||||
(defn- check-valid-email-bounce
|
||||
"Check if the email is part of the global complain report"
|
||||
[conn email show?]
|
||||
(when (eml/has-bounce-reports? conn email)
|
||||
@@ -101,7 +103,7 @@
|
||||
:email (if show? email "private")
|
||||
:hint "this email has been repeatedly reported as bounce")))
|
||||
|
||||
(defn- check-email-spam
|
||||
(defn- check-valid-email-spam
|
||||
"Check if the member email is part of the global complain report"
|
||||
[conn email show?]
|
||||
(when (eml/has-complaint-reports? conn email)
|
||||
@@ -225,16 +227,16 @@
|
||||
;; --- Query: Team Members
|
||||
|
||||
(def sql:team-members
|
||||
"SELECT tp.*,
|
||||
"select tp.*,
|
||||
p.id,
|
||||
p.email,
|
||||
p.fullname AS name,
|
||||
p.fullname AS fullname,
|
||||
p.fullname as name,
|
||||
p.fullname as fullname,
|
||||
p.photo_id,
|
||||
p.is_active
|
||||
FROM team_profile_rel AS tp
|
||||
JOIN profile AS p ON (p.id = tp.profile_id)
|
||||
WHERE tp.team_id = ?")
|
||||
from team_profile_rel as tp
|
||||
join profile as p on (p.id = tp.profile_id)
|
||||
where tp.team_id = ?")
|
||||
|
||||
(defn get-team-members
|
||||
[conn team-id]
|
||||
@@ -401,19 +403,17 @@
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:create-team}
|
||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(quotes/check-quote! conn {::quotes/id ::quotes/teams-per-profile
|
||||
::quotes/profile-id profile-id})
|
||||
|
||||
(quotes/check! cfg {::quotes/id ::quotes/teams-per-profile
|
||||
::quotes/profile-id profile-id})
|
||||
|
||||
(let [features (-> (cfeat/get-enabled-features cf/flags)
|
||||
(cfeat/check-client-features! (:features params)))
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features features))
|
||||
team (db/tx-run! cfg create-team params)]
|
||||
|
||||
(with-meta team
|
||||
{::audit/props {:id (:id team)}})))
|
||||
(let [features (-> (cfeat/get-enabled-features cf/flags)
|
||||
(cfeat/check-client-features! (:features params)))
|
||||
team (create-team cfg (assoc params
|
||||
:profile-id profile-id
|
||||
:features features))]
|
||||
(with-meta team
|
||||
{::audit/props {:id (:id team)}})))))
|
||||
|
||||
(defn create-team
|
||||
"This is a complete team creation process, it creates the team
|
||||
@@ -767,51 +767,21 @@
|
||||
:member-id member-id}))
|
||||
|
||||
(defn- create-profile-identity-token
|
||||
[cfg profile-id]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid uuid for profile-id"
|
||||
(uuid? profile-id))
|
||||
|
||||
[cfg profile]
|
||||
(tokens/generate (::setup/props cfg)
|
||||
{:iss :profile-identity
|
||||
:profile-id profile-id
|
||||
:profile-id (:id profile)
|
||||
:exp (dt/in-future {:days 30})}))
|
||||
|
||||
(def ^:private schema:create-invitation
|
||||
[:map {:title "params:create-invitation"}
|
||||
[::rpc/profile-id ::sm/uuid]
|
||||
[:team
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]]]
|
||||
[:profile
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:fullname :string]]]
|
||||
[:role [::sm/one-of valid-roles]]
|
||||
[:email ::sm/email]])
|
||||
|
||||
(def ^:private check-create-invitation-params!
|
||||
(sm/check-fn schema:create-invitation))
|
||||
|
||||
(defn- create-invitation
|
||||
[{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid connection on cfg parameter"
|
||||
(db/connection? conn))
|
||||
|
||||
(dm/assert!
|
||||
"expected valid params for `create-invitation` fn"
|
||||
(check-create-invitation-params! params))
|
||||
|
||||
(let [email (profile/clean-email email)
|
||||
member (profile/get-profile-by-email conn email)]
|
||||
|
||||
(check-profile-muted conn member)
|
||||
(check-email-bounce conn email true)
|
||||
(check-email-spam conn email true)
|
||||
(check-valid-email-muted conn member)
|
||||
(check-valid-email-bounce conn email true)
|
||||
(check-valid-email-spam conn email true)
|
||||
|
||||
|
||||
;; When we have email verification disabled and invitation user is
|
||||
;; already present in the database, we proceed to add it to the
|
||||
@@ -845,8 +815,7 @@
|
||||
(name role) expire
|
||||
(name role) expire])
|
||||
updated? (not= id (:id invitation))
|
||||
profile-id (:id profile)
|
||||
tprops {:profile-id profile-id
|
||||
tprops {:profile-id (:id profile)
|
||||
:invitation-id (:id invitation)
|
||||
:valid-until expire
|
||||
:team-id (:id team)
|
||||
@@ -854,11 +823,12 @@
|
||||
:member-id (:id member)
|
||||
:role role}
|
||||
itoken (create-invitation-token cfg tprops)
|
||||
ptoken (create-profile-identity-token cfg profile-id)]
|
||||
ptoken (create-profile-identity-token cfg profile)]
|
||||
|
||||
(when (contains? cf/flags :log-invitation-tokens)
|
||||
(l/info :hint "invitation token" :token itoken))
|
||||
|
||||
|
||||
(let [props (-> (dissoc tprops :profile-id)
|
||||
(audit/clean-props))
|
||||
evname (if updated?
|
||||
@@ -881,27 +851,26 @@
|
||||
itoken))))
|
||||
|
||||
(defn- add-user-to-team
|
||||
[conn profile team role email]
|
||||
[conn profile team email role]
|
||||
|
||||
(let [team-id (:id team)
|
||||
member (db/get* conn :profile
|
||||
{:email (str/lower email)}
|
||||
{::sql/columns [:id :email]})
|
||||
params (merge
|
||||
{:team-id team-id
|
||||
:profile-id (:id member)}
|
||||
(role->params role))]
|
||||
member (db/get* conn :profile
|
||||
{:email (str/lower email)}
|
||||
{::sql/columns [:id :email]})
|
||||
params (merge
|
||||
{:team-id team-id
|
||||
:profile-id (:id member)}
|
||||
(role->params role))]
|
||||
|
||||
;; Do not allow blocked users to join teams.
|
||||
(when (:is-blocked member)
|
||||
(ex/raise :type :restriction
|
||||
:code :profile-blocked))
|
||||
|
||||
(quotes/check!
|
||||
{::db/conn conn
|
||||
::quotes/id ::quotes/profiles-per-team
|
||||
::quotes/profile-id (:id member)
|
||||
::quotes/team-id team-id})
|
||||
(quotes/check-quote! conn
|
||||
{::quotes/id ::quotes/profiles-per-team
|
||||
::quotes/profile-id (:id member)
|
||||
::quotes/team-id team-id})
|
||||
|
||||
;; Insert the member to the team
|
||||
(db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true})
|
||||
@@ -933,89 +902,68 @@
|
||||
[conn team-id]
|
||||
(db/exec! conn [sql:valid-requests-email team-id]))
|
||||
|
||||
(def ^:private xf:map-email
|
||||
(map :email))
|
||||
|
||||
(defn- create-team-invitations
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile team role emails] :as params}]
|
||||
(let [join-requests (into #{} xf:map-email
|
||||
(get-valid-requests-email conn (:id team)))
|
||||
team-members (into #{} xf:map-email
|
||||
(get-team-members conn (:id team)))
|
||||
|
||||
invitations (into #{}
|
||||
(comp
|
||||
;; We don't re-send inviation to
|
||||
;; already existing members
|
||||
(remove team-members)
|
||||
;; We don't send invitations to
|
||||
;; join-requested members
|
||||
(remove join-requests)
|
||||
(map (fn [email] (assoc params :email email)))
|
||||
(keep (partial create-invitation cfg)))
|
||||
emails)]
|
||||
|
||||
;; For requested invitations, do not send invitation emails, add
|
||||
;; the user directly to the team
|
||||
(->> (filter join-requests emails)
|
||||
(run! (partial add-user-to-team conn profile team role)))
|
||||
|
||||
invitations))
|
||||
|
||||
(def ^:private schema:create-team-invitations
|
||||
[:map {:title "create-team-invitations"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:role schema:role]
|
||||
[:emails [::sm/set ::sm/email]]])
|
||||
|
||||
(def ^:private max-invitations-by-request-threshold
|
||||
"The number of invitations can be sent in a single rpc request"
|
||||
25)
|
||||
|
||||
(sv/defmethod ::create-team-invitations
|
||||
"A rpc call that allow to send a single or multiple invitations to
|
||||
join the team."
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:create-team-invitations}
|
||||
[cfg {:keys [::rpc/profile-id team-id emails] :as params}]
|
||||
(let [perms (get-permissions cfg profile-id team-id)
|
||||
profile (db/get-by-id cfg :profile profile-id)
|
||||
emails (into #{} (map profile/clean-email) emails)]
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id emails role] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [perms (get-permissions conn profile-id team-id)
|
||||
profile (db/get-by-id conn :profile profile-id)
|
||||
team (db/get-by-id conn :team team-id)
|
||||
emails (into #{} (map profile/clean-email) emails)]
|
||||
|
||||
(when-not (:is-admin perms)
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
(run! (partial quotes/check-quote! conn)
|
||||
(list {::quotes/id ::quotes/invitations-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id (:id team)
|
||||
::quotes/incr (count emails)}
|
||||
{::quotes/id ::quotes/profiles-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id (:id team)
|
||||
::quotes/incr (count emails)}))
|
||||
|
||||
(when (> (count emails) max-invitations-by-request-threshold)
|
||||
(ex/raise :type :validation
|
||||
:code :max-invitations-by-request
|
||||
:hint "the maximum of invitation on single request is reached"
|
||||
:threshold max-invitations-by-request-threshold))
|
||||
(when-not (:is-admin perms)
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
|
||||
(-> cfg
|
||||
(assoc ::quotes/profile-id profile-id)
|
||||
(assoc ::quotes/team-id team-id)
|
||||
(assoc ::quotes/incr (count emails))
|
||||
(quotes/check! {::quotes/id ::quotes/invitations-per-team}
|
||||
{::quotes/id ::quotes/profiles-per-team}))
|
||||
;; Check if the current profile is allowed to send emails.
|
||||
(check-valid-email-muted conn profile)
|
||||
|
||||
;; Check if the current profile is allowed to send emails
|
||||
(check-profile-muted cfg profile)
|
||||
|
||||
(let [team (db/get-by-id cfg :team team-id)
|
||||
;; NOTE: Is important pass RPC method params down to the
|
||||
;; `create-team-invitations` because it uses the implicit
|
||||
;; RPC properties from params for fill necessary data on
|
||||
;; emiting an entry to the audit-log
|
||||
invitations (db/tx-run! cfg create-team-invitations
|
||||
(-> params
|
||||
(assoc :profile profile)
|
||||
(assoc :team team)
|
||||
(assoc :emails emails)))]
|
||||
(let [requested (into #{} (map :email) (get-valid-requests-email conn team-id))
|
||||
emails-to-add (filter #(contains? requested %) emails)
|
||||
emails (remove #(contains? requested %) emails)
|
||||
cfg (assoc cfg ::db/conn conn)
|
||||
members (->> (db/exec! conn [sql:team-members team-id])
|
||||
(into #{} (map :email)))
|
||||
|
||||
(with-meta {:total (count invitations)
|
||||
:invitations invitations}
|
||||
{::audit/props {:invitations (count invitations)}}))))
|
||||
invitations (into #{}
|
||||
(comp
|
||||
;; We don't re-send inviation to already existing members
|
||||
(remove (partial contains? members))
|
||||
(map (fn [email]
|
||||
(-> params
|
||||
(assoc :email email)
|
||||
(assoc :team team)
|
||||
(assoc :profile profile)
|
||||
(assoc :role role))))
|
||||
(keep (partial create-invitation cfg)))
|
||||
emails)]
|
||||
;; For requested invitations, do not send invitation emails, add the user directly to the team
|
||||
(doseq [email emails-to-add]
|
||||
(add-user-to-team conn profile team email role))
|
||||
|
||||
(with-meta {:total (count invitations)
|
||||
:invitations invitations}
|
||||
{::audit/props {:invitations (count invitations)}})))))
|
||||
|
||||
;; --- Mutation: Create Team & Invite Members
|
||||
|
||||
@@ -1029,50 +977,52 @@
|
||||
|
||||
(sv/defmethod ::create-team-with-invitations
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:create-team-with-invitations
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id emails role name] :as params}]
|
||||
(let [features (-> (cfeat/get-enabled-features cf/flags)
|
||||
(cfeat/check-client-features! (:features params)))
|
||||
::sm/params schema:create-team-with-invitations}
|
||||
[cfg {:keys [::rpc/profile-id emails role name] :as params}]
|
||||
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features features))
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(let [features (-> (cfeat/get-enabled-features cf/flags)
|
||||
(cfeat/check-client-features! (:features params)))
|
||||
|
||||
team (create-team cfg params)
|
||||
emails (into #{} (map profile/clean-email) emails)]
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features features))
|
||||
|
||||
(-> cfg
|
||||
(assoc ::quotes/profile-id profile-id)
|
||||
(assoc ::quotes/team-id (:id team))
|
||||
(assoc ::quotes/incr (count emails))
|
||||
(quotes/check! {::quotes/id ::quotes/teams-per-profile}
|
||||
{::quotes/id ::quotes/invitations-per-team}
|
||||
{::quotes/id ::quotes/profiles-per-team}))
|
||||
cfg (assoc cfg ::db/conn conn)
|
||||
team (create-team cfg params)
|
||||
profile (db/get-by-id conn :profile profile-id)
|
||||
emails (into #{} (map profile/clean-email) emails)]
|
||||
|
||||
(when (> (count emails) max-invitations-by-request-threshold)
|
||||
(ex/raise :type :validation
|
||||
:code :max-invitations-by-request
|
||||
:hint "the maximum of invitation on single request is reached"
|
||||
:threshold max-invitations-by-request-threshold))
|
||||
(let [props {:name name :features features}
|
||||
event (-> (audit/event-from-rpc-params params)
|
||||
(assoc ::audit/name "create-team")
|
||||
(assoc ::audit/props props))]
|
||||
(audit/submit! cfg event))
|
||||
|
||||
(let [props {:name name :features features}
|
||||
event (-> (audit/event-from-rpc-params params)
|
||||
(assoc ::audit/name "create-team")
|
||||
(assoc ::audit/props props))]
|
||||
(audit/submit! cfg event))
|
||||
;; Create invitations for all provided emails.
|
||||
(->> emails
|
||||
(map (fn [email]
|
||||
(-> params
|
||||
(assoc :team team)
|
||||
(assoc :profile profile)
|
||||
(assoc :email email)
|
||||
(assoc :role role))))
|
||||
(run! (partial create-invitation cfg)))
|
||||
|
||||
;; Create invitations for all provided emails.
|
||||
(let [profile (db/get-by-id conn :profile profile-id)
|
||||
params (-> params
|
||||
(assoc :team team)
|
||||
(assoc :profile profile)
|
||||
(assoc :role role))
|
||||
invitations (->> emails
|
||||
(map (fn [email] (assoc params :email email)))
|
||||
(map (partial create-invitation cfg)))]
|
||||
(run! (partial quotes/check-quote! conn)
|
||||
(list {::quotes/id ::quotes/teams-per-profile
|
||||
::quotes/profile-id profile-id}
|
||||
{::quotes/id ::quotes/invitations-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id (:id team)
|
||||
::quotes/incr (count emails)}
|
||||
{::quotes/id ::quotes/profiles-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id (:id team)
|
||||
::quotes/incr (count emails)}))
|
||||
|
||||
(vary-meta team assoc ::audit/props {:invitations (count invitations)}))))
|
||||
(vary-meta team assoc ::audit/props {:invitations (count emails)})))))
|
||||
|
||||
;; --- Query: get-team-invitation-token
|
||||
|
||||
@@ -1265,11 +1215,11 @@
|
||||
:code :invalid-parameters))
|
||||
|
||||
;; Check that the requester is not muted
|
||||
(check-profile-muted conn requester)
|
||||
(check-valid-email-muted conn requester)
|
||||
|
||||
;; Check that the owner is not marked as bounce nor spam
|
||||
(check-email-bounce conn (:email team-owner) false)
|
||||
(check-email-spam conn (:email team-owner) true)
|
||||
(check-valid-email-bounce conn (:email team-owner) false)
|
||||
(check-valid-email-spam conn (:email team-owner) true)
|
||||
|
||||
(let [request (create-team-access-request
|
||||
cfg {:team team :requester requester :team-owner team-owner :file file :is-viewer is-viewer})]
|
||||
|
||||
@@ -37,12 +37,14 @@
|
||||
::doc/added "1.15"
|
||||
::doc/module :auth
|
||||
::sm/params schema:verify-token}
|
||||
[cfg {:keys [token] :as params}]
|
||||
(let [claims (tokens/verify (::setup/props cfg) {:token token})]
|
||||
(db/tx-run! cfg process-token params claims)))
|
||||
[{:keys [::db/pool] :as cfg} {:keys [token] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [claims (tokens/verify (::setup/props cfg) {:token token})
|
||||
cfg (assoc cfg :conn conn)]
|
||||
(process-token cfg params claims))))
|
||||
|
||||
(defmethod process-token :change-email
|
||||
[{:keys [::db/conn] :as cfg} _params {:keys [profile-id email] :as claims}]
|
||||
[{:keys [conn] :as cfg} _params {:keys [profile-id email] :as claims}]
|
||||
(let [email (profile/clean-email email)]
|
||||
(when (profile/get-profile-by-email conn email)
|
||||
(ex/raise :type :validation
|
||||
@@ -58,7 +60,7 @@
|
||||
::audit/profile-id profile-id})))
|
||||
|
||||
(defmethod process-token :verify-email
|
||||
[{:keys [::db/conn] :as cfg} _ {:keys [profile-id] :as claims}]
|
||||
[{:keys [conn] :as cfg} _ {:keys [profile-id] :as claims}]
|
||||
(let [profile (profile/get-profile conn profile-id)
|
||||
claims (assoc claims :profile profile)]
|
||||
|
||||
@@ -79,14 +81,14 @@
|
||||
::audit/profile-id (:id profile)}))))
|
||||
|
||||
(defmethod process-token :auth
|
||||
[{:keys [::db/conn] :as cfg} _params {:keys [profile-id] :as claims}]
|
||||
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
|
||||
(let [profile (profile/get-profile conn profile-id)]
|
||||
(assoc claims :profile profile)))
|
||||
|
||||
;; --- Team Invitation
|
||||
|
||||
(defn- accept-invitation
|
||||
[{:keys [::db/conn] :as cfg} {:keys [team-id role member-email] :as claims} invitation member]
|
||||
[{:keys [conn] :as cfg} {:keys [team-id role member-email] :as claims} invitation member]
|
||||
(let [;; Update the role if there is an invitation
|
||||
role (or (some-> invitation :role keyword) role)
|
||||
params (merge
|
||||
@@ -99,9 +101,10 @@
|
||||
(ex/raise :type :restriction
|
||||
:code :profile-blocked))
|
||||
|
||||
(quotes/check! cfg {::quotes/id ::quotes/profiles-per-team
|
||||
::quotes/profile-id (:id member)
|
||||
::quotes/team-id team-id})
|
||||
(quotes/check-quote! conn
|
||||
{::quotes/id ::quotes/profiles-per-team
|
||||
::quotes/profile-id (:id member)
|
||||
::quotes/team-id team-id})
|
||||
|
||||
;; Insert the invited member to the team
|
||||
(db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true})
|
||||
@@ -137,7 +140,7 @@
|
||||
(sm/lazy-validator schema:team-invitation-claims))
|
||||
|
||||
(defmethod process-token :team-invitation
|
||||
[{:keys [::db/conn] :as cfg}
|
||||
[{:keys [conn] :as cfg}
|
||||
{:keys [::rpc/profile-id token] :as params}
|
||||
{:keys [member-id team-id member-email] :as claims}]
|
||||
|
||||
|
||||
@@ -7,13 +7,16 @@
|
||||
(ns app.rpc.quotes
|
||||
"Penpot resource usage quotes."
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defmulti check-quote ::id)
|
||||
@@ -31,9 +34,6 @@
|
||||
[::id :keyword]
|
||||
[::profile-id ::sm/uuid]])
|
||||
|
||||
(def valid-quote?
|
||||
(sm/lazy-validator schema:quote))
|
||||
|
||||
(def ^:private enabled (volatile! true))
|
||||
|
||||
(defn enable!
|
||||
@@ -46,31 +46,20 @@
|
||||
[]
|
||||
(vswap! enabled (constantly false)))
|
||||
|
||||
(defn- check
|
||||
[cfg quote]
|
||||
(let [quote (merge cfg quote)
|
||||
id (::id quote)]
|
||||
(defn check-quote!
|
||||
[ds quote]
|
||||
(dm/assert!
|
||||
"expected valid quote map"
|
||||
(sm/validate schema:quote quote))
|
||||
|
||||
(when-not (valid-quote? quote)
|
||||
(ex/raise :type :internal
|
||||
:code :invalid-quote-definition
|
||||
:hint "found invalid data for quote schema"
|
||||
:quote (name id)))
|
||||
|
||||
(-> (assoc quote ::target (name id))
|
||||
(check-quote))))
|
||||
|
||||
(defn check!
|
||||
([cfg]
|
||||
(when (contains? cf/flags :quotes)
|
||||
(when @enabled
|
||||
(db/run! cfg check {}))))
|
||||
|
||||
([cfg & others]
|
||||
(when (contains? cf/flags :quotes)
|
||||
(when @enabled
|
||||
(db/run! cfg (fn [cfg]
|
||||
(run! (partial check cfg) others)))))))
|
||||
(when (contains? cf/flags :quotes)
|
||||
(when @enabled
|
||||
;; This approach add flexibility on how and where the
|
||||
;; check-quote! can be called (in or out of transaction)
|
||||
(db/run! ds (fn [cfg]
|
||||
(-> (merge cfg quote)
|
||||
(assoc ::target (name (::id quote)))
|
||||
(check-quote)))))))
|
||||
|
||||
(defn- send-notification!
|
||||
[{:keys [::db/conn] :as params}]
|
||||
@@ -111,7 +100,7 @@
|
||||
(map :quote)
|
||||
(reduce max (- Integer/MAX_VALUE)))
|
||||
quote (if (pos? quote) quote default)
|
||||
total (:total (db/exec-one! conn count-sql))]
|
||||
total (->> (db/exec! conn count-sql) first :total)]
|
||||
|
||||
(when (> (+ total incr) quote)
|
||||
(if (contains? cf/flags :soft-quotes)
|
||||
@@ -123,81 +112,72 @@
|
||||
:count total)))))
|
||||
|
||||
(def ^:private sql:get-quotes-1
|
||||
"SELECT id, quote
|
||||
FROM usage_quote
|
||||
WHERE target = ?
|
||||
AND profile_id = ?
|
||||
AND team_id IS NULL
|
||||
AND project_id IS NULL
|
||||
AND file_id IS NULL;")
|
||||
"select id, quote from usage_quote
|
||||
where target = ?
|
||||
and profile_id = ?
|
||||
and team_id is null
|
||||
and project_id is null
|
||||
and file_id is null;")
|
||||
|
||||
(def ^:private sql:get-quotes-2
|
||||
"SELECT id, quote
|
||||
FROM usage_quote
|
||||
WHERE target = ?
|
||||
AND ((team_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
|
||||
(profile_id = ? AND team_id IS NULL AND project_id IS NULL AND file_id IS NULL));")
|
||||
"select id, quote from usage_quote
|
||||
where target = ?
|
||||
and ((team_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
|
||||
|
||||
(def ^:private sql:get-quotes-3
|
||||
"SELECT id, quote
|
||||
FROM usage_quote
|
||||
WHERE target = ?
|
||||
AND ((project_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
|
||||
(team_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
|
||||
(profile_id = ? AND team_id IS NULL AND project_id IS NULL AND file_id IS NULL));")
|
||||
"select id, quote from usage_quote
|
||||
where target = ?
|
||||
and ((project_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(team_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
|
||||
|
||||
(def ^:private sql:get-quotes-4
|
||||
"SELECT id, quote
|
||||
FROM usage_quote
|
||||
WHERE target = ?
|
||||
AND ((file_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
|
||||
(project_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
|
||||
(team_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
|
||||
(profile_id = ? AND team_id IS NULL AND project_id IS NULL AND file_id IS NULL));")
|
||||
"select id, quote from usage_quote
|
||||
where target = ?
|
||||
and ((file_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(project_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(team_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: TEAMS-PER-PROFILE
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private schema:teams-per-profile
|
||||
[:map [::profile-id ::sm/uuid]])
|
||||
|
||||
(def ^:private valid-teams-per-profile-quote?
|
||||
(sm/lazy-validator schema:teams-per-profile))
|
||||
|
||||
(def ^:private sql:get-teams-per-profile
|
||||
"SELECT count(*) AS total
|
||||
FROM team_profile_rel
|
||||
WHERE profile_id = ?")
|
||||
"select count(*) as total
|
||||
from team_profile_rel
|
||||
where profile_id = ?")
|
||||
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::teams-per-profile
|
||||
(s/keys :req [::profile-id ::target]))
|
||||
|
||||
(defmethod check-quote ::teams-per-profile
|
||||
[{:keys [::profile-id ::target] :as quote}]
|
||||
(assert (valid-teams-per-profile-quote? quote) "invalid quote parameters")
|
||||
(us/assert! ::teams-per-profile quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-teams-per-profile Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-1 target profile-id])
|
||||
(assoc ::count-sql [sql:get-teams-per-profile profile-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: ACCESS-TOKENS-PER-PROFILE
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private schema:access-tokens-per-profile
|
||||
[:map [::profile-id ::sm/uuid]])
|
||||
|
||||
(def ^:private valid-access-tokens-per-profile-quote?
|
||||
(sm/lazy-validator schema:access-tokens-per-profile))
|
||||
|
||||
(def ^:private sql:get-access-tokens-per-profile
|
||||
"SELECT count(*) AS total
|
||||
FROM access_token
|
||||
WHERE profile_id = ?")
|
||||
"select count(*) as total
|
||||
from access_token
|
||||
where profile_id = ?")
|
||||
|
||||
(s/def ::access-tokens-per-profile
|
||||
(s/keys :req [::profile-id ::target]))
|
||||
|
||||
(defmethod check-quote ::access-tokens-per-profile
|
||||
[{:keys [::profile-id ::target] :as quote}]
|
||||
(assert (valid-access-tokens-per-profile-quote? quote) "invalid quote parameters")
|
||||
|
||||
(us/assert! ::access-tokens-per-profile quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-access-tokens-per-profile Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-1 target profile-id])
|
||||
@@ -208,51 +188,40 @@
|
||||
;; QUOTE: PROJECTS-PER-TEAM
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private schema:projects-per-team
|
||||
[:map
|
||||
[::profile-id ::sm/uuid]
|
||||
[::team-id ::sm/uuid]])
|
||||
|
||||
(def ^:private valid-projects-per-team-quote?
|
||||
(sm/lazy-validator schema:projects-per-team))
|
||||
|
||||
(def ^:private sql:get-projects-per-team
|
||||
"SELECT count(*) AS total
|
||||
FROM project AS p
|
||||
WHERE p.team_id = ?
|
||||
AND p.deleted_at IS NULL")
|
||||
"select count(*) as total
|
||||
from project as p
|
||||
where p.team_id = ?
|
||||
and p.deleted_at is null")
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::projects-per-team
|
||||
(s/keys :req [::profile-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::projects-per-team
|
||||
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
||||
(assert (valid-projects-per-team-quote? quote) "invalid quote parameters")
|
||||
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-projects-per-team Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
|
||||
(assoc ::count-sql [sql:get-projects-per-team team-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: FONT-VARIANTS-PER-TEAM
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private schema:font-variants-per-team
|
||||
[:map
|
||||
[::profile-id ::sm/uuid]
|
||||
[::team-id ::sm/uuid]])
|
||||
|
||||
(def ^:private valid-font-variant-per-team-quote?
|
||||
(sm/lazy-validator schema:font-variants-per-team))
|
||||
|
||||
(def ^:private sql:get-font-variants-per-team
|
||||
"SELECT count(*) AS total
|
||||
FROM team_font_variant AS v
|
||||
WHERE v.team_id = ?")
|
||||
"select count(*) as total
|
||||
from team_font_variant as v
|
||||
where v.team_id = ?")
|
||||
|
||||
(s/def ::font-variants-per-team
|
||||
(s/keys :req [::profile-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::font-variants-per-team
|
||||
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
||||
(assert (valid-font-variant-per-team-quote? quote) "invalid quote parameters")
|
||||
|
||||
(us/assert! ::font-variants-per-team quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-font-variants-per-team Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
|
||||
@@ -264,86 +233,70 @@
|
||||
;; QUOTE: INVITATIONS-PER-TEAM
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private schema:invitations-per-team
|
||||
[:map
|
||||
[::profile-id ::sm/uuid]
|
||||
[::team-id ::sm/uuid]])
|
||||
|
||||
(def ^:private valid-invitations-per-team-quote?
|
||||
(sm/lazy-validator schema:invitations-per-team))
|
||||
|
||||
(def ^:private sql:get-invitations-per-team
|
||||
"SELECT count(*) AS total
|
||||
FROM team_invitation
|
||||
WHERE team_id = ?")
|
||||
"select count(*) as total
|
||||
from team_invitation
|
||||
where team_id = ?")
|
||||
|
||||
(s/def ::invitations-per-team
|
||||
(s/keys :req [::profile-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::invitations-per-team
|
||||
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
||||
(assert (valid-invitations-per-team-quote? quote) "invalid quote parameters")
|
||||
|
||||
(us/assert! ::invitations-per-team quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-invitations-per-team Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
|
||||
(assoc ::count-sql [sql:get-invitations-per-team team-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: PROFILES-PER-TEAM
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private schema:profiles-per-team
|
||||
[:map
|
||||
[::profile-id ::sm/uuid]
|
||||
[::team-id ::sm/uuid]])
|
||||
|
||||
(def ^:private valid-profiles-per-team-quote?
|
||||
(sm/lazy-validator schema:profiles-per-team))
|
||||
|
||||
(def ^:private sql:get-profiles-per-team
|
||||
"SELECT (SELECT count(*)
|
||||
FROM team_profile_rel
|
||||
WHERE team_id = ?) +
|
||||
(SELECT count(*)
|
||||
FROM team_invitation
|
||||
WHERE team_id = ?
|
||||
AND valid_until > now()) AS total;")
|
||||
"select (select count(*)
|
||||
from team_profile_rel
|
||||
where team_id = ?) +
|
||||
(select count(*)
|
||||
from team_invitation
|
||||
where team_id = ?
|
||||
and valid_until > now()) as total;")
|
||||
|
||||
;; NOTE: the total number of profiles is determined by the number of
|
||||
;; effective members plus ongoing valid invitations.
|
||||
|
||||
(s/def ::profiles-per-team
|
||||
(s/keys :req [::profile-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::profiles-per-team
|
||||
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
||||
(assert (valid-profiles-per-team-quote? quote) "invalid quote parameters")
|
||||
|
||||
(us/assert! ::profiles-per-team quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-profiles-per-team Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
|
||||
(assoc ::count-sql [sql:get-profiles-per-team team-id team-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: FILES-PER-PROJECT
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private schema:files-per-project
|
||||
[:map
|
||||
[::profile-id ::sm/uuid]
|
||||
[::project-id ::sm/uuid]
|
||||
[::team-id ::sm/uuid]])
|
||||
|
||||
(def ^:private valid-files-per-project-quote?
|
||||
(sm/lazy-validator schema:files-per-project))
|
||||
|
||||
(def ^:private sql:get-files-per-project
|
||||
"SELECT count(*) AS total
|
||||
FROM file AS f
|
||||
WHERE f.project_id = ?
|
||||
AND f.deleted_at IS NULL")
|
||||
"select count(*) as total
|
||||
from file as f
|
||||
where f.project_id = ?
|
||||
and f.deleted_at is null")
|
||||
|
||||
(s/def ::project-id ::us/uuid)
|
||||
(s/def ::files-per-project
|
||||
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::files-per-project
|
||||
[{:keys [::profile-id ::project-id ::team-id ::target] :as quote}]
|
||||
(assert (valid-files-per-project-quote? quote) "invalid quote parameters")
|
||||
|
||||
(us/assert! ::files-per-project quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-files-per-project Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-3 target project-id profile-id team-id profile-id profile-id])
|
||||
@@ -354,24 +307,17 @@
|
||||
;; QUOTE: COMMENT-THREADS-PER-FILE
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private schema:comment-threads-per-file
|
||||
[:map
|
||||
[::profile-id ::sm/uuid]
|
||||
[::project-id ::sm/uuid]
|
||||
[::team-id ::sm/uuid]])
|
||||
|
||||
(def ^:private valid-comment-threads-per-file-quote?
|
||||
(sm/lazy-validator schema:comment-threads-per-file))
|
||||
|
||||
(def ^:private sql:get-comment-threads-per-file
|
||||
"SELECT count(*) AS total
|
||||
FROM comment_thread AS ct
|
||||
WHERE ct.file_id = ?")
|
||||
"select count(*) as total
|
||||
from comment_thread as ct
|
||||
where ct.file_id = ?")
|
||||
|
||||
(s/def ::comment-threads-per-file
|
||||
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::comment-threads-per-file
|
||||
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}]
|
||||
(assert (valid-comment-threads-per-file-quote? quote) "invalid quote parameters")
|
||||
|
||||
(us/assert! ::files-per-project quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-comment-threads-per-file Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id
|
||||
@@ -379,28 +325,23 @@
|
||||
(assoc ::count-sql [sql:get-comment-threads-per-file file-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: COMMENTS-PER-FILE
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private schema:comments-per-file
|
||||
[:map
|
||||
[::profile-id ::sm/uuid]
|
||||
[::project-id ::sm/uuid]
|
||||
[::team-id ::sm/uuid]])
|
||||
|
||||
(def ^:private valid-comments-per-file-quote?
|
||||
(sm/lazy-validator schema:comments-per-file))
|
||||
|
||||
(def ^:private sql:get-comments-per-file
|
||||
"SELECT count(*) AS total
|
||||
FROM comment AS c
|
||||
JOIN comment_thread AS ct ON (ct.id = c.thread_id)
|
||||
WHERE ct.file_id = ?")
|
||||
"select count(*) as total
|
||||
from comment as c
|
||||
join comment_thread as ct on (ct.id = c.thread_id)
|
||||
where ct.file_id = ?")
|
||||
|
||||
(s/def ::comments-per-file
|
||||
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::comments-per-file
|
||||
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}]
|
||||
(assert (valid-comments-per-file-quote? quote) "invalid quote parameters")
|
||||
(us/assert! ::files-per-project quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-comments-per-file Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.http.client :as http]
|
||||
@@ -20,26 +19,26 @@
|
||||
[datoteka.fs :as fs]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(def ^:private schema:template
|
||||
(def ^:private
|
||||
schema:template
|
||||
[:map {:title "Template"}
|
||||
[:id ::sm/word-string]
|
||||
[:name ::sm/word-string]
|
||||
[:file-uri ::sm/word-string]])
|
||||
|
||||
(def ^:private schema:templates
|
||||
(def ^:private
|
||||
schema:templates
|
||||
[:vector schema:template])
|
||||
|
||||
(def check-templates!
|
||||
(sm/check-fn schema:templates
|
||||
:code :invalid-templates
|
||||
:hint "invalid templates"))
|
||||
|
||||
(defmethod ig/init-key ::setup/templates
|
||||
[_ _]
|
||||
(let [templates (-> "app/onboarding.edn" io/resource slurp edn/read-string)
|
||||
templates (check-templates! templates)
|
||||
dest (fs/join fs/*cwd* "builtin-templates")]
|
||||
|
||||
(dm/verify!
|
||||
"expected a valid templates file"
|
||||
(sm/check! schema:templates templates))
|
||||
|
||||
(doseq [{:keys [id path] :as template} templates]
|
||||
(let [path (or path (fs/join dest id))]
|
||||
(if (fs/exists? path)
|
||||
@@ -59,9 +58,9 @@
|
||||
(let [resp (http/req! cfg
|
||||
{:method :get :uri (:file-uri template)}
|
||||
{:response-type :input-stream :sync? true})]
|
||||
(when-not (= 200 (:status resp))
|
||||
(ex/raise :type :internal
|
||||
:code :unexpected-status-code
|
||||
:hint (str "unable to download template, recevied status " (:status resp))))
|
||||
|
||||
(dm/verify!
|
||||
"unexpected response found on fetching template"
|
||||
(= 200 (:status resp)))
|
||||
|
||||
(io/input-stream (:body resp)))))))
|
||||
|
||||
@@ -155,10 +155,9 @@
|
||||
|
||||
(defn enable-team-feature!
|
||||
[team-id feature]
|
||||
(when-not (contains? cfeat/supported-features feature)
|
||||
(ex/raise :type :assertion
|
||||
:code :feature-not-supported
|
||||
:hint (str "feature '" feature "' not supported")))
|
||||
(dm/verify!
|
||||
"feature should be supported"
|
||||
(contains? cfeat/supported-features feature))
|
||||
|
||||
(let [team-id (h/parse-uuid team-id)]
|
||||
(db/tx-run! main/system
|
||||
@@ -174,11 +173,9 @@
|
||||
|
||||
(defn disable-team-feature!
|
||||
[team-id feature]
|
||||
|
||||
(when-not (contains? cfeat/supported-features feature)
|
||||
(ex/raise :type :assertion
|
||||
:code :feature-not-supported
|
||||
:hint (str "feature '" feature "' not supported")))
|
||||
(dm/verify!
|
||||
"feature should be supported"
|
||||
(contains? cfeat/supported-features feature))
|
||||
|
||||
(let [team-id (h/parse-uuid team-id)]
|
||||
(db/tx-run! main/system
|
||||
@@ -206,11 +203,9 @@
|
||||
[{:keys [::mbus/msgbus ::db/pool]} & {:keys [dest code message level]
|
||||
:or {code :generic level :info}
|
||||
:as params}]
|
||||
|
||||
(when-not (contains? #{:success :error :info :warning} level)
|
||||
(ex/raise :type :assertion
|
||||
:code :incorrect-level
|
||||
:hint (str "level '" level "' not supported")))
|
||||
(dm/verify!
|
||||
["invalid level %" level]
|
||||
(contains? #{:success :error :info :warning} level))
|
||||
|
||||
(letfn [(send [dest]
|
||||
(l/inf :hint "sending notification" :dest (str dest))
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
(t/deftest ttf-font-upload-1
|
||||
(with-mocks [mock {:target 'app.rpc.quotes/check! :return nil}]
|
||||
(with-mocks [mock {:target 'app.rpc.quotes/check-quote! :return nil}]
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team-id (:default-team-id prof)
|
||||
proj-id (:default-project-id prof)
|
||||
|
||||
@@ -108,6 +108,14 @@
|
||||
`(do ~@body)
|
||||
(reverse (partition 2 bindings))))
|
||||
|
||||
(defmacro check
|
||||
"Applies a predicate to the value, if result is true, return the
|
||||
value if not, returns nil."
|
||||
[pred-fn value]
|
||||
`(if (~pred-fn ~value)
|
||||
~value
|
||||
nil))
|
||||
|
||||
(defmacro get-prop
|
||||
"A macro based, optimized variant of `get` that access the property
|
||||
directly on CLJS, on CLJ works as get."
|
||||
@@ -116,32 +124,47 @@
|
||||
(list 'js* (c/str "(~{}?." (str/snake prop) "?? ~{})") obj (list 'cljs.core/get obj prop))
|
||||
(list `c/get obj prop)))
|
||||
|
||||
(defn runtime-assert
|
||||
[hint f]
|
||||
(try
|
||||
(when-not (f)
|
||||
(throw (ex-info hint {:type :assertion
|
||||
:code :expr-validation
|
||||
:hint hint})))
|
||||
(catch #?(:clj Throwable :cljs :default) cause
|
||||
(let [data (-> (ex-data cause)
|
||||
(assoc :type :assertion)
|
||||
(assoc :code :expr-validation)
|
||||
(assoc :hint hint))]
|
||||
(throw (ex-info hint data cause))))))
|
||||
(def ^:dynamic *assert-context* nil)
|
||||
|
||||
(defmacro assert!
|
||||
([expr]
|
||||
`(assert! nil ~expr))
|
||||
([hint expr]
|
||||
(let [hint (cond
|
||||
(vector? hint)
|
||||
`(str/ffmt ~@hint)
|
||||
(let [hint (cond
|
||||
(vector? hint)
|
||||
`(str/ffmt ~@hint)
|
||||
|
||||
(some? hint)
|
||||
hint
|
||||
(some? hint)
|
||||
hint
|
||||
|
||||
:else
|
||||
(str "expr assert: " (pr-str expr)))]
|
||||
:else
|
||||
(str "expr assert: " (pr-str expr)))]
|
||||
(when *assert*
|
||||
`(runtime-assert ~hint (fn [] ~expr))))))
|
||||
`(binding [*assert-context* ~hint]
|
||||
(when-not ~expr
|
||||
(let [hint# ~hint
|
||||
params# {:type :assertion
|
||||
:code :expr-validation
|
||||
:hint hint#}]
|
||||
(throw (ex-info hint# params#)))))))))
|
||||
|
||||
(defmacro verify!
|
||||
([expr]
|
||||
`(verify! nil ~expr))
|
||||
([hint expr]
|
||||
(let [hint (cond
|
||||
(vector? hint)
|
||||
`(str/ffmt ~@hint)
|
||||
|
||||
(some? hint)
|
||||
hint
|
||||
|
||||
:else
|
||||
(str "expr assert: " (pr-str expr)))]
|
||||
`(binding [*assert-context* ~hint]
|
||||
(when-not ~expr
|
||||
(let [hint# ~hint
|
||||
params# {:type :assertion
|
||||
:code :expr-validation
|
||||
:hint hint#}]
|
||||
(throw (ex-info hint# params#))))))))
|
||||
|
||||
@@ -49,8 +49,7 @@
|
||||
"components/v2"
|
||||
"styles/v2"
|
||||
"layout/grid"
|
||||
"plugins/runtime"
|
||||
"text-editor/v2"})
|
||||
"plugins/runtime"})
|
||||
|
||||
;; A set of features enabled by default
|
||||
(def default-features
|
||||
@@ -65,8 +64,7 @@
|
||||
;; team feature field
|
||||
(def frontend-only-features
|
||||
#{"styles/v2"
|
||||
"plugins/runtime"
|
||||
"text-editor/v2"})
|
||||
"plugins/runtime"})
|
||||
|
||||
;; Features that are mainly backend only or there are a proper
|
||||
;; fallback when frontend reports no support for it
|
||||
@@ -83,8 +81,7 @@
|
||||
"fdata/pointer-map"
|
||||
"layout/grid"
|
||||
"fdata/shape-data-type"
|
||||
"plugins/runtime"
|
||||
"text-editor/v2"}
|
||||
"plugins/runtime"}
|
||||
(into frontend-only-features)))
|
||||
|
||||
(sm/register! ::features
|
||||
@@ -104,7 +101,6 @@
|
||||
:feature-fdata-objects-map "fdata/objects-map"
|
||||
:feature-fdata-pointer-map "fdata/pointer-map"
|
||||
:feature-plugins "plugins/runtime"
|
||||
:feature-text-editor-v2 "text-editor/v2"
|
||||
nil))
|
||||
|
||||
(defn migrate-legacy-features
|
||||
|
||||
@@ -414,11 +414,10 @@
|
||||
;; If object has changed or is new verify is correct
|
||||
(when (and (some? shape-new)
|
||||
(not= shape-old shape-new))
|
||||
(when-not (and (cts/valid-shape? shape-new)
|
||||
(cts/shape? shape-new))
|
||||
(ex/raise :type :assertion
|
||||
:code :data-validation
|
||||
:hint "invalid shape found after applying changes")))))]
|
||||
(dm/verify!
|
||||
"expected valid shape"
|
||||
(and (cts/valid-shape? shape-new)
|
||||
(cts/shape? shape-new))))))]
|
||||
|
||||
(->> (into #{} (map :page-id) items)
|
||||
(mapcat (fn [page-id]
|
||||
|
||||
@@ -1056,14 +1056,9 @@
|
||||
(not (contains? page :default-grids)))
|
||||
(assoc :default-grids (:saved-grids options))
|
||||
|
||||
(and (some? (:background options))
|
||||
(not (contains? page :background)))
|
||||
(assoc :background (:background options))
|
||||
|
||||
(and (some? (:flows options))
|
||||
(or (not (contains? page :flows))
|
||||
(not (map? (:flows page)))))
|
||||
(assoc :flows (d/index-by :id (:flows options)))
|
||||
(not (contains? page :flows)))
|
||||
(assoc :flows (:flows options))
|
||||
|
||||
(and (some? (:guides options))
|
||||
(not (contains? page :guides)))
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]))
|
||||
|
||||
(defn shape-stroke-margin
|
||||
@@ -61,7 +60,6 @@
|
||||
filter-y (mth/min y (+ y offset-y (- spread) (- blur) -5))
|
||||
filter-w (+ w (mth/abs offset-x) (* spread 2) (* blur 2) 10)
|
||||
filter-h (+ h (mth/abs offset-y) (* spread 2) (* blur 2) 10)]
|
||||
|
||||
(grc/make-rect filter-x filter-y filter-w filter-h)))
|
||||
|
||||
(defn get-rect-filter-bounds
|
||||
@@ -98,15 +96,12 @@
|
||||
([shape ignore-margin?]
|
||||
(let [strokes (:strokes shape)
|
||||
|
||||
open-path? (and ^boolean (cfh/path-shape? shape)
|
||||
^boolean (gsh/open-path? shape))
|
||||
|
||||
stroke-width
|
||||
(->> strokes
|
||||
(map #(case (get % :stroke-alignment :center)
|
||||
:center (/ (:stroke-width % 0) 2)
|
||||
:outer (:stroke-width % 0)
|
||||
(if open-path? (:stroke-width % 0) 0)))
|
||||
0))
|
||||
(reduce d/max 0))
|
||||
|
||||
stroke-margin
|
||||
|
||||
@@ -852,10 +852,8 @@
|
||||
|
||||
(defn ray-overlaps?
|
||||
[ray-point {selrect :selrect}]
|
||||
(and (or (> (:y ray-point) (:y1 selrect))
|
||||
(mth/almost-zero? (- (:y ray-point) (:y1 selrect))))
|
||||
(or (< (:y ray-point) (:y2 selrect))
|
||||
(mth/almost-zero? (- (:y ray-point) (:y2 selrect))))))
|
||||
(and (>= (:y ray-point) (:y1 selrect))
|
||||
(<= (:y ray-point) (:y2 selrect))))
|
||||
|
||||
(defn content->geom-data
|
||||
[content]
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
;; All parents of any deleted shape must be resized.
|
||||
(into res (cfh/get-parent-ids objects id)))
|
||||
(d/ordered-set)
|
||||
(concat ids-to-delete ids-to-hide))
|
||||
ids-to-delete)
|
||||
|
||||
all-children
|
||||
(->> ids-to-delete ;; Children of deleted shapes must be also deleted.
|
||||
@@ -408,12 +408,17 @@
|
||||
;; Resize parent containers that need to
|
||||
(pcb/resize-parents parents))))
|
||||
|
||||
|
||||
|
||||
|
||||
(defn change-show-in-viewer [shape hide?]
|
||||
(assoc shape :hide-in-viewer hide?))
|
||||
(cond-> (assoc shape :hide-in-viewer hide?)
|
||||
;; When a frame is no longer shown in view mode, it cannot have interactions
|
||||
hide?
|
||||
(dissoc :interactions)))
|
||||
|
||||
(defn add-new-interaction [shape interaction]
|
||||
(-> shape
|
||||
(update :interactions ctsi/add-interaction interaction)))
|
||||
|
||||
(defn show-in-viewer [shape]
|
||||
(dissoc shape :hide-in-viewer))
|
||||
(update :interactions ctsi/add-interaction interaction)
|
||||
;; When a interaction is created, the frame must be shown in view mode
|
||||
(dissoc :hide-in-viewer)))
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#?(:cljs (:require-macros [app.common.schema :refer [ignoring]]))
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.schema.openapi :as-alias oapi]
|
||||
@@ -242,35 +243,59 @@
|
||||
(defn- fast-check!
|
||||
"A fast path for checking process, assumes the ILazySchema protocol
|
||||
implemented on the provided `s` schema. Sould not be used directly."
|
||||
[s type code hint value]
|
||||
[s value]
|
||||
(when-not ^boolean (-validate s value)
|
||||
(let [explain (-explain s value)]
|
||||
(throw (ex-info hint {:type type
|
||||
:code code
|
||||
(let [hint (d/nilv dm/*assert-context* "check error")
|
||||
explain (-explain s value)]
|
||||
(throw (ex-info hint {:type :assertion
|
||||
:code :data-validation
|
||||
:hint hint
|
||||
::explain explain}))))
|
||||
value)
|
||||
true)
|
||||
|
||||
(declare ^:private lazy-schema)
|
||||
|
||||
(defn check-fn
|
||||
"Create a predefined check function"
|
||||
[s & {:keys [hint type code]}]
|
||||
(let [schema (if (lazy-schema? s) s (lazy-schema s))
|
||||
hint (or ^boolean hint "check error")
|
||||
type (or ^boolean type :assertion)
|
||||
code (or ^boolean code :data-validation)]
|
||||
(partial fast-check! schema type code hint)))
|
||||
[s]
|
||||
(let [schema (if (lazy-schema? s) s (lazy-schema s))]
|
||||
(partial fast-check! schema)))
|
||||
|
||||
(defn check!
|
||||
"A helper intended to be used on assertions for validate/check the
|
||||
schema over provided data. Raises an assertion exception."
|
||||
[s value & {:keys [hint type code]}]
|
||||
(let [s (if (lazy-schema? s) s (lazy-schema s))
|
||||
hint (or ^boolean hint "check error")
|
||||
type (or ^boolean type :assertion)
|
||||
code (or ^boolean code :data-validation)]
|
||||
(fast-check! s type code hint value)))
|
||||
schema over provided data. Raises an assertion exception, should be
|
||||
used together with `dm/assert!` or `dm/verify!`."
|
||||
[s value]
|
||||
(let [s (if (lazy-schema? s) s (lazy-schema s))]
|
||||
(fast-check! s value)))
|
||||
|
||||
(defn- fast-validate!
|
||||
"A fast path for validation process, assumes the ILazySchema protocol
|
||||
implemented on the provided `s` schema. Sould not be used directly."
|
||||
([s value] (fast-validate! s value nil))
|
||||
([s value options]
|
||||
(when-not ^boolean (-validate s value)
|
||||
(let [explain (-explain s value)
|
||||
options (into {:type :validation
|
||||
:code :data-validation
|
||||
::explain explain}
|
||||
options)
|
||||
hint (get options :hint "schema validation error")]
|
||||
(throw (ex-info hint options))))
|
||||
value))
|
||||
|
||||
(defn validate-fn
|
||||
"Create a predefined validate function that raises an expception"
|
||||
[s]
|
||||
(let [schema (if (lazy-schema? s) s (lazy-schema s))]
|
||||
(partial fast-validate! schema)))
|
||||
|
||||
(defn validate!
|
||||
"A generic validation function for predefined schemas."
|
||||
([s value] (validate! s value nil))
|
||||
([s value options]
|
||||
(let [s (if (lazy-schema? s) s (lazy-schema s))]
|
||||
(fast-validate! s value options))))
|
||||
|
||||
(defn register! [type s]
|
||||
(let [s (if (map? s) (m/-simple-schema s) s)]
|
||||
@@ -366,7 +391,7 @@
|
||||
(defn parse-email
|
||||
[s]
|
||||
(if (string? s)
|
||||
(first (re-seq email-re s))
|
||||
(re-matches email-re s)
|
||||
nil))
|
||||
|
||||
(defn email-string?
|
||||
@@ -980,12 +1005,6 @@
|
||||
(def check-email!
|
||||
(check-fn ::email))
|
||||
|
||||
(def check-uuid!
|
||||
(check-fn ::uuid :hint "expected valid uuid instance"))
|
||||
|
||||
(def check-string!
|
||||
(check-fn :string :hint "expected string"))
|
||||
|
||||
(def check-coll-of-uuid!
|
||||
(check-fn ::coll-of-uuid))
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
[app.common.colors :as clr]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
@@ -30,12 +29,12 @@
|
||||
{:x 0 :y 0 :width 1 :height 1})
|
||||
|
||||
(defn- assert-valid-num [attr num]
|
||||
(when-not (and (d/num? num)
|
||||
(<= num max-safe-int)
|
||||
(>= num min-safe-int))
|
||||
(ex/raise :type :assertion
|
||||
:code :data-validation
|
||||
:hint (str "invalid numeric value for `" attr "`: " num)))
|
||||
(dm/verify!
|
||||
["%1 attribute has invalid value: %2" (d/name attr) num]
|
||||
(and (d/num? num)
|
||||
(<= num max-safe-int)
|
||||
(>= num min-safe-int)))
|
||||
|
||||
(cond
|
||||
(and (> num 0) (< num 1)) 1
|
||||
(and (< num 0) (> num -1)) -1
|
||||
@@ -44,21 +43,19 @@
|
||||
(defn- assert-valid-pos-num
|
||||
[attr num]
|
||||
|
||||
(when-not (pos? num)
|
||||
(ex/raise :type :assertion
|
||||
:code :data-validation
|
||||
:hint (str "invalid numeric value for `" attr "`: " num " (should be positive)")))
|
||||
(dm/verify!
|
||||
["%1 attribute should be positive" (d/name attr)]
|
||||
(pos? num))
|
||||
|
||||
num)
|
||||
|
||||
(defn- assert-valid-blend-mode
|
||||
[mode]
|
||||
(let [value (-> mode str/trim str/lower keyword)]
|
||||
|
||||
(when-not (contains? cts/blend-modes value)
|
||||
(ex/raise :type :assertion
|
||||
:code :data-validation
|
||||
:hint (str "unexpected blend mode: " value)))
|
||||
value))
|
||||
(let [clean-value (-> mode str/trim str/lower keyword)]
|
||||
(dm/verify!
|
||||
["%1 is not a valid blend mode" clean-value]
|
||||
(contains? cts/blend-modes clean-value))
|
||||
clean-value))
|
||||
|
||||
(defn- svg-dimensions
|
||||
[{:keys [attrs] :as data}]
|
||||
|
||||
@@ -78,12 +78,6 @@
|
||||
|
||||
(def text-all-attrs (d/concat-set shape-attrs root-attrs paragraph-attrs text-node-attrs))
|
||||
|
||||
(def text-style-attrs
|
||||
(d/concat-vec root-attrs paragraph-attrs text-node-attrs))
|
||||
|
||||
(def default-root-attrs
|
||||
{:vertical-align "top"})
|
||||
|
||||
(def default-text-attrs
|
||||
{:typography-ref-file nil
|
||||
:typography-ref-id nil
|
||||
@@ -98,13 +92,9 @@
|
||||
:text-transform "none"
|
||||
:text-align "left"
|
||||
:text-decoration "none"
|
||||
:text-direction "ltr"
|
||||
:fills [{:fill-color clr/black
|
||||
:fill-opacity 1}]})
|
||||
|
||||
(def default-attrs
|
||||
(merge default-root-attrs default-text-attrs))
|
||||
|
||||
(def typography-fields
|
||||
[:font-id
|
||||
:font-family
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
(sm/register! ::color-attrs schema:color-attrs)
|
||||
|
||||
(def check-color!
|
||||
(sm/check-fn schema:color :hint "expected valid color struct"))
|
||||
(sm/check-fn schema:color))
|
||||
|
||||
(def check-recent-color!
|
||||
(sm/check-fn schema:recent-color))
|
||||
|
||||
@@ -355,8 +355,7 @@
|
||||
(sm/check-fn schema:shape-attrs))
|
||||
|
||||
(def check-shape!
|
||||
(sm/check-fn schema:shape
|
||||
:hint "expected valid shape"))
|
||||
(sm/check-fn schema:shape))
|
||||
|
||||
(def valid-shape?
|
||||
(sm/lazy-validator schema:shape))
|
||||
|
||||
75
common/test/common_tests/logic/hide_in_viewer_test.cljc
Normal file
75
common/test/common_tests/logic/hide_in_viewer_test.cljc
Normal file
@@ -0,0 +1,75 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns common-tests.logic.hide-in-viewer-test
|
||||
(:require
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.logic.shapes :as cls]
|
||||
[app.common.test-helpers.compositions :as tho]
|
||||
[app.common.test-helpers.files :as thf]
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.test-helpers.shapes :as ths]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :each thi/test-fixture)
|
||||
|
||||
|
||||
(t/deftest test-remove-show-in-view-mode-delete-interactions
|
||||
(let [;; ==== Setup
|
||||
|
||||
file (-> (thf/sample-file :file1)
|
||||
(tho/add-frame :frame-dest)
|
||||
(tho/add-frame :frame-origin)
|
||||
(ths/add-interaction :frame-origin :frame-dest))
|
||||
|
||||
frame-origin (ths/get-shape file :frame-origin)
|
||||
|
||||
page (thf/current-page file)
|
||||
|
||||
|
||||
;; ==== Action
|
||||
changes (-> (pcb/empty-changes nil (:id page))
|
||||
(pcb/with-objects (:objects page))
|
||||
(pcb/update-shapes [(:id frame-origin)] #(cls/change-show-in-viewer % true)))
|
||||
file' (thf/apply-changes file changes)
|
||||
|
||||
;; ==== Get
|
||||
frame-origin' (ths/get-shape file' :frame-origin)]
|
||||
|
||||
;; ==== Check
|
||||
(t/is (some? (:interactions frame-origin)))
|
||||
(t/is (nil? (:interactions frame-origin')))))
|
||||
|
||||
|
||||
|
||||
(t/deftest test-add-new-interaction-updates-show-in-view-mode
|
||||
(let [;; ==== Setup
|
||||
|
||||
file (-> (thf/sample-file :file1)
|
||||
(tho/add-frame :frame-dest :hide-in-viewer true)
|
||||
(tho/add-frame :frame-origin :hide-in-viewer true))
|
||||
frame-dest (ths/get-shape file :frame-dest)
|
||||
frame-origin (ths/get-shape file :frame-origin)
|
||||
|
||||
page (thf/current-page file)
|
||||
|
||||
;; ==== Action
|
||||
new-interaction (-> ctsi/default-interaction
|
||||
(ctsi/set-destination (:id frame-dest))
|
||||
(assoc :position-relative-to (:id frame-dest)))
|
||||
|
||||
changes (-> (pcb/empty-changes nil (:id page))
|
||||
(pcb/with-objects (:objects page))
|
||||
(pcb/update-shapes [(:id frame-origin)] #(cls/add-new-interaction % new-interaction)))
|
||||
file' (thf/apply-changes file changes)
|
||||
|
||||
;; ==== Get
|
||||
frame-origin' (ths/get-shape file' :frame-origin)]
|
||||
|
||||
;; ==== Check
|
||||
(t/is (true? (:hide-in-viewer frame-origin)))
|
||||
(t/is (nil? (:hide-in-viewer frame-origin')))))
|
||||
@@ -265,6 +265,16 @@ RUN set -eux; \
|
||||
rm rustup-init; \
|
||||
chmod -R a+w $RUSTUP_HOME $CARGO_HOME;
|
||||
|
||||
WORKDIR /usr/local
|
||||
|
||||
# Install emscripten SDK and activate it
|
||||
RUN set -eux; \
|
||||
git clone https://github.com/emscripten-core/emsdk.git; \
|
||||
cd emsdk; \
|
||||
./emsdk install latest; \
|
||||
./emsdk activate latest; \
|
||||
rustup target add wasm32-unknown-emscripten;
|
||||
|
||||
WORKDIR /home
|
||||
|
||||
COPY files/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export PATH=/usr/lib/jvm/openjdk/bin:/usr/local/nodejs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
|
||||
export JAVA_OPTS=${JAVA_OPTS:-"-Xmx1000m -Xms200m"};
|
||||
export JAVA_OPTS="-Xmx1000m -Xms50m"
|
||||
|
||||
alias l='ls --color -GFlh'
|
||||
alias rm='rm -r'
|
||||
@@ -11,6 +11,7 @@ alias lsf='ls -h *(.)'
|
||||
|
||||
# init Cargo / Rust env
|
||||
. "/usr/local/cargo/env"
|
||||
EMSDK_QUIET=1 . "/usr/local/emsdk/emsdk_env.sh"
|
||||
|
||||
# include .bashrc if it exists
|
||||
if [ -f "$HOME/.bashrc.local" ]; then
|
||||
|
||||
@@ -10,7 +10,7 @@ rm -rf target
|
||||
export NODE_ENV=production;
|
||||
|
||||
# Build the application
|
||||
clojure -M:dev:shadow-cljs release main;
|
||||
clojure -J-Xms100M -J-Xmx1000M -J-XX:+UseSerialGC -M:dev:shadow-cljs release main;
|
||||
|
||||
# Remove source
|
||||
rm -rf target/app;
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
:output-wrapper false}
|
||||
|
||||
:release
|
||||
{:compiler-options
|
||||
{:closure-defines {goog.debug.LOGGING_ENABLED true}
|
||||
:compiler-options
|
||||
{:fn-invoke-direct true
|
||||
:source-map true
|
||||
:optimizations #shadow/env ["PENPOT_BUILD_OPTIMIZATIONS" :as :keyword :default :simple]
|
||||
|
||||
@@ -1 +1 @@
|
||||
v20.11.1
|
||||
v14.15.0
|
||||
|
||||
@@ -20,9 +20,10 @@
|
||||
"build:app:assets": "node ./scripts/build-app-assets.js",
|
||||
"build:storybook": "yarn run build:storybook:assets && yarn run build:storybook:cljs && storybook build",
|
||||
"build:storybook:assets": "node ./scripts/build-storybook-assets.js",
|
||||
"build:storybook:cljs": "clojure -M:dev:shadow-cljs compile storybook",
|
||||
"build:storybook:cljs": "clojure -M:dev:shadow-cljs release storybook",
|
||||
"build:renderer": "yarn run wasm-pack build ./renderer --target web --out-dir ../resources/public/js/renderer --release",
|
||||
"e2e:server": "node ./scripts/e2e-server.js",
|
||||
"e2e:test": "playwright test --project default",
|
||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
||||
"fmt:clj:check": "cljfmt check --parallel=false src/ test/",
|
||||
"fmt:js": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -w",
|
||||
@@ -34,7 +35,6 @@
|
||||
"test:compile": "clojure -M:dev:shadow-cljs compile test --config-merge '{:autorun false}'",
|
||||
"test:run": "node target/tests.cjs",
|
||||
"test:watch": "clojure -M:dev:shadow-cljs watch test",
|
||||
"test:e2e": "playwright test --project default",
|
||||
"translations": "node ./scripts/translations.js",
|
||||
"watch": "yarn run watch:app:assets",
|
||||
"watch:app:assets": "node ./scripts/watch.js",
|
||||
|
||||
@@ -56,7 +56,7 @@ export default defineConfig({
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
timeout: 2 * 60 * 1000,
|
||||
command: "yarn run e2e:server",
|
||||
command: "yarn e2e:server",
|
||||
url: "http://localhost:3000",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
|
||||
@@ -168,7 +168,7 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
async moveSelectionToShape(name) {
|
||||
await this.page.locator("rect.viewport-selrect").hover();
|
||||
await this.page.mouse.down();
|
||||
await this.viewport.getByText(name).first().hover({ force: true });
|
||||
await this.viewport.getByTestId(name).first().hover({ force: true });
|
||||
await this.page.mouse.up();
|
||||
}
|
||||
|
||||
|
||||
1
frontend/render_v2/cpp/.gitignore
vendored
Normal file
1
frontend/render_v2/cpp/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
out
|
||||
108
frontend/render_v2/cpp/Dockerfile
Normal file
108
frontend/render_v2/cpp/Dockerfile
Normal file
@@ -0,0 +1,108 @@
|
||||
FROM debian:11
|
||||
RUN apt update && apt dist-upgrade -y && apt install -y \
|
||||
git \
|
||||
clang \
|
||||
python \
|
||||
curl \
|
||||
build-essential \
|
||||
libfontconfig-dev \
|
||||
libgl1-mesa-dev \
|
||||
libglu1-mesa-dev \
|
||||
procps \
|
||||
vim \
|
||||
binaryen \
|
||||
wabt \
|
||||
&& groupadd -g 2000 skia \
|
||||
&& useradd -u 2000 -g 2000 skia
|
||||
|
||||
# TODO(kjlubick): Try a shallow clone of depot_tools
|
||||
RUN cd /tmp \
|
||||
&& git clone 'https://chromium.googlesource.com/chromium/tools/depot_tools.git'
|
||||
|
||||
RUN cd /tmp \
|
||||
&& git clone 'https://gn.googlesource.com/gn'
|
||||
|
||||
RUN cd /tmp \
|
||||
&& git clone 'https://skia.googlesource.com/skia' \
|
||||
&& cd skia \
|
||||
&& git checkout 'chrome/m129'
|
||||
|
||||
ENV PATH=${PATH}:/tmp/depot_tools
|
||||
ENV PATH=${PATH}:/tmp/gn
|
||||
|
||||
ADD --chown=skia:skia https://storage.googleapis.com/skia-swiftshader/libGLESv2.so /usr/local/lib/libGLESv2.so
|
||||
ADD --chown=skia:skia https://storage.googleapis.com/skia-swiftshader/libEGL.so /usr/local/lib/libEGL.so
|
||||
|
||||
# FIXME: I don't like this approach because it implies that
|
||||
# git-sync-deps is going to fail and we need to run it two
|
||||
# times. The weird thing is that git-sync-deps fails consistently
|
||||
# the first time.
|
||||
RUN cd /tmp/skia; \
|
||||
tools/git-sync-deps; \
|
||||
tools/git-sync-deps; \
|
||||
exit 0
|
||||
|
||||
RUN cd /tmp/skia && python3 bin/fetch-ninja
|
||||
|
||||
RUN . "tmp/skia/third_party/externals/emsdk/emsdk_env.sh"
|
||||
|
||||
RUN cd /tmp/skia && ./bin/gn gen out/wasm \
|
||||
--args="is_debug=false \
|
||||
is_official_build=true \
|
||||
is_component_build=false \
|
||||
is_trivial_abi=true \
|
||||
werror=true \
|
||||
target_cpu=\"wasm\" \
|
||||
skia_use_angle=false \
|
||||
skia_use_dng_sdk=false \
|
||||
skia_use_dawn=false \
|
||||
skia_use_webgl=true \
|
||||
skia_use_webgpu=false \
|
||||
skia_use_expat=false \
|
||||
skia_use_fontconfig=false \
|
||||
skia_use_freetype=true \
|
||||
skia_use_libheif=false \
|
||||
skia_use_libjpeg_turbo_decode=true \
|
||||
skia_use_libjpeg_turbo_encode=true \
|
||||
skia_use_no_jpeg_encode=false \
|
||||
skia_use_libpng_decode=true \
|
||||
skia_use_libpng_encode=true \
|
||||
skia_use_no_png_encode=false \
|
||||
skia_use_libwebp_decode=true \
|
||||
skia_use_libwebp_encode=true \
|
||||
skia_use_no_webp_encode=false \
|
||||
skia_use_lua=false \
|
||||
skia_use_piex=false \
|
||||
skia_use_system_freetype2=false \
|
||||
skia_use_system_libjpeg_turbo=false \
|
||||
skia_use_system_libpng=false \
|
||||
skia_use_system_libwebp=false \
|
||||
skia_use_system_zlib=false \
|
||||
skia_use_vulkan=false \
|
||||
skia_use_wuffs=true \
|
||||
skia_use_zlib=true \
|
||||
skia_enable_ganesh=true \
|
||||
skia_enable_sksl=true \
|
||||
skia_build_for_debugger=false \
|
||||
skia_enable_sksl_tracing=true \
|
||||
skia_use_icu=true \
|
||||
skia_use_client_icu=false \
|
||||
skia_use_libgrapheme=false \
|
||||
skia_use_system_icu=false \
|
||||
skia_use_harfbuzz=true \
|
||||
skia_use_system_harfbuzz=false \
|
||||
skia_enable_fontmgr_custom_directory=false \
|
||||
skia_enable_fontmgr_custom_embedded=true \
|
||||
skia_enable_fontmgr_custom_empty=false \
|
||||
skia_fontmgr_factory=\":fontmgr_custom_embedded_factory\" \
|
||||
skia_use_freetype_woff2=true \
|
||||
skia_enable_skshaper=true \
|
||||
skia_enable_skparagraph=true \
|
||||
skia_enable_pdf=false"
|
||||
|
||||
RUN cd /tmp/skia; ninja -C out/wasm
|
||||
RUN echo "source '/tmp/skia/third_party/externals/emsdk/emsdk_env.sh'" >> /root/.bashrc
|
||||
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
65
frontend/render_v2/cpp/Makefile
Normal file
65
frontend/render_v2/cpp/Makefile
Normal file
@@ -0,0 +1,65 @@
|
||||
all:
|
||||
# -fno-rtti: Removes C++ Run-Time Type Info support.
|
||||
# --no-entry: Disables the necessity of an entry point.
|
||||
# -sALLOW_MEMORY_GROWTH: Creates a resizable memory ArrayBuffer.
|
||||
# -sMODULARIZE: Exports emscripten as a CommonJS/AMD module.
|
||||
# -sENVIRONMENT: Removes unnecessary environments (node,worker,etc).
|
||||
# -sUSE_PTHREADS: Disables pthreads.
|
||||
# -sMAX_WEBGL_VERSION: Max WebGL set to 2
|
||||
# -sUSE_WEBGL2: Uses WebGL2 by default.
|
||||
em++ \
|
||||
-std=c++20 \
|
||||
-lembind \
|
||||
-fno-rtti \
|
||||
--no-entry \
|
||||
-sALLOW_MEMORY_GROWTH \
|
||||
-sUSE_PTHREADS=0 \
|
||||
-sMODULARIZE=1 \
|
||||
-sDISABLE_EXCEPTION_CATCHING \
|
||||
-sNODEJS_CATCH_EXIT=0 \
|
||||
-sMAX_WEBGL_VERSION=2 \
|
||||
-sUSE_WEBGL2=1 \
|
||||
-sFORCE_FILESYSTEM=0 \
|
||||
-sDYNAMIC_EXECUTION=0 \
|
||||
-sFILESYSTEM=0 \
|
||||
-sENVIRONMENT='web' \
|
||||
-sINITIAL_MEMORY=128MB \
|
||||
-DCK_ENABLE_WEBGL \
|
||||
-DCK_NO_FONTS \
|
||||
-DSK_RELEASE \
|
||||
-DSK_DISABLE_TRACING \
|
||||
-DSK_FORCE_AAA \
|
||||
-DSK_FORCE_8_BYTE_ALIGNMENT \
|
||||
-DSK_SHAPER_HARFBUZZ_AVAILABLE \
|
||||
-DCK_SERIALIZE_SKP \
|
||||
-DSK_GANESH \
|
||||
-DSK_DISABLE_LEGACY_SHADERCONTEXT \
|
||||
-DCK_INCLUDE_PATHOPS \
|
||||
-DCK_INCLUDE_RUNTIME_EFFECT \
|
||||
-DSKSL_ENABLE_TRACING \
|
||||
-DNDEBUG \
|
||||
-DSK_TRIVIAL_ABI="[[clang::trivial_abi]]" \
|
||||
-DSK_TYPEFACE_FACTORY_FREETYPE \
|
||||
-DSK_GL \
|
||||
-DSK_CODEC_DECODES_JPEG \
|
||||
-DSK_CODEC_DECODES_PNG \
|
||||
-DSK_CODEC_DECODES_WEBP \
|
||||
-DSK_HAS_WUFFS_LIBRARY \
|
||||
-DSK_ENABLE_SKSL \
|
||||
-DSK_ENABLE_PRECOMPILE \
|
||||
-DSKNX_NO_SIMD \
|
||||
-DSK_ASSUME_WEBGL=1 \
|
||||
-DSK_USE_WEBGL \
|
||||
-DSK_ENABLE_PARAGRAPH \
|
||||
-DSK_UNICODE_AVAILABLE \
|
||||
-DSK_UNICODE_ICU_IMPLEMENTATION \
|
||||
-DSK_ENABLE_SKOTTIE \
|
||||
-DSK_ENABLE_SKOTTIE_SKSLEFFECT \
|
||||
-DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0 \
|
||||
--pre-js js/preamble.js \
|
||||
--pre-js js/postamble.js \
|
||||
-I/tmp/skia \
|
||||
-o out/renderer.js \
|
||||
/tmp/skia/out/wasm/modules/canvaskit/fonts/NotoMono-Regular.ttf.ninja.cpp \
|
||||
/tmp/skia/out/wasm/libskia.a \
|
||||
src/main.cpp
|
||||
49
frontend/render_v2/cpp/README.md
Normal file
49
frontend/render_v2/cpp/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Renderer
|
||||
|
||||
## How this works?
|
||||
|
||||
First of all we need a proper environment to build Skia, this
|
||||
environment is heavily based on the [Skia docker image](https://github.com/google/skia/blob/main/docker/skia-release/Dockerfile) but with some tweaks to support building
|
||||
a C++ WebAssembly module using [Emscripten](https://emscripten.org/index.html).
|
||||
|
||||
## Building everything
|
||||
|
||||
From the root directory of `frontend/renderer` just run:
|
||||
|
||||
```sh
|
||||
./build
|
||||
```
|
||||
|
||||
This is going to build the docker image and run the container to build
|
||||
the artifacts and then copy them to the necessary directories.
|
||||
|
||||
> :smile_cat: Be patient, the first time the docker image is built usually takes
|
||||
> a few minutes.
|
||||
|
||||
## Building the Skia build tools Docker image
|
||||
|
||||
To build just the Skia build tools image:
|
||||
|
||||
```sh
|
||||
cd frontend/renderer
|
||||
docker build . -t skia-build-tools
|
||||
```
|
||||
|
||||
## Building the renderer WebAssembly module
|
||||
|
||||
Just run the container and it will generate all the necessary
|
||||
artifacts in the `out` directory.
|
||||
|
||||
```sh
|
||||
cd frontend/renderer
|
||||
docker run -t -v ${PWD}:/tmp/renderer skia-build-tools
|
||||
```
|
||||
|
||||
Once the `renderer.js` and `renderer.wasm` are created in the `out` directory
|
||||
we need to move them where Penpot can have access to them, so we need to execute
|
||||
`./scripts/copy-artifacts`.
|
||||
|
||||
## C++ <-> JS
|
||||
|
||||
To add some extra functionality to the exported `Module` by the Emscripten
|
||||
compiler, we use a series of javascript scripts that exist on the `js` directory.
|
||||
11
frontend/render_v2/cpp/TODO.md
Normal file
11
frontend/render_v2/cpp/TODO.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# TO DO
|
||||
|
||||
- [x] Compile Skia.
|
||||
- [x] Compile simple `renderer.wasm` with a exported function.
|
||||
- [ ] Compile a `renderer.wasm` that uses a WebGL context.
|
||||
|
||||
## Notes
|
||||
|
||||
- I've used the Skia `main` branch and it looks that there's something missing from the last release (`chrome/m117`) so I tried to switch to that branch but now I have different issues.
|
||||
|
||||
- It is necessary to use the GL emscripten module to deal with WebGL contexts. See `js/preamble.js` and
|
||||
4
frontend/render_v2/cpp/build
Executable file
4
frontend/render_v2/cpp/build
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
docker build . -t skia-build-tools
|
||||
docker run -t -v $PWD:/tmp/renderer skia-build-tools
|
||||
./scripts/copy-artifacts
|
||||
4
frontend/render_v2/cpp/docker/entrypoint.sh
Executable file
4
frontend/render_v2/cpp/docker/entrypoint.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
source /root/.bashrc
|
||||
cd /tmp/renderer
|
||||
emmake make
|
||||
2
frontend/render_v2/cpp/js/postamble.js
Normal file
2
frontend/render_v2/cpp/js/postamble.js
Normal file
@@ -0,0 +1,2 @@
|
||||
console.log("postamble");
|
||||
}(Module));
|
||||
69
frontend/render_v2/cpp/js/preamble.js
Normal file
69
frontend/render_v2/cpp/js/preamble.js
Normal file
@@ -0,0 +1,69 @@
|
||||
// Adds compile-time JS functions to augment Renderer interface.
|
||||
(function (Renderer) {
|
||||
console.log("preamble", Renderer);
|
||||
|
||||
const LCG_MULTIPLIER = 1103515245;
|
||||
const LCG_INCREMENT = 12345;
|
||||
const LCG_MODULUS = Math.pow(2, 31);
|
||||
const LCG_MASK = (LCG_MODULUS - 1);
|
||||
|
||||
function lcg(x, a, c, m) {
|
||||
return (x * a + c) % m;
|
||||
}
|
||||
|
||||
class Random {
|
||||
constructor(seed) {
|
||||
this._seed = seed;
|
||||
}
|
||||
|
||||
value() {
|
||||
this._seed = lcg(this._seed, LCG_MULTIPLIER, LCG_INCREMENT, LCG_MODULUS);
|
||||
return (this._seed & LCG_MASK) / LCG_MODULUS;
|
||||
}
|
||||
}
|
||||
|
||||
const random = new Random(0)
|
||||
|
||||
// Sets canvas.
|
||||
Renderer.setCanvas = function setCanvas(canvas, attrs) {
|
||||
const context = GL.createContext(canvas, attrs);
|
||||
if (!context) {
|
||||
throw new Error('Could not create a new WebGL context')
|
||||
}
|
||||
GL.makeContextCurrent(context);
|
||||
|
||||
// Emscripten does not enable this by default and Skia needs this
|
||||
// to handle certain GPU corner cases.
|
||||
GL.currentContext.GLctx.getExtension('WEBGL_debug_renderer_info');
|
||||
|
||||
// Initializes everything needed.
|
||||
this._InitCanvas(canvas.width, canvas.height);
|
||||
};
|
||||
|
||||
Renderer.setObjects = function setObjects(vbox, zoom, objects) {
|
||||
// this._SetObjects(objects.cnt);
|
||||
const numObjects = 20_000;
|
||||
this._SetObjects(numObjects);
|
||||
for (let index = 0; index < numObjects; index++) {
|
||||
// const object = objects.arr[index * 2 + 1];
|
||||
this._SetObjectRect(
|
||||
index,
|
||||
// object.selrect.x,
|
||||
random.value() * 2000,
|
||||
// object.selrect.y,
|
||||
random.value() * 2000,
|
||||
// object.selrect.width,
|
||||
random.value() * 200,
|
||||
// object.selrect.height,
|
||||
random.value() * 200
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Renderer.drawCanvas = function drawCanvas(vbox, zoom, objects) {
|
||||
performance.mark('draw-canvas:start');
|
||||
this._DrawCanvas(vbox.x, vbox.y, zoom);
|
||||
performance.mark('draw-canvas:end');
|
||||
const { duration } = performance.measure('draw-canvas', 'draw-canvas:start', 'draw-canvas:end');
|
||||
console.log('draw-canvas', `${duration}ms`);
|
||||
};
|
||||
11
frontend/render_v2/cpp/scripts/copy-artifacts
Executable file
11
frontend/render_v2/cpp/scripts/copy-artifacts
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
mkdir -p ../../resources/public/js/render_v2/cpp
|
||||
mkdir -p ../../src/app/render_v2/
|
||||
|
||||
# FIXME: This is a VERY HACKY way to set the correct `scriptDirectory` but
|
||||
# I didn't find a better way yet.
|
||||
PREAMBLE_LINES=`wc -l js/preamble.js | egrep -o [0-9]+`
|
||||
POSTAMBLE_LINES=`wc -l js/postamble.js | egrep -o [0-9]+`
|
||||
LINE_NUMBER=`echo "200 + ${PREAMBLE_LINES} + ${POSTAMBLE_LINES}" | bc | egrep -o [0-9]+`
|
||||
sed "${LINE_NUMBER} i \ \ scriptDirectory += 'js/render_v2/cpp/';" out/renderer.js > ../../src/app/render_v2/cpp.js
|
||||
cp out/renderer.wasm ../../resources/public/js/render_v2/cpp
|
||||
275
frontend/render_v2/cpp/src/main.cpp
Normal file
275
frontend/render_v2/cpp/src/main.cpp
Normal file
@@ -0,0 +1,275 @@
|
||||
#include "include/android/SkAnimatedImage.h"
|
||||
#include "include/codec/SkAndroidCodec.h"
|
||||
#include "include/codec/SkEncodedImageFormat.h"
|
||||
#include "include/core/SkBBHFactory.h"
|
||||
#include "include/core/SkBlendMode.h"
|
||||
#include "include/core/SkBlender.h"
|
||||
#include "include/core/SkBlurTypes.h"
|
||||
#include "include/core/SkCanvas.h"
|
||||
#include "include/core/SkColor.h"
|
||||
#include "include/core/SkColorFilter.h"
|
||||
#include "include/core/SkColorSpace.h"
|
||||
#include "include/core/SkData.h"
|
||||
#include "include/core/SkImage.h"
|
||||
#include "include/core/SkImageFilter.h"
|
||||
#include "include/core/SkImageGenerator.h"
|
||||
#include "include/core/SkImageInfo.h"
|
||||
#include "include/core/SkM44.h"
|
||||
#include "include/core/SkMaskFilter.h"
|
||||
#include "include/core/SkPaint.h"
|
||||
#include "include/core/SkPath.h"
|
||||
#include "include/core/SkPathEffect.h"
|
||||
#include "include/core/SkPathMeasure.h"
|
||||
#include "include/core/SkPathUtils.h"
|
||||
#include "include/core/SkPicture.h"
|
||||
#include "include/core/SkPictureRecorder.h"
|
||||
#include "include/core/SkPoint3.h"
|
||||
#include "include/core/SkRRect.h"
|
||||
#include "include/core/SkSamplingOptions.h"
|
||||
#include "include/core/SkScalar.h"
|
||||
#include "include/core/SkSerialProcs.h"
|
||||
#include "include/core/SkShader.h"
|
||||
#include "include/core/SkStream.h"
|
||||
#include "include/core/SkString.h"
|
||||
#include "include/core/SkStrokeRec.h"
|
||||
#include "include/core/SkSurface.h"
|
||||
#include "include/core/SkTextBlob.h"
|
||||
#include "include/core/SkTypeface.h"
|
||||
#include "include/core/SkTypes.h"
|
||||
#include "include/core/SkVertices.h"
|
||||
#include "include/effects/Sk1DPathEffect.h"
|
||||
#include "include/effects/Sk2DPathEffect.h"
|
||||
#include "include/effects/SkCornerPathEffect.h"
|
||||
#include "include/effects/SkDashPathEffect.h"
|
||||
#include "include/effects/SkDiscretePathEffect.h"
|
||||
#include "include/effects/SkGradientShader.h"
|
||||
#include "include/effects/SkImageFilters.h"
|
||||
#include "include/effects/SkLumaColorFilter.h"
|
||||
#include "include/effects/SkPerlinNoiseShader.h"
|
||||
#include "include/effects/SkRuntimeEffect.h"
|
||||
#include "include/effects/SkTrimPathEffect.h"
|
||||
#include "include/encode/SkJpegEncoder.h"
|
||||
#include "include/encode/SkPngEncoder.h"
|
||||
#include "include/encode/SkWebpEncoder.h"
|
||||
#include "include/private/SkShadowFlags.h"
|
||||
#include "include/utils/SkParsePath.h"
|
||||
#include "include/utils/SkShadowUtils.h"
|
||||
#include "src/core/SkPathPriv.h"
|
||||
#include "src/core/SkResourceCache.h"
|
||||
#include "src/image/SkImage_Base.h"
|
||||
#include "src/sksl/SkSLCompiler.h"
|
||||
|
||||
#include "modules/canvaskit/WasmCommon.h"
|
||||
|
||||
#include <emscripten.h>
|
||||
#include <emscripten/bind.h>
|
||||
#include <emscripten/html5.h>
|
||||
|
||||
#if defined(CK_ENABLE_WEBGL) || defined(CK_ENABLE_WEBGPU)
|
||||
#define ENABLE_GPU
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_GPU
|
||||
#include "include/gpu/GpuTypes.h"
|
||||
#include "include/gpu/GrDirectContext.h"
|
||||
#include "include/gpu/ganesh/GrExternalTextureGenerator.h"
|
||||
#include "include/gpu/ganesh/SkImageGanesh.h"
|
||||
#include "include/gpu/ganesh/SkSurfaceGanesh.h"
|
||||
#include "src/gpu/ganesh/GrCaps.h"
|
||||
#endif // ENABLE_GPU
|
||||
|
||||
#ifdef CK_ENABLE_WEBGL
|
||||
#include "include/gpu/GrBackendSurface.h"
|
||||
#include "include/gpu/GrTypes.h"
|
||||
#include "include/gpu/ganesh/gl/GrGLBackendSurface.h"
|
||||
#include "include/gpu/gl/GrGLInterface.h"
|
||||
#include "include/gpu/gl/GrGLTypes.h"
|
||||
#include "src/gpu/RefCntedCallback.h"
|
||||
#include "src/gpu/ganesh/GrProxyProvider.h"
|
||||
#include "src/gpu/ganesh/GrRecordingContextPriv.h"
|
||||
#include "src/gpu/ganesh/gl/GrGLDefines.h"
|
||||
|
||||
#include <webgl/webgl1.h>
|
||||
#endif // CK_ENABLE_WEBGL
|
||||
|
||||
#ifdef CK_ENABLE_WEBGPU
|
||||
#include <emscripten/html5_webgpu.h>
|
||||
#include <webgpu/webgpu.h>
|
||||
#include <webgpu/webgpu_cpp.h>
|
||||
#endif // CK_ENABLE_WEBGPU
|
||||
|
||||
#ifndef CK_NO_FONTS
|
||||
#include "include/core/SkFont.h"
|
||||
#include "include/core/SkFontMetrics.h"
|
||||
#include "include/core/SkFontMgr.h"
|
||||
#include "include/core/SkFontTypes.h"
|
||||
#ifdef CK_INCLUDE_PARAGRAPH
|
||||
#include "modules/skparagraph/include/Paragraph.h"
|
||||
#endif // CK_INCLUDE_PARAGRAPH
|
||||
#endif // CK_NO_FONTS
|
||||
|
||||
#ifdef CK_INCLUDE_PATHOPS
|
||||
#include "include/pathops/SkPathOps.h"
|
||||
#endif
|
||||
|
||||
#if defined(CK_INCLUDE_RUNTIME_EFFECT) && defined(SKSL_ENABLE_TRACING)
|
||||
#include "include/sksl/SkSLDebugTrace.h"
|
||||
#endif
|
||||
|
||||
#ifndef CK_NO_FONTS
|
||||
#include "include/ports/SkFontMgr_data.h"
|
||||
#endif
|
||||
|
||||
// Global data needed to keep everything in place.
|
||||
sk_sp<GrDirectContext> context = nullptr;
|
||||
sk_sp<SkSurface> surface = nullptr;
|
||||
SkCanvas *canvas = nullptr;
|
||||
|
||||
struct PenpotRect {
|
||||
float x, y, width, height;
|
||||
};
|
||||
|
||||
struct PenpotColor {
|
||||
float r, g, b, a;
|
||||
};
|
||||
|
||||
struct PenpotObject {
|
||||
PenpotRect selRect;
|
||||
};
|
||||
|
||||
std::vector<PenpotObject> objects(0);
|
||||
|
||||
constexpr uint32_t LCG_MULTIPLIER = 1103515245;
|
||||
constexpr uint32_t LCG_INCREMENT = 12345;
|
||||
constexpr uint32_t LCG_MODULUS = 0x80000000;
|
||||
constexpr uint32_t LCG_MASK = 0x7FFFFFFF;
|
||||
|
||||
uint32_t lcg(uint32_t x, uint32_t a, uint32_t c, uint32_t m)
|
||||
{
|
||||
return (x * a + c) % m;
|
||||
}
|
||||
|
||||
class Random {
|
||||
private:
|
||||
uint32_t _seed;
|
||||
|
||||
public:
|
||||
Random(uint32_t seed) : _seed(seed) {};
|
||||
|
||||
void reset(uint32_t new_seed) {
|
||||
_seed = new_seed;
|
||||
}
|
||||
|
||||
float value() {
|
||||
_seed = lcg(_seed, LCG_MULTIPLIER, LCG_INCREMENT, LCG_MODULUS);
|
||||
return (float)(_seed & LCG_MASK) / (float)LCG_MODULUS;
|
||||
}
|
||||
|
||||
uint8_t byte() {
|
||||
_seed = lcg(_seed, LCG_MULTIPLIER, LCG_INCREMENT, LCG_MODULUS);
|
||||
return (_seed & LCG_MASK) % 0xFF;
|
||||
}
|
||||
};
|
||||
|
||||
Random lcg_random(0);
|
||||
|
||||
// Initializes all the structures and elements needed to start rendering things.
|
||||
void InitCanvas(int width, int height)
|
||||
{
|
||||
emscripten_log(EM_LOG_CONSOLE, "Initializing canvas %d %d", width, height);
|
||||
|
||||
// We assume that any calls we make to GL for the remainder of this function will go to the
|
||||
// desired WebGL Context.
|
||||
// setup interface.
|
||||
auto interface = GrGLMakeNativeInterface();
|
||||
// setup context.
|
||||
context = GrDirectContext::MakeGL(interface);
|
||||
|
||||
emscripten_log(EM_LOG_CONSOLE, "GL context initialized");
|
||||
|
||||
GrGLint sampleCnt = 0;
|
||||
GrGLint stencil = 16;
|
||||
|
||||
// WebGL should already be clearing the color and stencil buffers, but do it again here to
|
||||
// ensure Skia receives them in the expected state.
|
||||
emscripten_glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
emscripten_glClearColor(0, 0, 0, 0);
|
||||
emscripten_glClearStencil(0);
|
||||
emscripten_glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
context->resetContext(kRenderTarget_GrGLBackendState | kMisc_GrGLBackendState);
|
||||
|
||||
// The on-screen canvas is FBO 0. Wrap it in a Skia render target so Skia can render to it.
|
||||
GrGLFramebufferInfo info;
|
||||
info.fFBOID = 0;
|
||||
|
||||
// Create the colorspace needed to represent graphics.
|
||||
sk_sp<SkColorSpace> colorSpace = SkColorSpace::MakeSRGB();
|
||||
|
||||
info.fFormat = GR_GL_RGBA8; // kRGBA_8888_SkColorType;
|
||||
auto target = GrBackendRenderTargets::MakeGL(
|
||||
width,
|
||||
height,
|
||||
sampleCnt,
|
||||
stencil,
|
||||
info
|
||||
);
|
||||
|
||||
emscripten_log(EM_LOG_CONSOLE, "Creating new surface");
|
||||
sk_sp<SkSurface> new_surface(
|
||||
SkSurfaces::WrapBackendRenderTarget(
|
||||
context.get(),
|
||||
target,
|
||||
kBottomLeft_GrSurfaceOrigin,
|
||||
kRGBA_8888_SkColorType,
|
||||
colorSpace,
|
||||
nullptr));
|
||||
|
||||
surface = new_surface;
|
||||
canvas = surface->getCanvas();
|
||||
emscripten_log(EM_LOG_CONSOLE, "Everything's ready!");
|
||||
}
|
||||
|
||||
void DrawCanvas(float x, float y, float zoom)
|
||||
{
|
||||
canvas->clear(SK_ColorTRANSPARENT);
|
||||
canvas->save();
|
||||
canvas->scale(zoom, zoom);
|
||||
canvas->translate(-x, -y);
|
||||
lcg_random.reset(0);
|
||||
emscripten_log(EM_LOG_CONSOLE, "Clearing canvas");
|
||||
for (auto object : objects) {
|
||||
// emscripten_log(EM_LOG_CONSOLE, "Drawing object");
|
||||
|
||||
SkPaint paint;
|
||||
paint.setARGB(255, lcg_random.byte(), lcg_random.byte(), lcg_random.byte());
|
||||
paint.setStyle(SkPaint::Style::kFill_Style);
|
||||
|
||||
SkRect rect = SkRect::MakeXYWH(object.selRect.x, object.selRect.y, object.selRect.width, object.selRect.height);
|
||||
canvas->drawRect(rect, paint);
|
||||
}
|
||||
canvas->restore();
|
||||
|
||||
emscripten_log(EM_LOG_CONSOLE, "Flushing and submitting");
|
||||
skgpu::ganesh::FlushAndSubmit(surface);
|
||||
}
|
||||
|
||||
void SetObjects(int num_objects) {
|
||||
// emscripten_log(EM_LOG_CONSOLE, "Resizing objects vector capacity %d", num_objects);
|
||||
objects.resize(num_objects);
|
||||
}
|
||||
|
||||
void SetObjectRect(int index, float x, float y, float width, float height) {
|
||||
// emscripten_log(EM_LOG_CONSOLE, "Setting object at %d %f %f %f %f", index, x, y, width, height);
|
||||
objects[index].selRect.x = x;
|
||||
objects[index].selRect.y = y;
|
||||
objects[index].selRect.width = width;
|
||||
objects[index].selRect.height = height;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_BINDINGS(Renderer)
|
||||
{
|
||||
function("_InitCanvas", InitCanvas);
|
||||
function("_DrawCanvas", DrawCanvas);
|
||||
function("_SetObjects", SetObjects);
|
||||
function("_SetObjectRect", SetObjectRect);
|
||||
}
|
||||
11
frontend/render_v2/rs/.gitignore
vendored
Normal file
11
frontend/render_v2/rs/.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
664
frontend/render_v2/rs/Cargo.lock
generated
Normal file
664
frontend/render_v2/rs/Cargo.lock
generated
Normal file
@@ -0,0 +1,664 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.69.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn",
|
||||
"which",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gl"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404"
|
||||
dependencies = [
|
||||
"gl_generator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gl_generator"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d"
|
||||
dependencies = [
|
||||
"khronos_api",
|
||||
"log",
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "home"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "khronos_api"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "lazycell"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.159"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
|
||||
|
||||
[[package]]
|
||||
name = "render"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"gl",
|
||||
"skia-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "skia-bindings"
|
||||
version = "0.78.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29880a81b088de322e9c5306236c70761a61b5fa4df3c15c93bad3ce890ce34c"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"cc",
|
||||
"flate2",
|
||||
"heck",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"serde_json",
|
||||
"tar",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "skia-safe"
|
||||
version = "0.78.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f15700ac678c06649077495acbba07e7ae01e5ca46b7dc18213f2c3477ada71"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"lazy_static",
|
||||
"skia-bindings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "4.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
|
||||
dependencies = [
|
||||
"either",
|
||||
"home",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26"
|
||||
22
frontend/render_v2/rs/Cargo.toml
Normal file
22
frontend/render_v2/rs/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "render"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/penpot/penpot"
|
||||
license-file = "../../../../LICENSE"
|
||||
description = "Wasm-based canvas render for Penpot"
|
||||
|
||||
[features]
|
||||
default = ["skia-safe/gl", "skia-safe/textlayout"]
|
||||
|
||||
[[bin]]
|
||||
name = "render_v2"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
gl = "0.14.0"
|
||||
skia-safe = "0.78.2"
|
||||
base64 = "0.13"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
6
frontend/render_v2/rs/build.sh
Executable file
6
frontend/render_v2/rs/build.sh
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
EMSDK_QUIET=1 . "/usr/local/emsdk/emsdk_env.sh"
|
||||
|
||||
EMCC_CFLAGS="--no-entry -s ERROR_ON_UNDEFINED_SYMBOLS=0 -s MAX_WEBGL_VERSION=2 -s MODULARIZE=1 -s EXPORT_NAME=createRustSkiaModule -s EXPORTED_RUNTIME_METHODS=GL -s ENVIRONMENT=web" cargo build --target=wasm32-unknown-emscripten
|
||||
|
||||
BIN
frontend/render_v2/rs/src/RobotoMono-Regular.ttf
Normal file
BIN
frontend/render_v2/rs/src/RobotoMono-Regular.ttf
Normal file
Binary file not shown.
362
frontend/render_v2/rs/src/main.rs
Normal file
362
frontend/render_v2/rs/src/main.rs
Normal file
@@ -0,0 +1,362 @@
|
||||
// use skia_safe::{
|
||||
// gpu::{self, gl::FramebufferInfo, DirectContext},
|
||||
// textlayout::{
|
||||
// FontCollection, ParagraphBuilder, ParagraphStyle, TextStyle, TypefaceFontProvider,
|
||||
// },
|
||||
// Canvas, Data, EncodedImageFormat, FontMgr, Paint, PaintStyle, Path, SurfaceProps,
|
||||
// };
|
||||
use skia_safe::{
|
||||
gpu::{self, gl::FramebufferInfo, DirectContext},
|
||||
textlayout::TypefaceFontProvider,
|
||||
PaintStyle,
|
||||
};
|
||||
use std::boxed::Box;
|
||||
|
||||
use skia_safe as skia;
|
||||
|
||||
static ROBOTO_REGULAR: &[u8] = include_bytes!("RobotoMono-Regular.ttf");
|
||||
static TYPEFACE_ALIAS: &str = "roboto-regular";
|
||||
|
||||
extern "C" {
|
||||
pub fn emscripten_GetProcAddress(
|
||||
name: *const ::std::os::raw::c_char,
|
||||
) -> *const ::std::os::raw::c_void;
|
||||
}
|
||||
|
||||
struct GpuState {
|
||||
context: DirectContext,
|
||||
framebuffer_info: FramebufferInfo,
|
||||
}
|
||||
|
||||
/// This struct holds the state of the Rust application between JS calls.
|
||||
///
|
||||
/// It is created by [init] and passed to the other exported functions. Note that rust-skia data
|
||||
/// structures are not thread safe, so a state must not be shared between different Web Workers.
|
||||
pub struct State {
|
||||
gpu_state: GpuState,
|
||||
surface: skia::Surface,
|
||||
typeface_font_provider: TypefaceFontProvider,
|
||||
default_font: skia_safe::Font,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn new(
|
||||
gpu_state: GpuState,
|
||||
surface: skia::Surface,
|
||||
typeface_font_provider: TypefaceFontProvider,
|
||||
default_font: skia_safe::Font,
|
||||
) -> Self {
|
||||
State {
|
||||
gpu_state,
|
||||
surface,
|
||||
typeface_font_provider,
|
||||
default_font,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_surface(&mut self, surface: skia::Surface) {
|
||||
self.surface = surface;
|
||||
}
|
||||
}
|
||||
|
||||
fn init_gl() {
|
||||
unsafe {
|
||||
gl::load_with(|addr| {
|
||||
let addr = std::ffi::CString::new(addr).unwrap();
|
||||
emscripten_GetProcAddress(addr.into_raw() as *const _) as *const _
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// This needs to be done once per WebGL context.
|
||||
fn create_gpu_state() -> GpuState {
|
||||
let interface = skia_safe::gpu::gl::Interface::new_native().unwrap();
|
||||
let context = skia_safe::gpu::direct_contexts::make_gl(interface, None).unwrap();
|
||||
let framebuffer_info = {
|
||||
let mut fboid: gl::types::GLint = 0;
|
||||
unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) };
|
||||
|
||||
FramebufferInfo {
|
||||
fboid: fboid.try_into().unwrap(),
|
||||
format: skia_safe::gpu::gl::Format::RGBA8.into(),
|
||||
protected: skia_safe::gpu::Protected::No,
|
||||
}
|
||||
};
|
||||
|
||||
GpuState {
|
||||
context,
|
||||
framebuffer_info,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the Skia surface that will be used for rendering.
|
||||
fn create_surface(gpu_state: &mut GpuState, width: i32, height: i32) -> skia::Surface {
|
||||
let backend_render_target =
|
||||
gpu::backend_render_targets::make_gl((width, height), 1, 8, gpu_state.framebuffer_info);
|
||||
|
||||
gpu::surfaces::wrap_backend_render_target(
|
||||
&mut gpu_state.context,
|
||||
&backend_render_target,
|
||||
skia_safe::gpu::SurfaceOrigin::BottomLeft,
|
||||
skia_safe::ColorType::RGBA8888,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn render_rect(surface: &mut skia::Surface, rect: skia::Rect, color: skia::Color) {
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_style(skia::PaintStyle::Fill);
|
||||
paint.set_color(color);
|
||||
// paint.set_anti_alias(true);
|
||||
surface.canvas().draw_rect(rect, &paint);
|
||||
}
|
||||
|
||||
fn render_rect_ref(surface: &mut skia::Surface, rect: &skia::Rect, paint: &skia::Paint) {
|
||||
surface.canvas().draw_rect(rect, paint);
|
||||
}
|
||||
|
||||
/// This is called from JS after the WebGL context has been created.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn init(width: i32, height: i32) -> Box<State> {
|
||||
let mut gpu_state = create_gpu_state();
|
||||
let surface = create_surface(&mut gpu_state, width, height);
|
||||
|
||||
// skia_safe::Font::default() is empty, let's use something better
|
||||
let font_mgr = skia_safe::FontMgr::new();
|
||||
let typeface = font_mgr
|
||||
.new_from_data(ROBOTO_REGULAR, None)
|
||||
.expect("Failed to load ROBOTO font");
|
||||
let default_font = skia_safe::Font::new(typeface.clone(), 10.0);
|
||||
|
||||
let typeface_font_provider = {
|
||||
let mut typeface_font_provider = TypefaceFontProvider::new();
|
||||
// We need a system font manager to be able to load typefaces.
|
||||
typeface_font_provider.register_typeface(typeface, TYPEFACE_ALIAS);
|
||||
typeface_font_provider
|
||||
};
|
||||
|
||||
let state = State::new(gpu_state, surface, typeface_font_provider, default_font);
|
||||
|
||||
Box::new(state)
|
||||
}
|
||||
|
||||
/// This is called from JS when the window is resized.
|
||||
/// # Safety
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn resize_surface(state: *mut State, width: i32, height: i32) {
|
||||
let state = unsafe { state.as_mut() }.expect("got an invalid state pointer");
|
||||
let surface = create_surface(&mut state.gpu_state, width, height);
|
||||
state.set_surface(surface);
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct Color {
|
||||
r: u8,
|
||||
g: u8,
|
||||
b: u8,
|
||||
a: f32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct Rect {
|
||||
left: f32,
|
||||
top: f32,
|
||||
right: f32,
|
||||
bottom: f32,
|
||||
r: f32,
|
||||
g: f32,
|
||||
b: f32,
|
||||
a: f32,
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn alloc_rects(len: usize) -> *mut Rect {
|
||||
// create a new mutable buffer with capacity `len`
|
||||
let mut buf: Vec<Rect> = Vec::with_capacity(len);
|
||||
let ptr = buf.as_mut_ptr();
|
||||
// take ownership of the memory block and ensure the its destructor is not
|
||||
// called when the object goes out of scope at the end of the function
|
||||
std::mem::forget(buf);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe fn free_rects(ptr: *mut Rect, len: usize) {
|
||||
let buf = Vec::<Rect>::from_raw_parts(ptr, len, len);
|
||||
std::mem::forget(buf);
|
||||
}
|
||||
|
||||
/// Draws a rect at the specified coordinates with the give ncolor
|
||||
/// # Safety
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn draw_rect(state: *mut State, rect: &Rect, color: &Color) {
|
||||
let state = unsafe { state.as_mut() }.expect("got an invalid state pointer");
|
||||
let r = skia::Rect::new(rect.left, rect.top, rect.right, rect.bottom);
|
||||
let color = skia::Color::from_argb((color.a * 255.0) as u8, color.r, color.g, color.b);
|
||||
render_rect(&mut state.surface, r, color);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn flush(state: *mut State) {
|
||||
let state = unsafe { state.as_mut() }.expect("got an invalid state pointer");
|
||||
state
|
||||
.gpu_state
|
||||
.context
|
||||
.flush_and_submit_surface(&mut state.surface, None);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn translate(state: *mut State, dx: f32, dy: f32) {
|
||||
(*state).surface.canvas().translate((dx, dy));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn scale(state: *mut State, sx: f32, sy: f32) {
|
||||
(*state).surface.canvas().scale((sx, sy));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn reset_canvas(state: *mut State) {
|
||||
let state = unsafe { state.as_mut() }.expect("got an invalid state pointer");
|
||||
state.surface.canvas().clear(skia_safe::Color::TRANSPARENT);
|
||||
state.surface.canvas().reset_matrix();
|
||||
flush(state);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn draw_shapes(
|
||||
state: *mut State,
|
||||
ptr: *mut Rect,
|
||||
len: usize,
|
||||
zoom: f32,
|
||||
dx: f32,
|
||||
dy: f32,
|
||||
) {
|
||||
let state = unsafe { state.as_mut() }.expect("got an invalid state pointer");
|
||||
reset_canvas(state);
|
||||
scale(state, zoom, zoom);
|
||||
translate(state, dx, dy);
|
||||
// create a `Vec<Rect>` from the pointer to the linear memory and length
|
||||
let buf = Vec::<Rect>::from_raw_parts(ptr, len, len);
|
||||
|
||||
// let mut text_paint = skia::Paint::default();
|
||||
// text_paint.set_anti_alias(true);
|
||||
// text_paint.set_style(skia_safe::paint::Style::StrokeAndFill);
|
||||
// text_paint.set_stroke_width(1.0);
|
||||
|
||||
// let mut path_paint = skia::Paint::default();
|
||||
// path_paint.set_color(skia_safe::Color::BLACK);
|
||||
// path_paint.set_anti_alias(true);
|
||||
// path_paint.set_stroke_width(1.0);
|
||||
// path_paint.set_style(PaintStyle::Stroke);
|
||||
|
||||
// let svg_canvas = skia_safe::svg::Canvas::new(skia_safe::Rect::from_size((10000, 10000)), None);
|
||||
|
||||
// let mut memory = Vec::new();
|
||||
// let mut document =
|
||||
// skia_safe::pdf::new_document(&mut memory, None).begin_page((10000, 10000), None);
|
||||
// // let pdf_canvas = document.canvas();
|
||||
|
||||
for rect in buf.iter() {
|
||||
let r = skia::Rect::new(rect.left, rect.top, rect.right, rect.bottom);
|
||||
|
||||
let color = skia::Color::from_argb(
|
||||
(rect.a * 255.0) as u8,
|
||||
rect.r as u8,
|
||||
rect.g as u8,
|
||||
rect.b as u8,
|
||||
);
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_style(skia::PaintStyle::Fill);
|
||||
paint.set_color(color);
|
||||
|
||||
// render_rect_ref(&mut state.surface, &r, &paint);
|
||||
state.surface.canvas().draw_rect(&r, &paint);
|
||||
// render_rect(&mut state.surface, r, color);
|
||||
|
||||
// paint.set_anti_alias(true);
|
||||
// state.surface.canvas().draw_rect(r, &paint);
|
||||
// state.surface.canvas().draw_text_align(
|
||||
// String::from("Lorem ipsum"),
|
||||
// (rect.left, rect.top),
|
||||
// &state.default_font,
|
||||
// &paint,
|
||||
// skia::utils::text_utils::Align::Left,
|
||||
// );
|
||||
|
||||
// let mut paint = skia::Paint::default();
|
||||
// paint.set_style(skia::PaintStyle::Fill);
|
||||
// paint.set_color(color);
|
||||
// paint.set_anti_alias(true);
|
||||
|
||||
// svg_canvas.draw_rect(r, &paint);
|
||||
// pdf_canvas.draw_rect(r, &paint);
|
||||
|
||||
// text_paint.set_color(color);
|
||||
// state.surface.canvas().draw_str(
|
||||
// "SKIA TEXT",
|
||||
// (rect.left, rect.top),
|
||||
// &state.default_font,
|
||||
// &text_paint,
|
||||
// );
|
||||
|
||||
// svg_canvas.draw_str("SKIA TEXT", (rect.left, rect.top), &state.default_font, &text_paint);
|
||||
// pdf_canvas.draw_str("SKIA TEXT", (rect.left, rect.top), &state.default_font, &text_paint);
|
||||
|
||||
// let mut path = Path::new();
|
||||
// path.move_to((rect.left, rect.top));
|
||||
// path.line_to((rect.right, rect.bottom));
|
||||
// state.surface.canvas().draw_path(&path, &path_paint);
|
||||
// svg_canvas.draw_path(&path, &path_paint);
|
||||
// pdf_canvas.draw_path(&path, &path_paint);
|
||||
|
||||
// https://github.com/rust-skia/rust-skia/blob/02c89a87649af8d2870fb631aae4a5e171887367/skia-org/src/skparagraph_example.rs#L18
|
||||
// let mut font_collection = FontCollection::new();
|
||||
// font_collection
|
||||
// .set_default_font_manager(Some(state.typeface_font_provider.clone().into()), None);
|
||||
// let paragraph_style = ParagraphStyle::new();
|
||||
// let mut paragraph_builder = ParagraphBuilder::new(¶graph_style, font_collection);
|
||||
// let mut ts = TextStyle::new();
|
||||
// ts.set_foreground_paint(&Paint::default())
|
||||
// .set_font_families(&[TYPEFACE_ALIAS]);
|
||||
// paragraph_builder.push_style(&ts);
|
||||
// paragraph_builder.add_text("Other skia text");
|
||||
// let mut paragraph = paragraph_builder.build();
|
||||
// paragraph.layout(256.0);
|
||||
// paragraph.paint(state.surface.canvas(), (rect.left, rect.top));
|
||||
// paragraph.paint(&svg_canvas, (rect.left, rect.top));
|
||||
}
|
||||
|
||||
/*
|
||||
// base64 image of the canvas
|
||||
let image = state.surface.image_snapshot();
|
||||
let mut context = state.surface.direct_context();
|
||||
let encoded_image = image.encode(context.as_mut(), EncodedImageFormat::PNG, None).unwrap();
|
||||
let base64_image = base64::encode(&encoded_image.as_bytes());
|
||||
println!("data:image/png;base64,{}", base64_image);
|
||||
|
||||
// SVG representation
|
||||
let svg_data = svg_canvas.end();
|
||||
let svg = String::from_utf8_lossy(svg_data.as_bytes());
|
||||
println!("svg: {}", svg.replace('\n', ""));
|
||||
|
||||
// PDF
|
||||
document.end_page().close();
|
||||
println!("PDF: ");
|
||||
print!("echo ");
|
||||
for byte in &memory {
|
||||
print!("{:02x}", byte);
|
||||
}
|
||||
println!("| xxd -r -p > output.pdf");
|
||||
*/
|
||||
|
||||
flush(state);
|
||||
std::mem::forget(buf);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
init_gl();
|
||||
}
|
||||
5
frontend/renderer/.gitignore
vendored
5
frontend/renderer/.gitignore
vendored
@@ -1,5 +0,0 @@
|
||||
target/
|
||||
debug/
|
||||
|
||||
**/*.rs.bk
|
||||
|
||||
324
frontend/renderer/Cargo.lock
generated
324
frontend/renderer/Cargo.lock
generated
@@ -1,324 +0,0 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "console_error_panic_hook"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.70"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "minicov"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c71e683cd655513b99affab7d317deb690528255a0d5f717f1024093c12b169"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "renderer"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test"
|
||||
version = "0.3.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68497a05fb21143a08a7d24fc81763384a3072ee43c44e86aad1744d6adef9d9"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
"minicov",
|
||||
"scoped-tls",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-bindgen-test-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test-macro"
|
||||
version = "0.3.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b8220be1fa9e4c889b30fd207d4906657e7e90b12e0e6b0c8b8d8709f5de021"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.70"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
@@ -1,19 +0,0 @@
|
||||
[package]
|
||||
name = "renderer"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/penpot/penpot"
|
||||
license-file = "../../../../LICENSE"
|
||||
description = "Wasm-based canvas renderer for Penpot"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2.93"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.43"
|
||||
@@ -1,36 +0,0 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
pub fn add(left: u64, right: u64) -> u64 {
|
||||
left + right
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_namespace = console)]
|
||||
fn log(s: &str);
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn print(msg: &str) {
|
||||
log(msg);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = add(2, 2);
|
||||
assert_eq!(result, 4);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn it_works_in_wasm() {
|
||||
let result = add(2, 2);
|
||||
assert_eq!(result, 4);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 711 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 164 KiB |
File diff suppressed because it is too large
Load Diff
@@ -29,7 +29,7 @@
|
||||
<script defer src="{{& polyfills}}"></script>
|
||||
{{/manifest}}
|
||||
|
||||
<script type="module" src="{{& pluginRuntimeUri}}"></script>
|
||||
<script type="module" src="{{pluginRuntimeUri}}/index.js"></script>
|
||||
|
||||
<script>
|
||||
window.penpotTranslations = JSON.parse({{& translations}});
|
||||
|
||||
@@ -181,16 +181,14 @@ export async function watch(baseDir, predicate, callback) {
|
||||
}
|
||||
|
||||
async function readShadowManifest() {
|
||||
const ts = Date.now();
|
||||
try {
|
||||
const manifestPath = "resources/public/js/manifest.json";
|
||||
let content = await fs.readFile(manifestPath, { encoding: "utf8" });
|
||||
content = JSON.parse(content);
|
||||
|
||||
const index = {
|
||||
ts: ts,
|
||||
config: "js/config.js?ts=" + ts,
|
||||
polyfills: "js/polyfills.js?ts=" + ts,
|
||||
config: "js/config.js?ts=" + Date.now(),
|
||||
polyfills: "js/polyfills.js?ts=" + Date.now(),
|
||||
};
|
||||
|
||||
for (let item of content) {
|
||||
@@ -200,13 +198,12 @@ async function readShadowManifest() {
|
||||
return index;
|
||||
} catch (cause) {
|
||||
return {
|
||||
ts: ts,
|
||||
config: "js/config.js?ts=" + ts,
|
||||
polyfills: "js/polyfills.js?ts=" + ts,
|
||||
main: "js/main.js?ts=" + ts,
|
||||
shared: "js/shared.js?ts=" + ts,
|
||||
worker: "js/worker.js?ts=" + ts,
|
||||
rasterizer: "js/rasterizer.js?ts=" + ts,
|
||||
config: "js/config.js",
|
||||
polyfills: "js/polyfills.js",
|
||||
main: "js/main.js",
|
||||
shared: "js/shared.js",
|
||||
worker: "js/worker.js",
|
||||
rasterizer: "js/rasterizer.js",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -412,8 +409,8 @@ async function generateTemplates() {
|
||||
|
||||
const pluginRuntimeUri =
|
||||
process.env.PENPOT_PLUGIN_DEV === "true"
|
||||
? "http://localhost:4200/index.js?ts=" + manifest.ts
|
||||
: "plugins-runtime/index.js?ts=" + manifest.ts;
|
||||
? "http://localhost:4200"
|
||||
: "./plugins-runtime";
|
||||
|
||||
content = await renderTemplate(
|
||||
"resources/templates/index.mustache",
|
||||
|
||||
@@ -62,12 +62,6 @@
|
||||
:depends-on #{:shared}
|
||||
:init-fn app.rasterizer/init}}
|
||||
|
||||
:js-options
|
||||
{:entry-keys ["module" "browser" "main"]
|
||||
:resolve {"penpot/vendor/text-editor-v2"
|
||||
{:target :file
|
||||
:file "vendor/text_editor_v2.js"}}}
|
||||
|
||||
:compiler-options
|
||||
{:output-feature-set :es2020
|
||||
:output-wrapper false
|
||||
@@ -151,12 +145,6 @@
|
||||
:ns-regexp "^frontend-tests.*-test$"
|
||||
:autorun true
|
||||
|
||||
:js-options
|
||||
{:entry-keys ["module" "browser" "main"]
|
||||
:resolve {"penpot/vendor/text-editor-v2"
|
||||
{:target :file
|
||||
:file "vendor/text_editor_v2.js"}}}
|
||||
|
||||
:compiler-options
|
||||
{:output-feature-set :es2020
|
||||
:output-wrapper false
|
||||
|
||||
@@ -111,7 +111,6 @@
|
||||
(def flex-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/"))
|
||||
(def grid-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/"))
|
||||
(def plugins-list-uri (obj/get global "penpotPluginsListUri" "https://penpot-docs-plugins.pages.dev/plugins/getting-started/#examples"))
|
||||
(def plugins-whitelist (into #{} (obj/get global "penpotPluginsWhitelist" [])))
|
||||
|
||||
(defn- normalize-uri
|
||||
[uri-str]
|
||||
|
||||
@@ -941,7 +941,7 @@
|
||||
(update-in [:dashboard-projects project-id :count] inc)))))
|
||||
|
||||
(defn create-file
|
||||
[{:keys [project-id name] :as params}]
|
||||
[{:keys [project-id] :as params}]
|
||||
(dm/assert! (uuid? project-id))
|
||||
(ptk/reify ::create-file
|
||||
ev/Event
|
||||
@@ -955,7 +955,7 @@
|
||||
|
||||
files (get state :dashboard-files)
|
||||
unames (cfh/get-used-names files)
|
||||
name (or name (cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1")))
|
||||
name (cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1"))
|
||||
features (-> (features/get-team-enabled-features state)
|
||||
(set/difference cfeat/frontend-only-features))
|
||||
params (-> params
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.data.plugins
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.register :as preg]
|
||||
[app.util.globals :as ug]
|
||||
[app.util.http :as http]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(defn fetch-manifest
|
||||
[plugin-url]
|
||||
(->> (http/send! {:method :get
|
||||
:uri plugin-url
|
||||
:omit-default-headers true
|
||||
:response-type :json})
|
||||
(rx/map :body)
|
||||
(rx/map #(preg/parse-manifest plugin-url %))))
|
||||
|
||||
(defn save-current-plugin
|
||||
[id]
|
||||
(ptk/reify ::save-current-plugin
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :open-plugins] (fnil conj #{}) id))))
|
||||
|
||||
(defn remove-current-plugin
|
||||
[id]
|
||||
(ptk/reify ::remove-current-plugin
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :open-plugins] (fnil disj #{}) id))))
|
||||
|
||||
(defn- load-plugin!
|
||||
[{:keys [plugin-id name description host code icon permissions]}]
|
||||
(try
|
||||
(st/emit! (save-current-plugin plugin-id))
|
||||
(.ɵloadPlugin
|
||||
^js ug/global
|
||||
#js {:pluginId plugin-id
|
||||
:name name
|
||||
:description description
|
||||
:host host
|
||||
:code code
|
||||
:icon icon
|
||||
:permissions (apply array permissions)}
|
||||
(fn []
|
||||
(st/emit! (remove-current-plugin plugin-id))))
|
||||
|
||||
(catch :default e
|
||||
(st/emit! (remove-current-plugin plugin-id))
|
||||
(.error js/console "Error" e))))
|
||||
|
||||
(defn open-plugin!
|
||||
[{:keys [url] :as manifest}]
|
||||
(if url
|
||||
;; If the saved manifest has a URL we fetch the manifest to check
|
||||
;; for updates
|
||||
(->> (fetch-manifest url)
|
||||
(rx/subs!
|
||||
(fn [new-manifest]
|
||||
(let [new-manifest (merge new-manifest (select-keys manifest [:plugin-id]))]
|
||||
(cond
|
||||
(not= (:permissions new-manifest) (:permissions manifest))
|
||||
(modal/show!
|
||||
:plugin-permissions-update
|
||||
{:plugin new-manifest
|
||||
:on-accept
|
||||
#(do
|
||||
(preg/install-plugin! new-manifest)
|
||||
(load-plugin! new-manifest))})
|
||||
|
||||
(not= new-manifest manifest)
|
||||
(do (preg/install-plugin! new-manifest)
|
||||
(load-plugin! manifest))
|
||||
:else
|
||||
(load-plugin! manifest))))
|
||||
(fn []
|
||||
;; Error fetching the manifest we'll load the plugin with the
|
||||
;; old manifest
|
||||
(load-plugin! manifest))))
|
||||
(load-plugin! manifest)))
|
||||
|
||||
(defn close-plugin!
|
||||
[{:keys [plugin-id]}]
|
||||
(try
|
||||
(.ɵunloadPlugin ^js ug/global plugin-id)
|
||||
(catch :default e
|
||||
(.error js/console "Error" e))))
|
||||
|
||||
(defn close-current-plugin
|
||||
[]
|
||||
(ptk/reify ::close-current-plugin
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [ids (dm/get-in state [:workspace-local :open-plugins])]
|
||||
(doseq [id ids]
|
||||
(close-plugin! (preg/get-plugin id)))))))
|
||||
|
||||
(defn delay-open-plugin
|
||||
[plugin]
|
||||
(ptk/reify ::delay-open-plugin
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state ::open-plugin (:plugin-id plugin)))))
|
||||
|
||||
(defn check-open-plugin
|
||||
[]
|
||||
(ptk/reify ::check-open-plugin
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when-let [pid (::open-plugin state)]
|
||||
(open-plugin! (preg/get-plugin pid))
|
||||
(rx/of #(dissoc % ::open-plugin))))))
|
||||
@@ -24,10 +24,6 @@ target.stopCallback = function (e, element, combo) {
|
||||
return false
|
||||
}
|
||||
|
||||
if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('composedPath' in e && typeof e.composedPath === 'function') {
|
||||
// For open shadow trees, update `element` so that the following check works.
|
||||
const initialEventTarget = e.composedPath()[0];
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.persistence :as dps]
|
||||
[app.main.data.plugins :as dp]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.data.workspace.bool :as dwb]
|
||||
[app.main.data.workspace.collapse :as dwco]
|
||||
@@ -75,7 +74,6 @@
|
||||
[app.main.repo :as rp]
|
||||
[app.main.streams :as ms]
|
||||
[app.main.worker :as uw]
|
||||
[app.renderer-v2 :as renderer]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as ug]
|
||||
[app.util.http :as http]
|
||||
@@ -86,7 +84,6 @@
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.v2.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]
|
||||
[potok.v2.core :as ptk]
|
||||
[promesa.core :as p]))
|
||||
@@ -133,7 +130,6 @@
|
||||
(when (and (not (boolean (-> state :profile :props :v2-info-shown)))
|
||||
(features/active-feature? state "components/v2"))
|
||||
(modal/show :v2-info {}))
|
||||
(dp/check-open-plugin)
|
||||
(fdf/fix-deleted-fonts)
|
||||
(fbs/fix-broken-shapes)))))
|
||||
|
||||
@@ -357,9 +353,6 @@
|
||||
(dcm/retrieve-comment-threads file-id)
|
||||
(fetch-bundle project-id file-id))
|
||||
|
||||
(when (contains? cf/flags :renderer-v2)
|
||||
(rx/of (renderer/init)))
|
||||
|
||||
(->> stream
|
||||
(rx/filter dch/commit?)
|
||||
(rx/map deref)
|
||||
@@ -1546,8 +1539,7 @@
|
||||
(let [objects (wsh/lookup-page-objects state)
|
||||
selected (->> (wsh/lookup-selected state)
|
||||
(cfh/clean-loops objects))
|
||||
features (-> (features/get-team-enabled-features state)
|
||||
(set/difference cfeat/frontend-only-features))
|
||||
features (features/get-team-enabled-features state)
|
||||
|
||||
file-id (:current-file-id state)
|
||||
frame-id (cfh/common-parent-frame objects selected)
|
||||
@@ -1725,8 +1717,8 @@
|
||||
[:images [:set :map]]
|
||||
[:position {:optional true} ::gpt/point]])
|
||||
|
||||
(def paste-data-valid?
|
||||
(sm/lazy-validator schema:paste-data))
|
||||
(def validate-paste-data!
|
||||
(sm/validate-fn schema:paste-data))
|
||||
|
||||
(defn- paste-transit
|
||||
[{:keys [images] :as pdata}]
|
||||
@@ -1751,10 +1743,8 @@
|
||||
(let [file-id (:current-file-id state)
|
||||
features (features/get-team-enabled-features state)]
|
||||
|
||||
(when-not (paste-data-valid? pdata)
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-paste-data
|
||||
:hibt "invalid paste data found"))
|
||||
(validate-paste-data! pdata {:hint "invalid paste data"
|
||||
:code :invalid-paste-data})
|
||||
|
||||
(cfeat/check-paste-features! features (:features pdata))
|
||||
(if (= file-id (:file-id pdata))
|
||||
|
||||
@@ -465,16 +465,16 @@
|
||||
(defn change-color-in-selected
|
||||
[operations new-color old-color]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid color operations"
|
||||
(dm/verify!
|
||||
"expected valid change color operations"
|
||||
(check-change-color-operations! operations))
|
||||
|
||||
(dm/assert!
|
||||
"expected valid color structure"
|
||||
(dm/verify!
|
||||
"expected a valid color struct for new-color param"
|
||||
(ctc/check-color! new-color))
|
||||
|
||||
(dm/assert!
|
||||
"expected valid color structure"
|
||||
(dm/verify!
|
||||
"expected a valid color struct for old-color param"
|
||||
(ctc/check-color! old-color))
|
||||
|
||||
(ptk/reify ::change-color-in-selected
|
||||
@@ -498,7 +498,7 @@
|
||||
[color stroke?]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid color structure"
|
||||
"should be a valid color"
|
||||
(ctc/check-color! color))
|
||||
|
||||
(ptk/reify ::apply-color-from-palette
|
||||
|
||||
@@ -147,15 +147,11 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [page-id (or page-id (:current-page-id state))]
|
||||
(rx/of (dwsh/update-shapes [shape-id]
|
||||
(fn [shape]
|
||||
(cls/add-new-interaction shape interaction))
|
||||
{:page-id page-id})
|
||||
|
||||
(when (:destination interaction)
|
||||
(dwsh/update-shapes [(:destination interaction)]
|
||||
cls/show-in-viewer
|
||||
{:page-id page-id})))))))
|
||||
(rx/of (dwsh/update-shapes
|
||||
[shape-id]
|
||||
(fn [shape]
|
||||
(cls/add-new-interaction shape interaction))
|
||||
{:page-id page-id}))))))
|
||||
|
||||
(defn add-new-interaction
|
||||
([shape] (add-new-interaction shape nil))
|
||||
@@ -171,20 +167,15 @@
|
||||
flows (get page :objects)
|
||||
flow (ctp/get-frame-flow flows (:id frame))]
|
||||
(rx/concat
|
||||
(rx/of (dwsh/update-shapes
|
||||
[(:id shape)]
|
||||
(fn [shape]
|
||||
(let [new-interaction (-> ctsi/default-interaction
|
||||
(ctsi/set-destination destination)
|
||||
(assoc :position-relative-to (:id shape)))]
|
||||
(cls/add-new-interaction shape new-interaction))))
|
||||
|
||||
(when destination
|
||||
(dwsh/update-shapes [destination] cls/show-in-viewer))
|
||||
|
||||
(when (and (not (connected-frame? objects (:id frame)))
|
||||
(nil? flow))
|
||||
(add-flow (:id frame))))))))))
|
||||
(rx/of (dwsh/update-shapes [(:id shape)]
|
||||
(fn [shape]
|
||||
(let [new-interaction (-> ctsi/default-interaction
|
||||
(ctsi/set-destination destination)
|
||||
(assoc :position-relative-to (:id shape)))]
|
||||
(cls/add-new-interaction shape new-interaction)))))
|
||||
(when (and (not (connected-frame? objects (:id frame)))
|
||||
(nil? flow))
|
||||
(rx/of (add-flow (:id frame))))))))))
|
||||
|
||||
(defn remove-interaction
|
||||
([shape index]
|
||||
@@ -195,7 +186,8 @@
|
||||
(watch [_ _ _]
|
||||
(rx/of (dwsh/update-shapes [(:id shape)]
|
||||
(fn [shape]
|
||||
(update shape :interactions ctsi/remove-interaction index))
|
||||
(update shape :interactions
|
||||
ctsi/remove-interaction index))
|
||||
{:page-id page-id}))))))
|
||||
(defn update-interaction
|
||||
([shape index update-fn]
|
||||
@@ -204,16 +196,11 @@
|
||||
(ptk/reify ::update-interaction
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [interactions (ctsi/update-interaction (:interactions shape) index update-fn)
|
||||
interaction (nth interactions index)]
|
||||
(rx/of (dwsh/update-shapes
|
||||
[(:id shape)]
|
||||
(fn [shape]
|
||||
(assoc shape :interactions interactions))
|
||||
options)
|
||||
|
||||
(when (some? (:destination interaction))
|
||||
(dwsh/update-shapes [(:destination interaction)] cls/show-in-viewer options))))))))
|
||||
(rx/of (dwsh/update-shapes [(:id shape)]
|
||||
(fn [shape]
|
||||
(update shape :interactions
|
||||
ctsi/update-interaction index update-fn))
|
||||
options))))))
|
||||
|
||||
(defn remove-all-interactions-nav-to
|
||||
"Remove all interactions that navigate to the given frame."
|
||||
|
||||
@@ -193,17 +193,9 @@
|
||||
|
||||
(defn rename-color
|
||||
[file-id id new-name]
|
||||
(dm/assert!
|
||||
"expected valid uuid for `id`"
|
||||
(uuid? id))
|
||||
|
||||
(dm/assert!
|
||||
"expected valid uuid for `file-id`"
|
||||
(uuid? file-id))
|
||||
|
||||
(dm/assert!
|
||||
"expected valid string for `new-name`"
|
||||
(string? new-name))
|
||||
(dm/verify! (uuid? file-id))
|
||||
(dm/verify! (uuid? id))
|
||||
(dm/verify! (string? new-name))
|
||||
|
||||
(ptk/reify ::rename-color
|
||||
ptk/WatchEvent
|
||||
@@ -251,15 +243,8 @@
|
||||
|
||||
(defn rename-media
|
||||
[id new-name]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid uuid for `id`"
|
||||
(uuid? id))
|
||||
|
||||
(dm/assert!
|
||||
"expected valid string for `new-name`"
|
||||
(string? new-name))
|
||||
|
||||
(dm/verify! (uuid? id))
|
||||
(dm/verify! (string? new-name))
|
||||
(ptk/reify ::rename-media
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
@@ -276,11 +261,8 @@
|
||||
(rx/of (dch/commit-changes changes))))))))
|
||||
|
||||
(defn delete-media
|
||||
[{:keys [id]}]
|
||||
(dm/assert!
|
||||
"expected valid uuid for `id`"
|
||||
(uuid? id))
|
||||
|
||||
[{:keys [id] :as params}]
|
||||
(dm/assert! (uuid? id))
|
||||
(ptk/reify ::delete-media
|
||||
ev/Event
|
||||
(-data [_] {:id id})
|
||||
@@ -453,14 +435,8 @@
|
||||
(defn rename-component
|
||||
"Rename the component with the given id, in the current file library."
|
||||
[id new-name]
|
||||
(dm/assert!
|
||||
"expected an uuid instance"
|
||||
(uuid? id))
|
||||
|
||||
(dm/assert!
|
||||
"expected string for new-name"
|
||||
(string? new-name))
|
||||
|
||||
(dm/verify! (uuid? id))
|
||||
(dm/verify! (string? new-name))
|
||||
(ptk/reify ::rename-component
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
@@ -511,11 +487,8 @@
|
||||
|
||||
(defn delete-component
|
||||
"Delete the component with the given id, from the current file library."
|
||||
[{:keys [id]}]
|
||||
(dm/assert!
|
||||
"expected valid uuid for `id`"
|
||||
(uuid? id))
|
||||
|
||||
[{:keys [id] :as params}]
|
||||
(dm/assert! (uuid? id))
|
||||
(ptk/reify ::delete-component
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
@@ -1156,9 +1129,7 @@
|
||||
(defn touch-component
|
||||
"Update the modified-at attribute of the component to now"
|
||||
[id]
|
||||
(dm/assert!
|
||||
"expected valid uuid for `id`"
|
||||
(uuid? id))
|
||||
(dm/verify! (uuid? id))
|
||||
(ptk/reify ::touch-component
|
||||
cljs.core/IDeref
|
||||
(-deref [_] [id])
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
|
||||
selected-points (dm/get-in state [:workspace-local :edit-path id :selected-points] #{})
|
||||
|
||||
start-position (apply min-key #(gpt/distance start-position %) selected-points)
|
||||
start-position (apply min #(gpt/distance start-position %) selected-points)
|
||||
|
||||
content (st/get-path state :content)
|
||||
points (upg/content->points content)]
|
||||
|
||||
@@ -98,8 +98,8 @@
|
||||
(add-shape shape {}))
|
||||
([shape {:keys [no-select? no-update-layout?]}]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid shape"
|
||||
(dm/verify!
|
||||
"expected a valid shape"
|
||||
(cts/check-shape! shape))
|
||||
|
||||
(ptk/reify ::add-shape
|
||||
|
||||
@@ -6,11 +6,9 @@
|
||||
|
||||
(ns app.main.data.workspace.shortcuts
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.exports :as de]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.plugins :as dpl]
|
||||
[app.main.data.preview :as dp]
|
||||
[app.main.data.shortcuts :as ds]
|
||||
[app.main.data.users :as du]
|
||||
@@ -30,7 +28,6 @@
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.hooks.resize :as r]
|
||||
[app.util.dom :as dom]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -47,17 +44,6 @@
|
||||
(when-not (deref refs/workspace-read-only?)
|
||||
(run! st/emit! events)))
|
||||
|
||||
(def esc-pressed
|
||||
(ptk/reify ::esc-pressed
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(rx/of
|
||||
:interrupt
|
||||
(let [selection (dm/get-in state [:workspace-local :selected])]
|
||||
(if (empty? selection)
|
||||
(dpl/close-current-plugin)
|
||||
(dw/deselect-all true)))))))
|
||||
|
||||
;; Shortcuts format https://github.com/ccampbell/mousetrap
|
||||
|
||||
(def base-shortcuts
|
||||
@@ -125,7 +111,7 @@
|
||||
:escape {:tooltip (ds/esc)
|
||||
:command "escape"
|
||||
:subsections [:edit]
|
||||
:fn #(st/emit! esc-pressed)}
|
||||
:fn #(st/emit! :interrupt (dw/deselect-all true))}
|
||||
|
||||
|
||||
;; MODIFY LAYERS
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
(ns app.main.data.workspace.texts
|
||||
(:require
|
||||
["penpot/vendor/text-editor-v2" :as editor.v2]
|
||||
[app.common.attrs :as attrs]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
@@ -25,26 +24,14 @@
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.features :as features]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.util.router :as rt]
|
||||
[app.util.text-editor :as ted]
|
||||
[app.util.text.content.styles :as styles]
|
||||
[app.util.timers :as ts]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
;; -- V2 Editor Helpers
|
||||
|
||||
(def ^function create-editor editor.v2/create)
|
||||
(def ^function set-editor-root! editor.v2/setRoot)
|
||||
(def ^function get-editor-root editor.v2/getRoot)
|
||||
(def ^function dispose! editor.v2/dispose)
|
||||
|
||||
(declare v2-update-text-shape-content)
|
||||
(declare v2-update-text-editor-styles)
|
||||
|
||||
;; -- Editor
|
||||
|
||||
(defn update-editor
|
||||
@@ -199,41 +186,22 @@
|
||||
[{:keys [attrs shape]}]
|
||||
(shape-current-values shape txt/is-root-node? attrs))
|
||||
|
||||
(defn v2-current-text-values
|
||||
[{:keys [editor-instance attrs]}]
|
||||
(let [result (-> (.-currentStyle editor-instance)
|
||||
(styles/get-styles-from-style-declaration)
|
||||
(select-keys attrs))
|
||||
result (if (empty? result) txt/default-text-attrs result)]
|
||||
result))
|
||||
|
||||
(defn v1-current-paragraph-values
|
||||
(defn current-paragraph-values
|
||||
[{:keys [editor-state attrs shape]}]
|
||||
(if editor-state
|
||||
(-> (ted/get-editor-current-block-data editor-state)
|
||||
(select-keys attrs))
|
||||
(shape-current-values shape txt/is-paragraph-node? attrs)))
|
||||
|
||||
(defn current-paragraph-values
|
||||
[{:keys [editor-state editor-instance attrs shape] :as options}]
|
||||
(cond
|
||||
(some? editor-instance) (v2-current-text-values options)
|
||||
(some? editor-state) (v1-current-paragraph-values options)
|
||||
:else (shape-current-values shape txt/is-paragraph-node? attrs)))
|
||||
|
||||
(defn v1-current-text-values
|
||||
[{:keys [editor-state attrs]}]
|
||||
(let [result (-> (ted/get-editor-current-inline-styles editor-state)
|
||||
(select-keys attrs))
|
||||
result (if (empty? result) txt/default-text-attrs result)]
|
||||
result))
|
||||
|
||||
(defn current-text-values
|
||||
[{:keys [editor-state editor-instance attrs shape] :as options}]
|
||||
(cond
|
||||
(some? editor-instance) (v2-current-text-values options)
|
||||
(some? editor-state) (v1-current-text-values options)
|
||||
:else (shape-current-values shape txt/is-text-node? attrs)))
|
||||
[{:keys [editor-state attrs shape]}]
|
||||
(if editor-state
|
||||
(let [result (-> (ted/get-editor-current-inline-styles editor-state)
|
||||
(select-keys attrs))
|
||||
result (if (empty? result) txt/default-text-attrs result)]
|
||||
result)
|
||||
(shape-current-values shape txt/is-text-node? attrs)))
|
||||
|
||||
|
||||
;; --- TEXT EDITION IMPL
|
||||
|
||||
@@ -440,9 +408,7 @@
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when (or
|
||||
(and (features/active-feature? state "text-editor/v2") (nil? (:workspace-editor state)))
|
||||
(and (not (features/active-feature? state "text-editor/v2")) (nil? (get-in state [:workspace-editor-state id]))))
|
||||
(when (nil? (get-in state [:workspace-editor-state id]))
|
||||
(let [objects (wsh/lookup-page-objects state)
|
||||
shape (get objects id)
|
||||
|
||||
@@ -464,17 +430,8 @@
|
||||
(-> shape
|
||||
(dissoc :fills)
|
||||
(d/update-when :content update-content)))]
|
||||
(rx/of (dwsh/update-shapes shape-ids update-shape)))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(when (features/active-feature? state "text-editor/v2")
|
||||
(let [instance (:workspace-editor state)
|
||||
styles (some-> (editor.v2/getCurrentStyle instance)
|
||||
(styles/get-styles-from-style-declaration)
|
||||
((comp update-node-fn migrate-node))
|
||||
(styles/attrs->styles))]
|
||||
(editor.v2/applyStylesToSelection instance styles))))))
|
||||
(rx/of (dwsh/update-shapes shape-ids update-shape)))))))
|
||||
|
||||
;; --- RESIZE UTILS
|
||||
|
||||
@@ -707,36 +664,22 @@
|
||||
[id attrs]
|
||||
(ptk/reify ::update-attrs
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [text-editor-instance (:workspace-editor state)]
|
||||
(if (and (features/active-feature? state "text-editor/v2")
|
||||
(some? text-editor-instance))
|
||||
(rx/empty)
|
||||
(rx/concat
|
||||
(let [attrs (select-keys attrs txt/root-attrs)]
|
||||
(if-not (empty? attrs)
|
||||
(rx/of (update-root-attrs {:id id :attrs attrs}))
|
||||
(rx/empty)))
|
||||
(watch [_ _ _]
|
||||
(rx/concat
|
||||
(let [attrs (select-keys attrs txt/root-attrs)]
|
||||
(if-not (empty? attrs)
|
||||
(rx/of (update-root-attrs {:id id :attrs attrs}))
|
||||
(rx/empty)))
|
||||
|
||||
(let [attrs (select-keys attrs txt/paragraph-attrs)]
|
||||
(if-not (empty? attrs)
|
||||
(rx/of (update-paragraph-attrs {:id id :attrs attrs}))
|
||||
(rx/empty)))
|
||||
(let [attrs (select-keys attrs txt/paragraph-attrs)]
|
||||
(if-not (empty? attrs)
|
||||
(rx/of (update-paragraph-attrs {:id id :attrs attrs}))
|
||||
(rx/empty)))
|
||||
|
||||
(let [attrs (select-keys attrs txt/text-node-attrs)]
|
||||
(if-not (empty? attrs)
|
||||
(rx/of (update-text-attrs {:id id :attrs attrs}))
|
||||
(rx/empty)))
|
||||
|
||||
(when (features/active-feature? state "text-editor/v2")
|
||||
(rx/of (v2-update-text-editor-styles id attrs)))))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(when (features/active-feature? state "text-editor/v2")
|
||||
(let [instance (:workspace-editor state)
|
||||
styles (styles/attrs->styles attrs)]
|
||||
(editor.v2/applyStylesToSelection instance styles))))))
|
||||
(let [attrs (select-keys attrs txt/text-node-attrs)]
|
||||
(if-not (empty? attrs)
|
||||
(rx/of (update-text-attrs {:id id :attrs attrs}))
|
||||
(rx/empty)))))))
|
||||
|
||||
(defn update-all-attrs
|
||||
[ids attrs]
|
||||
@@ -830,52 +773,3 @@
|
||||
(rx/of (update-attrs (:id shape)
|
||||
{:typography-ref-id typ-id
|
||||
:typography-ref-file file-id}))))))))
|
||||
|
||||
;; -- New Editor
|
||||
|
||||
(defn v2-update-text-editor-styles
|
||||
[id new-styles]
|
||||
(ptk/reify ::v2-update-text-editor-styles
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [merged-styles (d/merge txt/default-text-attrs
|
||||
(get-in state [:workspace-global :default-font])
|
||||
new-styles)]
|
||||
(update-in state [:workspace-v2-editor-state id] (fnil merge {}) merged-styles)))))
|
||||
|
||||
(defn v2-update-text-shape-position-data
|
||||
[shape-id position-data]
|
||||
(ptk/reify ::v2-update-text-shape-position-data
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let []
|
||||
(update-in state [:workspace-text-modifier shape-id] {:position-data position-data})))))
|
||||
|
||||
(defn v2-update-text-shape-content
|
||||
([id content]
|
||||
(v2-update-text-shape-content id content false nil))
|
||||
([id content update-name?]
|
||||
(v2-update-text-shape-content id content update-name? nil))
|
||||
([id content update-name? name]
|
||||
(ptk/reify ::v2-update-text-shape-content
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [objects (wsh/lookup-page-objects state)
|
||||
shape (get objects id)
|
||||
modifiers (get-in state [:workspace-text-modifier id])
|
||||
new-shape? (nil? (:content shape))]
|
||||
(rx/of
|
||||
(dwsh/update-shapes
|
||||
[id]
|
||||
(fn [shape]
|
||||
(let [{:keys [width height position-data]} modifiers]
|
||||
(let [new-shape (-> shape
|
||||
(assoc :content content)
|
||||
(cond-> position-data
|
||||
(assoc :position-data position-data))
|
||||
(cond-> (and update-name? (some? name))
|
||||
(assoc :name name))
|
||||
(cond-> (or (some? width) (some? height))
|
||||
(gsh/transform-shape (ctm/change-size shape width height))))]
|
||||
new-shape)))
|
||||
{:undo-group (when new-shape? id)})))))))
|
||||
|
||||
@@ -109,8 +109,7 @@
|
||||
(watch [_ _ _]
|
||||
(when *assert*
|
||||
(->> (rx/from cfeat/no-migration-features)
|
||||
;; text editor v2 isn't enabled by default even in devenv
|
||||
(rx/filter #(not (or (contains? cfeat/backend-only-features %) (= "text-editor/v2" %))))
|
||||
(rx/filter #(not (contains? cfeat/backend-only-features %)))
|
||||
(rx/observe-on :async)
|
||||
(rx/map enable-feature))))
|
||||
|
||||
|
||||
@@ -184,9 +184,6 @@
|
||||
(def options-mode-global
|
||||
(l/derived :options-mode workspace-global))
|
||||
|
||||
(def default-font
|
||||
(l/derived :default-font workspace-global))
|
||||
|
||||
(def inspect-expanded
|
||||
(l/derived :inspect-expanded workspace-local))
|
||||
|
||||
@@ -358,9 +355,6 @@
|
||||
(def workspace-editor-state
|
||||
(l/derived :workspace-editor-state st/state))
|
||||
|
||||
(def workspace-v2-editor-state
|
||||
(l/derived :workspace-v2-editor-state st/state))
|
||||
|
||||
(def workspace-modifiers
|
||||
(l/derived :workspace-modifiers st/state =))
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
:dashboard-team-webhooks
|
||||
:dashboard-team-settings)
|
||||
[:?
|
||||
#_[:& app.main.ui.releases/release-notes-modal {:version "2.3"}]
|
||||
#_[:& app.main.ui.releases/release-notes-modal {:version "1.19"}]
|
||||
#_[:& app.main.ui.onboarding/onboarding-templates-modal]
|
||||
#_[:& app.main.ui.onboarding/onboarding-modal]
|
||||
#_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal]
|
||||
|
||||
@@ -483,8 +483,7 @@
|
||||
|
||||
;; Empty values means "submit" the form (whent some items have been added
|
||||
(when (and (kbd/enter? event) (str/empty? @value) (not-empty @items))
|
||||
(when (fn? on-submit)
|
||||
(on-submit form event)))
|
||||
(on-submit form))
|
||||
|
||||
;; If we have a string in the input we add it only if valid
|
||||
(when (and (valid-item-fn val) (not (str/empty? @value)))
|
||||
|
||||
@@ -30,4 +30,4 @@
|
||||
|
||||
(def workspace-read-only? (mf/create-context nil))
|
||||
(def is-component? (mf/create-context false))
|
||||
(def sidebar (mf/create-context nil))
|
||||
(def sidebar (mf/create-context nil))
|
||||
|
||||
@@ -8,15 +8,10 @@
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.dashboard.shortcuts :as sc]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as notif]
|
||||
[app.main.data.plugins :as dp]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.context :as ctx]
|
||||
@@ -30,16 +25,11 @@
|
||||
[app.main.ui.dashboard.team :refer [team-settings-page team-members-page team-invitations-page team-webhooks-page]]
|
||||
[app.main.ui.dashboard.templates :refer [templates-section]]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.workspace.plugins]
|
||||
[app.plugins.register :as preg]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
[app.util.router :as rt]
|
||||
[beicon.v2.core :as rx]
|
||||
[goog.events :as events]
|
||||
[okulary.core :as l]
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn ^boolean uuid-str?
|
||||
@@ -153,65 +143,6 @@
|
||||
(def dashboard-initialized
|
||||
(l/derived :current-team-id st/state))
|
||||
|
||||
(defn use-plugin-register
|
||||
[plugin-url team-id project-id]
|
||||
|
||||
(let [navegate-file!
|
||||
(fn [plugin {:keys [project-id id data]}]
|
||||
(st/emit!
|
||||
(dp/delay-open-plugin plugin)
|
||||
(rt/nav :workspace
|
||||
{:project-id project-id :file-id id}
|
||||
{:page-id (dm/get-in data [:pages 0])})))
|
||||
|
||||
create-file!
|
||||
(fn [plugin]
|
||||
(st/emit!
|
||||
(modal/hide)
|
||||
(let [data
|
||||
(with-meta
|
||||
{:project-id project-id
|
||||
:name (dm/str "Try plugin: " (:name plugin))}
|
||||
{:on-success (partial navegate-file! plugin)})]
|
||||
(-> (dd/create-file data)
|
||||
(with-meta {::ev/origin "plugin-try-out"})))))
|
||||
|
||||
open-try-out-dialog
|
||||
(fn [plugin]
|
||||
(modal/show
|
||||
:plugin-try-out
|
||||
{:plugin plugin
|
||||
:on-accept #(create-file! plugin)
|
||||
:on-close #(modal/hide!)}))
|
||||
|
||||
open-permissions-dialog
|
||||
(fn [plugin]
|
||||
(modal/show!
|
||||
:plugin-permissions
|
||||
{:plugin plugin
|
||||
:on-accept
|
||||
#(do (preg/install-plugin! plugin)
|
||||
(st/emit! (modal/hide)
|
||||
(rt/nav :dashboard-projects {:team-id team-id})
|
||||
(open-try-out-dialog plugin)))
|
||||
:on-close
|
||||
#(st/emit! (modal/hide)
|
||||
(rt/nav :dashboard-projects {:team-id team-id}))}))]
|
||||
|
||||
(mf/with-layout-effect
|
||||
[plugin-url team-id project-id]
|
||||
(when plugin-url
|
||||
(->> (dp/fetch-manifest plugin-url)
|
||||
(rx/subs!
|
||||
(fn [plugin]
|
||||
(if plugin
|
||||
(do
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "install-plugin" :name (:name plugin) :url plugin-url}))
|
||||
(open-permissions-dialog plugin))
|
||||
(st/emit! (notif/error "Cannot parser the plugin manifest"))))
|
||||
(fn [_]
|
||||
(st/emit! (notif/error "The plugin URL is incorrect")))))))))
|
||||
|
||||
(mf/defc dashboard
|
||||
{::mf/props :obj}
|
||||
[{:keys [route profile]}]
|
||||
@@ -219,12 +150,8 @@
|
||||
params (parse-params route)
|
||||
|
||||
project-id (:project-id params)
|
||||
|
||||
team-id (:team-id params)
|
||||
search-term (:search-term params)
|
||||
|
||||
plugin-url (-> route :query-params :plugin)
|
||||
|
||||
invite-email (-> route :query-params :invite-email)
|
||||
|
||||
teams (mf/deref refs/teams)
|
||||
@@ -233,8 +160,6 @@
|
||||
projects (mf/deref refs/dashboard-projects)
|
||||
project (get projects project-id)
|
||||
|
||||
default-project (->> projects vals (d/seek :is-default))
|
||||
|
||||
initialized? (mf/deref dashboard-initialized)]
|
||||
|
||||
(hooks/use-shortcuts ::dashboard sc/shortcuts)
|
||||
@@ -253,8 +178,6 @@
|
||||
(fn []
|
||||
(events/unlistenByKey key))))
|
||||
|
||||
(use-plugin-register plugin-url team-id (:id default-project))
|
||||
|
||||
[:& (mf/provider ctx/current-team-id) {:value team-id}
|
||||
[:& (mf/provider ctx/current-project-id) {:value project-id}
|
||||
;; NOTE: dashboard events and other related functions assumes
|
||||
@@ -283,3 +206,4 @@
|
||||
:search-term search-term
|
||||
:team team
|
||||
:invite-email invite-email}])])]]))
|
||||
|
||||
|
||||
@@ -19,8 +19,7 @@
|
||||
|
||||
(mf/defc search-page
|
||||
[{:keys [team search-term] :as props}]
|
||||
(let [search-term (or search-term "")
|
||||
result (mf/deref refs/dashboard-search-result)
|
||||
(let [result (mf/deref refs/dashboard-search-result)
|
||||
[rowref limit] (hooks/use-dynamic-grid-item-width)]
|
||||
|
||||
(mf/use-effect
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cfg]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.events :as ev]
|
||||
@@ -29,6 +30,7 @@
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[beicon.v2.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@@ -127,10 +129,17 @@
|
||||
]
|
||||
(filterv identity)))
|
||||
|
||||
(s/def ::emails (s/and ::us/set-of-valid-emails d/not-empty?))
|
||||
(s/def ::role ::us/keyword)
|
||||
(s/def ::team-id ::us/uuid)
|
||||
|
||||
(s/def ::invite-member-form
|
||||
(s/keys :req-un [::role ::emails ::team-id]))
|
||||
|
||||
(def ^:private schema:invite-member-form
|
||||
[:map {:title "InviteMemberForm"}
|
||||
[:role :keyword]
|
||||
[:emails [::sm/set {:min 1} ::sm/email]]
|
||||
[:emails [::sm/set {:kind ::sm/email :min 1}]]
|
||||
[:team-id ::sm/uuid]])
|
||||
|
||||
(mf/defc invite-members-modal
|
||||
@@ -172,14 +181,6 @@
|
||||
(st/emit! (ntf/error (tr "errors.profile-is-muted"))
|
||||
(modal/hide))
|
||||
|
||||
(and (= :validation type)
|
||||
(= :max-invitations-by-request code))
|
||||
(swap! error-text (tr "errors.maximum-invitations-by-request-reached" (:threshold error)))
|
||||
|
||||
(and (= :restriction type)
|
||||
(= :max-quote-reached code))
|
||||
(swap! error-text (tr "errors.max-quote-reached" (:target error)))
|
||||
|
||||
(or (= :member-is-muted code)
|
||||
(= :email-has-permanent-bounces code)
|
||||
(= :email-has-complaints code))
|
||||
@@ -225,9 +226,10 @@
|
||||
:name :emails
|
||||
:auto-focus? true
|
||||
:trim true
|
||||
:valid-item-fn sm/parse-email
|
||||
:valid-item-fn us/parse-email
|
||||
:caution-item-fn current-members-emails
|
||||
:label (tr "modals.invite-member.emails")
|
||||
:on-submit on-submit
|
||||
:invite-email invite-email}]]
|
||||
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
|
||||
@@ -195,17 +195,19 @@
|
||||
(fn [_event]
|
||||
(swap! collapsed* not)))
|
||||
|
||||
update-can-move
|
||||
(fn [scroll-left scroll-available client-width]
|
||||
(reset! can-move {:left (> scroll-left 0)
|
||||
:right (> scroll-available client-width)}))
|
||||
|
||||
on-scroll
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
(let [scroll (dom/get-target-scroll e)
|
||||
scroll-left (:scroll-left scroll)
|
||||
(let [scroll (dom/get-target-scroll e)
|
||||
scroll-left (:scroll-left scroll)
|
||||
scroll-available (- (:scroll-width scroll) scroll-left)
|
||||
client-rect (dom/get-client-size (dom/get-target e))
|
||||
client-width (unchecked-get client-rect "width")]
|
||||
|
||||
(reset! can-move {:left (> scroll-left 0)
|
||||
:right (> scroll-available client-width)}))))
|
||||
client-rect (dom/get-client-size (dom/get-target e))]
|
||||
(update-can-move scroll-left scroll-available (unchecked-get client-rect "width")))))
|
||||
|
||||
on-move-left
|
||||
(mf/use-fn #(move-left))
|
||||
@@ -229,7 +231,7 @@
|
||||
(let [content (mf/ref-val content-ref)]
|
||||
(when (and (some? content) (some? templates))
|
||||
(dom/scroll-to content #js {:behavior "instant" :left 0 :top 0})
|
||||
(dom/dispatch-event content (dom/event "scroll")))))
|
||||
(.dispatchEvent content (js/Event. "scroll")))))
|
||||
|
||||
(mf/with-effect [profile collapsed]
|
||||
(swap! storage/global assoc ::collapsed collapsed)
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.notifications.context-notification :refer [context-notification]]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[potok.v2.core :as ptk]
|
||||
@@ -57,7 +57,7 @@
|
||||
(def ^:private schema:invite-form
|
||||
[:map {:title "InviteForm"}
|
||||
[:role :keyword]
|
||||
[:emails {:optional true} [::sm/set ::sm/email]]])
|
||||
[:emails {:optional true} [::sm/set {:kind ::sm/email}]]])
|
||||
|
||||
(defn- get-available-roles
|
||||
[]
|
||||
@@ -67,14 +67,17 @@
|
||||
(mf/defc team-form-step-2
|
||||
{::mf/props :obj}
|
||||
[{:keys [name on-back go-to-team?]}]
|
||||
(let [initial (mf/with-memo []
|
||||
{:role "editor" :name name})
|
||||
(let [initial (mf/use-memo
|
||||
#(do {:role "editor"
|
||||
:name name}))
|
||||
|
||||
form (fm/use-form :schema schema:invite-form
|
||||
:initial initial)
|
||||
|
||||
params (:clean-data @form)
|
||||
emails (:emails params)
|
||||
|
||||
roles (mf/use-memo get-available-roles)
|
||||
error* (mf/use-state nil)
|
||||
|
||||
on-success
|
||||
(mf/use-fn
|
||||
@@ -87,28 +90,8 @@
|
||||
|
||||
on-error
|
||||
(mf/use-fn
|
||||
(fn [cause]
|
||||
(let [{:keys [type code] :as error} (ex-data cause)]
|
||||
(cond
|
||||
(and (= :validation type)
|
||||
(= :profile-is-muted code))
|
||||
(swap! error* (tr "errors.profile-is-muted"))
|
||||
|
||||
(and (= :validation type)
|
||||
(= :max-invitations-by-request code))
|
||||
(swap! error* (tr "errors.maximum-invitations-by-request-reached" (:threshold error)))
|
||||
|
||||
(and (= :restriction type)
|
||||
(= :max-quote-reached code))
|
||||
(swap! error* (tr "errors.max-quote-reached" (:target error)))
|
||||
|
||||
(or (= :member-is-muted code)
|
||||
(= :email-has-permanent-bounces code)
|
||||
(= :email-has-complaints code))
|
||||
(swap! error* (tr "errors.email-spam-or-permanent-bounces" (:email error)))
|
||||
|
||||
:else
|
||||
(swap! error* (tr "errors.generic"))))))
|
||||
(fn [_]
|
||||
(st/emit! (ntf/error (tr "errors.generic")))))
|
||||
|
||||
on-invite-later
|
||||
(mf/use-fn
|
||||
@@ -128,7 +111,7 @@
|
||||
|
||||
on-invite-now
|
||||
(mf/use-fn
|
||||
(fn [{:keys [name emails] :as params}]
|
||||
(fn [{:keys [name] :as params}]
|
||||
(let [mdata {:on-success on-success
|
||||
:on-error on-error}]
|
||||
|
||||
@@ -160,10 +143,6 @@
|
||||
[:& fm/form {:form form
|
||||
:class (stl/css :modal-form-invitations)
|
||||
:on-submit on-submit}
|
||||
|
||||
(when-let [content (deref error*)]
|
||||
[:& context-notification {:content content :level :error}])
|
||||
|
||||
[:div {:class (stl/css :role-select)}
|
||||
[:p {:class (stl/css :role-title)} (tr "onboarding.choice.team-up.roles")]
|
||||
[:& fm/select {:name :role :options roles}]]
|
||||
@@ -176,22 +155,18 @@
|
||||
:valid-item-fn sm/parse-email
|
||||
:caution-item-fn #{}
|
||||
:label (tr "modals.invite-member.emails")
|
||||
;; :on-submit on-submit
|
||||
}]]
|
||||
:on-submit on-submit}]]
|
||||
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
[:button {:class (stl/css :back-button)
|
||||
:on-click on-back}
|
||||
(tr "labels.back")]
|
||||
|
||||
(let [params (:clean-data @form)
|
||||
emails (:emails params)]
|
||||
[:> fm/submit-button*
|
||||
{:class (stl/css :accept-button)
|
||||
:label (if (> (count emails) 0)
|
||||
(tr "onboarding.choice.team-up.create-team-and-invite")
|
||||
(tr "onboarding.choice.team-up.create-team-without-invite"))}])]
|
||||
|
||||
[:> fm/submit-button*
|
||||
{:class (stl/css :accept-button)
|
||||
:label (if (> (count emails) 0)
|
||||
(tr "onboarding.choice.team-up.create-team-and-invite")
|
||||
(tr "onboarding.choice.team-up.create-team-without-invite"))}]]
|
||||
[:div {:class (stl/css :modal-hint)}
|
||||
"(" (tr "onboarding.choice.team-up.create-team-and-send-invites-description") ")"]]]
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
[app.main.ui.releases.v2-0]
|
||||
[app.main.ui.releases.v2-1]
|
||||
[app.main.ui.releases.v2-2]
|
||||
[app.main.ui.releases.v2-3]
|
||||
[app.util.object :as obj]
|
||||
[app.util.timers :as tm]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.releases.v2-3
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.ui.releases.common :as c]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; TODO: Review all copies and alt text
|
||||
(defmethod c/render-release-notes "2.3"
|
||||
[{:keys [slide klass next finish navigate version]}]
|
||||
(mf/html
|
||||
(case slide
|
||||
:start
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.3-slide-0.png"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "A graphic illustration with Penpot style"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"What’s new in Penpot?"]
|
||||
|
||||
[:div {:class (stl/css :version-tag)}
|
||||
(dm/str "Version " version)]]
|
||||
|
||||
[:div {:class (stl/css :features-block)}
|
||||
[:span {:class (stl/css :feature-title)}
|
||||
"Penpot can now be extended by using Plugins!"]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"The introduction of our brand new Plugin system allows you to access even richer ecosystem of capabilities."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"We are beyond excitement about how this will further involve the Penpot community in building the best design and prototyping platform."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Let’s dive in!"]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:button {:class (stl/css :next-btn)
|
||||
:on-click next} "Continue"]]]]]]
|
||||
|
||||
0
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.3-img-slide-1.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Build Plugins to enhance your workflow"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"Build Plugins and enhance your workflow"]]
|
||||
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Penpot Plugins encourage developers to easily customize and expand the platform using standard web technologies like JavaScript, CSS, and HTML."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Find everything you need in ouor full comprehensive documentation to start building your plugins now!"]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 2}]
|
||||
|
||||
[:button {:on-click next
|
||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||
|
||||
|
||||
1
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.3-img-slide-2.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Plugins are safe and extremely easy to use"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"Plugins are safe and extremely easy to use"]]
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Penpot plugins are quite easy to install."]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Be sure to keep an eye on our evolving " [:a {:href "https://penpot.app/penpothub" :target "_blank"} "Penpot Hub"] " to pick the ones that are best suited to enhance your workflow."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"This is just the beginning of a myriad of possibilities. Let’s build this community together <3."]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 2}]
|
||||
|
||||
[:button {:on-click finish
|
||||
:class (stl/css :next-btn)} "Let's go"]]]]]])))
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@import "refactor/common-refactor.scss";
|
||||
|
||||
.modal-overlay {
|
||||
@extend .modal-overlay-base;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
display: grid;
|
||||
grid-template-columns: $s-324 1fr;
|
||||
height: $s-500;
|
||||
width: $s-888;
|
||||
border-radius: $br-8;
|
||||
background-color: var(--modal-background-color);
|
||||
border: $s-2 solid var(--modal-border-color);
|
||||
}
|
||||
|
||||
.start-image {
|
||||
width: $s-324;
|
||||
border-radius: $br-8 0 0 $br-8;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: $s-40;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr $s-32;
|
||||
gap: $s-24;
|
||||
|
||||
a {
|
||||
color: var(--button-primary-background-color-rest);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: grid;
|
||||
gap: $s-8;
|
||||
}
|
||||
|
||||
.version-tag {
|
||||
@include flexCenter;
|
||||
@include headlineSmallTypography;
|
||||
height: $s-32;
|
||||
width: $s-96;
|
||||
background-color: var(--communication-tag-background-color);
|
||||
color: var(--communication-tag-foreground-color);
|
||||
border-radius: $br-8;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include headlineLargeTypography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.features-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-16;
|
||||
width: $s-440;
|
||||
}
|
||||
|
||||
.feature {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-8;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
@include bodyLargeTypography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.feature-content {
|
||||
@include bodyMediumTypography;
|
||||
margin: 0;
|
||||
color: var(--modal-text-foreground-color);
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
@include bodyMediumTypography;
|
||||
color: var(--modal-text-foreground-color);
|
||||
list-style: disc;
|
||||
display: grid;
|
||||
gap: $s-8;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-areas: "bullets button";
|
||||
}
|
||||
|
||||
.next-btn {
|
||||
@extend .button-primary;
|
||||
width: $s-100;
|
||||
justify-self: flex-end;
|
||||
grid-area: button;
|
||||
}
|
||||
@@ -8,7 +8,6 @@
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.users :as du]
|
||||
@@ -17,7 +16,6 @@
|
||||
[app.util.router :as rt]
|
||||
[beicon.v2.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(s/def ::page-id ::us/uuid)
|
||||
@@ -96,11 +94,10 @@
|
||||
(defn on-navigate
|
||||
[router path]
|
||||
(let [location (.-location js/document)
|
||||
[base-path qs] (str/split path "?")
|
||||
location-path (dm/str (.-origin location) (.-pathname location))
|
||||
valid-location? (= location-path (dm/str cf/public-uri))
|
||||
match (match-path router path)
|
||||
empty-path? (or (= base-path "") (= base-path "/"))]
|
||||
empty-path? (or (= path "") (= path "/"))]
|
||||
(cond
|
||||
(not valid-location?)
|
||||
(st/emit! (rt/assign-exception {:type :not-found}))
|
||||
@@ -119,7 +116,7 @@
|
||||
(st/emit! (rt/nav :auth-login))
|
||||
|
||||
empty-path?
|
||||
(st/emit! (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)} (u/query-string->map qs)))
|
||||
(st/emit! (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)}))
|
||||
|
||||
:else
|
||||
(st/emit! (rt/assign-exception {:type :not-found})))))))))
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
(mf/defc export-flows
|
||||
[{:keys [flows]}]
|
||||
[:> "penpot:flows" #js {}
|
||||
(for [{:keys [id name starting-frame]} (vals flows)]
|
||||
(for [{:keys [id name starting-frame]} flows]
|
||||
[:> "penpot:flow" #js {:id id
|
||||
:name name
|
||||
:starting-frame starting-frame}])])
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.shapes.bounds :as gsb]
|
||||
[app.common.math :as mth]
|
||||
[app.common.uuid :as uuid]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
@@ -130,34 +129,6 @@
|
||||
[filters]
|
||||
(map #(assoc %1 :filter-in %2) filters (cons nil (map :id filters))))
|
||||
|
||||
(defn filter-coords
|
||||
[bounds selrect padding]
|
||||
(if (or (mth/close? 0.01 (:width selrect))
|
||||
(mth/close? 0.01 (:height selrect)))
|
||||
|
||||
;; We cannot use "objectBoundingbox" if the shape doesn't have width/heigth
|
||||
;; From the SVG spec (https://www.w3.org/TR/SVG11/coords.html#ObjectBoundingBox
|
||||
;; Keyword objectBoundingBox should not be used when the geometry of the applicable element
|
||||
;; has no width or no height, such as the case of a horizontal or vertical line, even when
|
||||
;; the line has actual thickness when viewed due to having a non-zero stroke width since
|
||||
;; stroke width is ignored for bounding box calculations. When the geometry of the
|
||||
;; applicable element has no width or height and objectBoundingBox is specified, then
|
||||
;; the given effect (e.g., a gradient or a filter) will be ignored.
|
||||
(let [filter-width (+ (:width bounds) (* 2 (:horizontal padding)))
|
||||
filter-height (+ (:height bounds) (* 2 (:vertical padding)))
|
||||
filter-x (- (:x bounds) #_(:x selrect) (:horizontal padding))
|
||||
filter-y (- (:y bounds) #_(:y selrect) (:vertical padding))
|
||||
filter-units "userSpaceOnUse"]
|
||||
[filter-x filter-y filter-width filter-height filter-units])
|
||||
|
||||
;; If the width/height is not zero we use objectBoundingBox as it's more stable
|
||||
(let [filter-width (/ (+ (:width bounds) (* 2 (:horizontal padding))) (:width selrect))
|
||||
filter-height (/ (+ (:height bounds) (* 2 (:vertical padding))) (:height selrect))
|
||||
filter-x (/ (- (:x bounds) (:x selrect) (:horizontal padding)) (:width selrect))
|
||||
filter-y (/ (- (:y bounds) (:y selrect) (:vertical padding)) (:height selrect))
|
||||
filter-units "objectBoundingBox"]
|
||||
[filter-x filter-y filter-width filter-height filter-units])))
|
||||
|
||||
(mf/defc filters
|
||||
[{:keys [filter-id shape]}]
|
||||
|
||||
@@ -165,17 +136,17 @@
|
||||
bounds (gsb/get-rect-filter-bounds (:selrect shape) filters (or (-> shape :blur :value) 0))
|
||||
padding (gsb/calculate-padding shape)
|
||||
selrect (:selrect shape)
|
||||
|
||||
[filter-x filter-y filter-width filter-height filter-units]
|
||||
(filter-coords bounds selrect padding)]
|
||||
|
||||
filter-x (/ (- (:x bounds) (:x selrect) (:horizontal padding)) (:width selrect))
|
||||
filter-y (/ (- (:y bounds) (:y selrect) (:vertical padding)) (:height selrect))
|
||||
filter-width (/ (+ (:width bounds) (* 2 (:horizontal padding))) (:width selrect))
|
||||
filter-height (/ (+ (:height bounds) (* 2 (:vertical padding))) (:height selrect))]
|
||||
(when (> (count filters) 2)
|
||||
[:filter {:id filter-id
|
||||
:x filter-x
|
||||
:y filter-y
|
||||
:width filter-width
|
||||
:height filter-height
|
||||
:filterUnits filter-units
|
||||
:filterUnits "objectBoundingBox"
|
||||
:color-interpolation-filters "sRGB"}
|
||||
(for [[index entry] (d/enumerate filters)]
|
||||
[:& filter-entry {:key (dm/str filter-id "-" index)
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.shapes.attrs :as attrs]
|
||||
[app.main.ui.shapes.custom-stroke :refer [shape-fills shape-strokes]]
|
||||
[app.main.ui.shapes.filters :as filters]
|
||||
[app.util.debug :as dbg]
|
||||
[app.util.object :as obj]
|
||||
[rumext.v2 :as mf]))
|
||||
@@ -66,11 +65,6 @@
|
||||
|
||||
render-id (mf/use-ctx muc/render-id)
|
||||
|
||||
filter-id-blur (dm/fmt "filter-blur-%" render-id)
|
||||
filter-id-shadows (dm/fmt "filter-shadow-%" render-id)
|
||||
filter-str-blur (filters/filter-str filter-id-blur shape)
|
||||
filter-str-shadows (filters/filter-str filter-id-shadows shape)
|
||||
|
||||
x (dm/get-prop shape :x)
|
||||
y (dm/get-prop shape :y)
|
||||
w (dm/get-prop shape :width)
|
||||
@@ -92,37 +86,29 @@
|
||||
:className "frame-background"})))
|
||||
path? (some? (.-d props))]
|
||||
|
||||
;; We need to separate blur from shadows because the blur is applied to the strokes
|
||||
;; while the shadows have to be placed *under* the stroke (for example, the inner shadows)
|
||||
;; and the shadows needs to be applied only to the content (without the stroke)
|
||||
[:g {:filter filter-str-blur}
|
||||
[:defs
|
||||
[:& filters/filters {:shape (dissoc shape :blur) :filter-id filter-id-shadows}]
|
||||
[:& filters/filters {:shape (assoc shape :shadow []) :filter-id filter-id-blur}]]
|
||||
[:*
|
||||
[:g {:clip-path (when-not ^boolean show-content?
|
||||
(frame-clip-url shape render-id))
|
||||
;; A frame sets back normal fill behavior (default
|
||||
;; transparent). It may have been changed to default black
|
||||
;; if a shape coming from an imported SVG file is
|
||||
;; rendered. See main.ui.shapes.attrs/add-style-attrs.
|
||||
:fill "none"
|
||||
:opacity opacity}
|
||||
|
||||
;; This need to be separated in two layers so the clip doesn't affect the shadow filters
|
||||
;; otherwise the shadow will be clipped and not visible
|
||||
[:g {:filter filter-str-shadows}
|
||||
[:g {:clip-path (when-not ^boolean show-content? (frame-clip-url shape render-id))
|
||||
;; A frame sets back normal fill behavior (default
|
||||
;; transparent). It may have been changed to default black
|
||||
;; if a shape coming from an imported SVG file is
|
||||
;; rendered. See main.ui.shapes.attrs/add-style-attrs.
|
||||
:fill "none"
|
||||
:opacity opacity}
|
||||
[:& shape-fills {:shape shape}
|
||||
(if ^boolean path?
|
||||
[:> :path props]
|
||||
[:> :rect props])]
|
||||
|
||||
[:& shape-fills {:shape shape}
|
||||
(if ^boolean path?
|
||||
[:> :path props]
|
||||
[:> :rect props])]
|
||||
|
||||
children]]
|
||||
children]
|
||||
|
||||
[:& shape-strokes {:shape shape}
|
||||
(if ^boolean path?
|
||||
[:> :path props]
|
||||
[:> :rect props])]]))
|
||||
|
||||
|
||||
(mf/defc frame-thumbnail-image
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user