Compare commits

...

52 Commits

Author SHA1 Message Date
Francis Santiago
6703402207 Revert "add new configurations" 2025-05-12 16:47:43 +02:00
Francis Santiago
f93059412a Merge pull request #6463 from sancfc/develop
add new configurations
2025-05-12 16:04:01 +02:00
Pablo Alba
294ce7bb1b 🐛 Fix variants override for nested components (#6421) 2025-05-12 15:50:06 +02:00
Andrey Antukh
a558bfdb2f Merge remote-tracking branch 'origin/staging' into develop 2025-05-12 15:16:19 +02:00
Andrey Antukh
86bcd1b681 🐛 Fix issue on shortcuts restore operation (#6462)
* 🐛 Fix issue on shortcuts restore operation

Happens when the order of shortcuts pop events is inconsistent with
push events. Using less strictly order policy for pop operations
allows relax this and make it eventually consistent.

* 💄 Add cosmetic changes on shortcuts hooks on colorpicker and    wport

* 📎 Update changelog

* 📎 Add PR feedback changes
2025-05-12 15:08:14 +02:00
Elena Torró
33c260c35b Merge pull request #6456 from penpot/alotor-perf-text-grow-2
 Reflow flex on grow text height
2025-05-12 14:16:29 +02:00
Andrey Antukh
94312bb35c Merge remote-tracking branch 'origin/staging' into develop 2025-05-12 13:44:24 +02:00
Andrey Antukh
70b1989f10 Merge tag '2.7.0-RC2' 2025-05-12 13:11:40 +02:00
Xavier Julian
c0eaa75232 💄 Fix errors UI on input token for value 2025-05-12 12:03:23 +02:00
Alejandro Alonso
dbb9971482 Merge pull request #6351 from penpot/niwinz-develop-improve-cleaner
 Add cleaner to file-gc
2025-05-12 11:52:01 +02:00
Alejandro Alonso
0828994840 Merge pull request #6419 from penpot/niwinz-refactor-library
♻️ Refactor penpot library
2025-05-12 11:47:00 +02:00
ºelhombretecla
e6b5618bd3 🎉 Add 2.7 release slides (#6440) 2025-05-12 11:38:00 +02:00
Aitor Moreno
9c24d3a521 Merge pull request #6370 from penpot/superalex-improve-zoom-in-zoom-out-performance-2
🎉 Improve zoom in/out performance
2025-05-12 11:22:57 +02:00
Alejandro Alonso
480e0887e3 🎉 Improve zoom in/out performance 2025-05-12 11:10:21 +02:00
Alejandro Alonso
e0e381bdfc Merge pull request #6451 from penpot/niwinz-develop-features-binfile-v1
🐛 Apply migrations in correct order for binfile-v1
2025-05-12 11:08:10 +02:00
Alejandro Alonso
5199b306aa Merge pull request #6446 from penpot/niwinz-staging-bugfixes-2
🐛 Properly propagate export errors from worker to main thread
2025-05-12 10:47:46 +02:00
Alejandro Alonso
8febfaa21e Merge pull request #6447 from penpot/niwinz-staging-bugfixes
 Ensure read-only mode on non-workspace access to file
2025-05-12 10:43:02 +02:00
Aitor Moreno
69062f03ee Merge pull request #6449 from penpot/superalex-add-shapes-buffer
🎉 Add shapes buffer to improve memory allocation
2025-05-12 10:23:34 +02:00
alonso.torres
eb04fa19e1 Reflow flex on grow text height 2025-05-12 09:48:57 +02:00
Alejandro Alonso
03b4fe3558 🎉 Add shapes buffer to improve memory allocation 2025-05-09 15:00:02 +02:00
Andrey Antukh
b349d08155 🐛 Apply migrations in correct order for binfile-v1
The patch was already existed but only applied to binfile-v3,
with this commit, the fix is properly applied to all binfile
formats and for duplicate file operation.
2025-05-09 13:38:13 +02:00
andrés gonzález
5e675dbf0b 📚 Update changelog for 2.7 (#6448) 2025-05-09 12:20:39 +02:00
Andrey Antukh
fc5d9659d6 🐛 Properly propagate export errors from worker to main thread 2025-05-09 11:15:04 +02:00
Andrey Antukh
bc20598b3d Don't persist file on several read operations
after applying migrations
2025-05-08 19:15:28 +02:00
Andrey Antukh
9de8ebb52c Add read-only option for files/get-file 2025-05-08 19:12:50 +02:00
Andrey Antukh
694a2084e2 Add file cleaner to file-gc process 2025-05-08 13:35:25 +02:00
Andrey Antukh
fef19a3c80 Add legacy flex dir cleaner 2025-05-08 13:35:25 +02:00
Andrey Antukh
3da8b945ca 📎 Don't send unnecesary features to worker 2025-05-08 13:35:24 +02:00
Andrey Antukh
8f27b82edd Extend cleaner to fix invalid root shapes 2025-05-08 13:34:48 +02:00
Andrey Antukh
0b7b6e2c23 ♻️ Refactor penpot library 2025-05-08 09:51:25 +02:00
Andrey Antukh
8bdec66927 Remove the ILazySchema internal abstraction from schema ns 2025-05-07 12:17:24 +02:00
Andrey Antukh
66ee9edaf8 Add minor enhacements and naming fixes on schemas 2025-05-07 12:17:24 +02:00
Andrey Antukh
ffd7bc883d ⬆️ Update shadow-cljs to 3.0.3 on common and frontend 2025-05-07 12:17:23 +02:00
Andrey Antukh
1bcfa4b8dc 🎉 Add facility to define custom js class 2025-05-07 12:17:23 +02:00
Andrey Antukh
99e325acaf 🔥 Remove support from legacy-zip format 2025-05-07 12:14:52 +02:00
Andrey Antukh
8badd1f2eb 💄 Add cosmetic improvements to common scripts/repl
Make it consistent with backend scripts/repl
2025-05-07 12:14:51 +02:00
Francis Santiago
a65aa5ea44 Merge pull request #1 from sancfc/github-actions-bundle-build
GitHub actions bundle build
2025-05-07 10:57:56 +02:00
fsantiago
689063cfb2 add initial functionality 2025-05-07 10:05:54 +02:00
fsantiago
e146ce7be4 🧪 temporary debug test 2025-05-07 10:05:17 +02:00
fsantiago
0fbd9812b3 🧪 basic execution test 2025-05-07 10:05:07 +02:00
fsantiago
ccd7b3bdce 🔧 🧹 minor code cleanup 2025-05-07 10:04:58 +02:00
fsantiago
60f8cfd492 add more tests for zip flow 2025-05-07 10:04:41 +02:00
fsantiago
7359b800ce 🔧 🧹 reorganize zip type logic 2025-05-07 10:04:29 +02:00
fsantiago
0032639831 🐛 🐛 correct zip type validation 2025-05-07 10:04:13 +02:00
fsantiago
d9cdd020e6 ♻️ ♻️ improve zip type definition 2025-05-07 10:04:01 +02:00
fsantiago
10d021b15e implement zip type logic 2025-05-07 10:03:48 +02:00
fsantiago
3be750410e 🔧 🔧 update file paths in project 2025-05-07 10:03:36 +02:00
fsantiago
47552830b1 🧪 add temporary integration test 2025-05-07 10:03:24 +02:00
fsantiago
0fb41f54b0 add initial tests for zip feature 2025-05-07 10:02:56 +02:00
fsantiago
5b777921a6 📝 📝 update reference to PENPOT 2025-05-07 10:02:43 +02:00
fsantiago
42dcc81767 🔧 🔧 add initial pipeline configuration 2025-05-07 10:01:56 +02:00
Andrey Antukh
c2b13a6d5d 📚 Update changelog 2025-04-29 14:46:15 +02:00
86 changed files with 2080 additions and 4219 deletions

View File

@@ -6,6 +6,36 @@
### :boom: Breaking changes & Deprecations
**Breaking changes on penpot library:**
- Change the signature of the `addPage` method: it now accepts an object (as a single argument) where you can pass `id`,
`name`, and `background` props (instead of the previous positional arguments)
- Rename the `file.createRect` method to `file.addRect`
- Rename the `file.createCircle` method to `file.addCircle`
- Rename the `file.createPath` method to `file.addPath`
- Rename the `file.createText` method to `file.addText`
- Rename `file.startComponent` to `file.addComponent` (to preserve the naming style)
- Rename `file.createComponentInstance` to `file.addComponentInstance` (to preserve the naming style)
- Rename `file.lookupShape` to `file.getShape`
- Rename `file.asMap` to `file.toMap`
- Remove `file.updateLibraryColor` (use `file.addLibraryColor` if you just need to replace a color)
- Remove `file.deleteLibraryColor` (this library is intended to build files)
- Remove `file.updateLibraryTypography` (use `file.addLibraryTypography` if you just need to replace a typography)
- Remove `file.deleteLibraryTypography` (this library is intended to build files)
- Remove `file.add/update/deleteLibraryMedia` (they are no longer supported by Penpot and have been replaced by components)
- Remove `file.deleteObject` (this library is intended to build files)
- Remove `file.updateObject` (this library is intended to build files)
- Remove `file.finishComponent` (it is no longer necessary; see below for more details on component creation changes)
- Change the `file.getCurrentPageId` function to a read-only `file.currentPageId` property
- Add `file.currentFrameId` read-only property
- Add `file.lastId` read-only property
There are also relevant semantic changes in how components should be created: this refactor removes
all notions of the old components (v1). Since v2, the shapes that are part of a component live on a
page. So, from now on, to create a component, you should first create a frame, then add shapes
and/or groups to that frame, and then create a component by declaring that frame as the component
root.
### :heart: Community contributions (Thank you!)
### :sparkles: New features
@@ -23,6 +53,8 @@
### :heart: Community contributions (Thank you!)
- Design improvements to the Invitations page with an empty state [GitHub #2608](https://github.com/penpot/penpot/issues/2608) by [@iprithvitharun](https://github.com/iprithvitharun)
### :sparkles: New features
- Update board presets with a newer devices [Taiga #10610](https://tree.taiga.io/project/penpot/us/10610)
@@ -32,9 +64,10 @@
- Add set selection in create Token themes flow [Taiga #10746](https://tree.taiga.io/project/penpot/issue/10746)
- Display indicator on not active sets [Taiga #10668](https://tree.taiga.io/project/penpot/issue/10668)
- Create `input*` wrapper component, and `label*`, `input-field*` and `hint-message*` components [Taiga #10713](https://tree.taiga.io/project/penpot/us/10713)
- Fix problem in viewer with the back button [Taiga #10907](https://tree.taiga.io/project/penpot/issue/10907)
### :bug: Bugs fixed
- Fix problem in viewer with the back button [Taiga #10907](https://tree.taiga.io/project/penpot/issue/10907)
- Fix resize bar background on tokens panel [Taiga #10811](https://tree.taiga.io/project/penpot/issue/10811)
- Fix shortcut for history version panel [Taiga #11006](https://tree.taiga.io/project/penpot/issue/11006)
- Fix positioning of comment drafts when near the right / bottom edges of viewport [Taiga #10534](https://tree.taiga.io/project/penpot/issue/10534)
@@ -59,8 +92,9 @@
- Fix Color should preserve color space [Github #69](https://github.com/tokens-studio/penpot/issues/69)
- Fix cannot rename Design Token Sets when group of same name exists [Taiga Issue #10773](https://tree.taiga.io/project/penpot/issue/10773)
- Fix problem when duplicating grid layout [Github #6391](https://github.com/penpot/penpot/issues/6391)
- Fix issue that makes workspace shortcuts stop working [Taiga #11062](https://tree.taiga.io/project/penpot/issue/11062)
## 2.6.2 (Unreleased)
## 2.6.2
### :bug: Bugs fixed

View File

@@ -9,6 +9,7 @@
for recently imported shapes."
(:require
[app.common.data :as d]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -55,9 +56,52 @@
(fn [shadows]
(into [] xform shadows)))))
(defn- fix-root-shape
"Ensure all root objects are well formed shapes"
[shape]
(if (= (:id shape) uuid/zero)
(-> shape
(assoc :parent-id uuid/zero)
(assoc :frame-id uuid/zero)
;; We explicitly dissoc them and let the shape-setup
;; to regenerate it with valid values.
(dissoc :selrect)
(dissoc :points)
(cts/setup-shape))
shape))
(defn- fix-legacy-flex-dir
"This operation is only relevant to old data and it is fixed just
for convenience."
[shape]
(d/update-when shape :layout-flex-dir
(fn [dir]
(case dir
:reverse-row :row-reverse
:reverse-column :column-reverse
dir))))
(defn clean-shape-post-decode
"A shape procesor that expected to be executed after schema decoding
process but before validation."
[shape]
(-> shape
(fix-shape-shadow-color)))
(fix-shape-shadow-color)
(fix-root-shape)
(fix-legacy-flex-dir)))
(defn- fix-container
[container]
(-> container
;; Remove possible `nil` keys on objects
(d/update-when :objects dissoc nil)
(d/update-when :objects d/update-vals clean-shape-post-decode)))
(defn clean-file
[file & {:as _opts}]
(update file :data
(fn [data]
(-> data
(d/update-when :pages-index d/update-vals fix-container)
(d/update-when :components d/update-vals fix-container)
(d/without-nils)))))

View File

@@ -431,7 +431,13 @@
(update :components relink-shapes)
(update :media relink-media)
(update :colors relink-colors)
(d/without-nils))))))
(d/without-nils))))
;; NOTE: this is necessary because when we just creating a new
;; file from imported artifact or cloned file there are no
;; migrations registered on the database, so we need to persist
;; all of them, not only the applied
(vary-meta dissoc ::fmg/migrated)))
(defn encode-file
[{:keys [::db/conn] :as cfg} {:keys [id features] :as file}]

View File

@@ -756,14 +756,7 @@
(assoc :name file-name)
(assoc :project-id project-id)
(dissoc :options)
(bfc/process-file)
;; NOTE: this is necessary because when we just
;; creating a new file from imported artifact,
;; there are no migrations registered on the
;; database, so we need to persist all of them, not
;; only the applied
(vary-meta dissoc ::fmg/migrated))]
(bfc/process-file))]
(bfm/register-pending-migrations! cfg file)
(bfc/save-file! cfg file ::db/return-keys false)

View File

@@ -208,7 +208,7 @@
[:project-id {:optional true} ::sm/uuid]])
(defn- migrate-file
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
[{:keys [::db/conn] :as cfg} {:keys [id] :as file} {:keys [read-only?]}]
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)
pmap/*tracked* (pmap/create-tracked)]
(let [;; For avoid unnecesary overhead of creating multiple pointers and
@@ -219,43 +219,45 @@
file (-> file
(update :data feat.fdata/process-pointers deref)
(update :data feat.fdata/process-objects (partial into {}))
(fmg/migrate-file))
(fmg/migrate-file))]
;; When file is migrated, we break the rule of no perform
;; mutations on get operations and update the file with all
;; migrations applied
;;
;; WARN: he following code will not work on read-only mode,
;; it is a known issue; we keep is not implemented until we
;; really need this.
file (if (contains? (:features file) "fdata/objects-map")
(feat.fdata/enable-objects-map file)
file)
file (if (contains? (:features file) "fdata/pointer-map")
(feat.fdata/enable-pointer-map file)
file)]
(if (or read-only? (db/read-only? conn))
file
(let [;; When file is migrated, we break the rule of no perform
;; mutations on get operations and update the file with all
;; migrations applied
file (if (contains? (:features file) "fdata/objects-map")
(feat.fdata/enable-objects-map file)
file)
file (if (contains? (:features file) "fdata/pointer-map")
(feat.fdata/enable-pointer-map file)
file)]
(db/update! conn :file
{:data (blob/encode (:data file))
:version (:version file)
:features (db/create-array conn "text" (:features file))}
{:id id})
(db/update! conn :file
{:data (blob/encode (:data file))
:version (:version file)
:features (db/create-array conn "text" (:features file))}
{:id id}
{::db/return-keys false})
(when (contains? (:features file) "fdata/pointer-map")
(feat.fdata/persist-pointers! cfg id))
(when (contains? (:features file) "fdata/pointer-map")
(feat.fdata/persist-pointers! cfg id))
(feat.fmigr/upsert-migrations! conn file)
(feat.fmigr/resolve-applied-migrations cfg file))))
(feat.fmigr/upsert-migrations! conn file)
(feat.fmigr/resolve-applied-migrations cfg file))))))
(defn get-file
[{:keys [::db/conn ::wrk/executor] :as cfg} id
& {:keys [project-id
migrate?
include-deleted?
lock-for-update?]
lock-for-update?
preload-pointers?]
:or {include-deleted? false
lock-for-update? false
migrate? true}}]
migrate? true
preload-pointers? false}
:as options}]
(assert (db/connection? conn) "expected cfg with valid connection")
@@ -273,10 +275,16 @@
;; because it has heavy and synchronous operations for
;; decoding file body that are not very friendly with virtual
;; threads.
file (px/invoke! executor #(decode-row file))]
file (px/invoke! executor #(decode-row file))
file (if (and migrate? (fmg/need-migration? file))
(migrate-file cfg file options)
file)]
(if preload-pointers?
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
(update file :data feat.fdata/process-pointers deref))
(if (and migrate? (fmg/need-migration? file))
(migrate-file cfg file)
file)))
(defn get-minimal-file
@@ -484,7 +492,7 @@
(let [perms (get-permissions conn profile-id file-id share-id)
file (get-file cfg file-id)
file (get-file cfg file-id :read-only? true)
proj (db/get conn :project {:id (:project-id file)})
@@ -741,7 +749,9 @@
:project-id project-id
:file-id id)
file (get-file cfg id :project-id project-id)]
file (get-file cfg id
:project-id project-id
:read-only? true)]
(-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params))

View File

@@ -55,8 +55,8 @@
:features features
:ignore-sync-until ignore-sync-until
:modified-at modified-at
:deleted-at deleted-at
:create-page create-page
:deleted-at deleted-at}
{:create-page create-page
:page-id page-id})
file (-> (bfc/insert-file! cfg file)
(bfc/decode-row))]

View File

@@ -10,7 +10,6 @@
[app.common.data.macros :as dm]
[app.common.features :as cfeat]
[app.common.files.helpers :as cfh]
[app.common.files.migrations :as fmg]
[app.common.geom.shapes :as gsh]
[app.common.schema :as sm]
[app.common.thumbnails :as thc]
@@ -18,7 +17,6 @@
[app.config :as cf]
[app.db :as db]
[app.db.sql :as-alias sql]
[app.features.fdata :as feat.fdata]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks]
[app.media :as media]
@@ -200,14 +198,13 @@
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-read-permissions! conn profile-id file-id)
(let [team (teams/get-team conn
:profile-id profile-id
:file-id file-id)
(let [team (teams/get-team conn
:profile-id profile-id
:file-id file-id)
file (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
(-> (files/get-file cfg file-id :migrate? false)
(update :data feat.fdata/process-pointers deref)
(fmg/migrate-file)))]
file (files/get-file cfg file-id
:preload-pointers? true
:read-only? true)]
(-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-file-features! (:features file)))

View File

@@ -10,6 +10,7 @@
file is eligible to be garbage collected after some period of
inactivity (the default threshold is 72h)."
(:require
[app.binfile.cleaner :as bfl]
[app.binfile.common :as bfc]
[app.common.files.helpers :as cfh]
[app.common.files.validate :as cfv]
@@ -258,6 +259,7 @@
(if-let [file (get-file cfg file-id)]
(let [file (->> file
(bfc/decode-file cfg)
(bfl/clean-file)
(clean-media! cfg)
(clean-fragments! cfg))
file (assoc file :has-media-trimmed true)]

View File

@@ -2,7 +2,7 @@
{org.clojure/clojure {:mvn/version "1.12.0"}
org.clojure/data.json {:mvn/version "2.5.1"}
org.clojure/tools.cli {:mvn/version "1.1.230"}
org.clojure/clojurescript {:mvn/version "1.11.132"}
org.clojure/clojurescript {:mvn/version "1.12.38"}
org.clojure/test.check {:mvn/version "1.1.1"}
org.clojure/data.fressian {:mvn/version "1.1.0"}
@@ -59,7 +59,7 @@
{:dev
{:extra-deps
{org.clojure/tools.namespace {:mvn/version "RELEASE"}
thheller/shadow-cljs {:mvn/version "2.28.20"}
thheller/shadow-cljs {:mvn/version "3.0.3"}
com.clojure-goes-fast/clj-async-profiler {:mvn/version "RELEASE"}
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
criterium/criterium {:mvn/version "RELEASE"}

View File

@@ -17,7 +17,7 @@
"devDependencies": {
"concurrently": "^9.0.1",
"nodemon": "^3.1.7",
"shadow-cljs": "2.28.20",
"shadow-cljs": "3.0.3",
"source-map-support": "^0.5.21",
"ws": "^8.17.0"
},

View File

@@ -2,16 +2,20 @@
export PENPOT_FLAGS="enable-asserts enable-audit-log $PENPOT_FLAGS"
export OPTIONS="
-A:dev \
-J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-J-Djdk.attach.allowAttachSelf \
-J-Dpolyglot.engine.WarnInterpreterOnly=false \
-J-XX:+EnableDynamicAgentLoading \
-J-XX:-OmitStackTraceInFastThrow \
-J-XX:+UnlockDiagnosticVMOptions \
-J-XX:+DebugNonSafepoints \
-J-Djdk.tracePinnedThreads=full"
export JAVA_OPTS="\
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-Djdk.attach.allowAttachSelf \
-Dlog4j2.configurationFile=log4j2-devenv-repl.xml \
-Djdk.tracePinnedThreads=full \
-XX:+EnableDynamicAgentLoading \
-XX:-OmitStackTraceInFastThrow \
-XX:+UnlockDiagnosticVMOptions \
-XX:+DebugNonSafepoints \
--sun-misc-unsafe-memory-access=allow \
--enable-preview \
--enable-native-access=ALL-UNNAMED";
export OPTIONS="-A:dev"
export OPTIONS_EVAL="nil"
# export OPTIONS_EVAL="(set! *warn-on-reflection* true)"

View File

File diff suppressed because it is too large Load Diff

View File

@@ -732,20 +732,22 @@
(update-group [group objects]
(let [lookup (d/getf objects)
children (->> group :shapes (map lookup))]
children (get group :shapes)]
(cond
;; If the group is empty we don't make any changes. Will be removed by a later process
(empty? children)
group
(= :bool (:type group))
(gsh/update-bool group children objects)
(gsh/update-bool group objects)
(:masked-group group)
(set-mask-selrect group children)
(->> (map lookup children)
(set-mask-selrect group))
:else
(gsh/update-group-selrect group children))))]
(->> (map lookup children)
(gsh/update-group-selrect group)))))]
(if page-id
(d/update-in-when data [:pages-index page-id :objects] reg-objects)

View File

@@ -660,9 +660,13 @@
nil ;; so it does not need resize
(= (:type parent) :bool)
(gsh/update-bool parent children objects)
(gsh/update-bool parent objects)
(= (:type parent) :group)
;; FIXME: this functions should be
;; normalized in the same way as
;; update-bool in order to make all
;; this code consistent
(if (:masked-group parent)
(gsh/update-mask-selrect parent children)
(gsh/update-group-selrect parent children)))]

View File

@@ -126,21 +126,20 @@
o)))
(def schema:matrix
{:type :map
:pred valid-matrix?
:type-properties
{:title "matrix"
:description "Matrix instance"
:error/message "expected a valid matrix instance"
:gen/gen (matrix-generator)
:decode/json decode-matrix
:decode/string decode-matrix
:encode/json matrix->json
:encode/string matrix->str
::oapi/type "string"
::oapi/format "matrix"}})
(sm/register! ::matrix schema:matrix)
(sm/register!
{:type ::matrix
:pred valid-matrix?
:type-properties
{:title "matrix"
:description "Matrix instance"
:error/message "expected a valid matrix instance"
:gen/gen (matrix-generator)
:decode/json decode-matrix
:decode/string decode-matrix
:encode/json matrix->json
:encode/string matrix->str
::oapi/type "string"
::oapi/format "matrix"}}))
;; FIXME: deprecated
(s/def ::a ::us/safe-float)

View File

@@ -85,24 +85,22 @@
(into {} p)
p))
;; FIXME: make like matrix
(def schema:point
{:type ::point
:pred valid-point?
:type-properties
{:title "point"
:description "Point"
:error/message "expected a valid point"
:gen/gen (->> (sg/tuple (sg/small-int) (sg/small-int))
(sg/fmap #(apply pos->Point %)))
::oapi/type "string"
::oapi/format "point"
:decode/json decode-point
:decode/string decode-point
:encode/json point->json
:encode/string point->str}})
(sm/register! schema:point)
(sm/register!
{:type ::point
:pred valid-point?
:type-properties
{:title "point"
:description "Point"
:error/message "expected a valid point"
:gen/gen (->> (sg/tuple (sg/small-int) (sg/small-int))
(sg/fmap #(apply pos->Point %)))
::oapi/type "string"
::oapi/format "point"
:decode/json decode-point
:decode/string decode-point
:encode/json point->json
:encode/string point->str}}))
(defn point-like?
[{:keys [x y] :as v}]

View File

@@ -455,12 +455,12 @@
(defn update-bool
"Calculates the selrect+points for the boolean shape"
[shape _children objects]
[shape objects]
(let [content (path/calc-bool-content shape objects)
shape (assoc shape :content content)]
(path/update-geometry shape)))
;; FIXME: revisit
(defn update-shapes-geometry
[objects ids]
(->> ids
@@ -474,7 +474,7 @@
(update-mask-selrect shape children)
(cfh/bool-shape? shape)
(update-bool shape children objects)
(update-bool shape objects)
(cfh/group-shape? shape)
(update-group-selrect shape children)

View File

@@ -1757,18 +1757,18 @@
(let [attr-group (get ctk/sync-attrs attr)
[roperations' uoperations']
(if (or
;; If the attribute is not valid for the destiny, don't copy it
;; If the attribute is not valid for the destiny, don't copy it
(not (cts/is-allowed-attr? attr (:type dest-shape)))
;; If the values are already equal, don't copy it
;; If the values are already equal, don't copy it
(= (get origin-shape attr) (get dest-shape attr))
;; If the referenced shape on the original component doesn't have the same value, don't copy it
;; Exceptions: :points :selrect and :content can be different
;; If the referenced shape on the original component doesn't have the same value, don't copy it
;; Exceptions: :points :selrect and :content can be different
(and
(not (contains? #{:points :selrect :content} attr))
(not= (get origin-ref-shape attr) (get dest-shape attr)))
;; The :content attr cant't be copied to elements of different type
;; The :content attr cant't be copied to elements of different type
(and (= attr :content) (not= (:type origin-shape) (:type dest-shape)))
;; If the attr is not touched in the origin shape, don't copy it
;; If the attr is not touched in the origin shape, don't copy it
(not (touched-origin attr-group)))
[roperations uoperations]
(add-update-attr-operations attr dest-shape origin-shape roperations uoperations touched))]

View File

@@ -5,7 +5,6 @@
[app.common.files.variant :as cfv]
[app.common.logic.libraries :as cll]
[app.common.logic.variant-properties :as clvp]
[app.common.types.components-list :as ctcl]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.variant :as ctv]))
@@ -48,12 +47,8 @@
(pcb/change-parent (:parent-id shape) [new-shape] 0))))
(defn generate-keep-touched
[changes new-shape original-shape original-shapes page]
(let [data (pcb/get-library-data changes)
objects (pcb/get-objects changes)
orig-comp (ctcl/get-component data (:component-id original-shape) true)
[changes new-shape original-shape original-shapes page libraries]
(let [objects (pcb/get-objects changes)
new-path-map (into {}
(map (fn [shape] {(generate-path "" objects (:id new-shape) shape) shape}))
(cfh/get-children-with-self objects (:id new-shape)))
@@ -65,7 +60,7 @@
(fn [changes touched-shape]
(let [path (generate-path "" orig-objects (:id original-shape) touched-shape)
related-shape (get new-path-map path)
orig-ref-shape (ctf/get-ref-shape data orig-comp touched-shape)]
orig-ref-shape (ctf/find-ref-shape nil container libraries touched-shape)]
(if related-shape
(cll/update-attrs-on-switch
changes related-shape touched-shape new-shape original-shape orig-ref-shape container)

View File

@@ -28,10 +28,6 @@
[malli.transform :as mt]
[malli.util :as mu]))
(defprotocol ILazySchema
(-validate [_ o])
(-explain [_ o]))
(def default-options
{:registry sr/default-registry})
@@ -51,10 +47,6 @@
[s]
(m/type-properties s))
(defn- lazy-schema?
[s]
(satisfies? ILazySchema s))
(defn schema
[s]
(if (schema? s)
@@ -111,12 +103,16 @@
(malli.error/error-value exp {:malli.error/mask-valid-values '...}))
(defn optional-keys
[schema]
(mu/optional-keys schema default-options))
([schema]
(mu/optional-keys schema nil default-options))
([schema keys]
(mu/optional-keys schema keys default-options)))
(defn required-keys
[schema]
(mu/required-keys schema default-options))
([schema]
(mu/required-keys schema nil default-options))
([schema keys]
(mu/required-keys schema keys default-options)))
(defn transformer
[& transformers]
@@ -229,6 +225,11 @@
(let [vfn (delay (decoder (if (delay? s) (deref s) s) transformer))]
(fn [v] (@vfn v))))
(defn decode-fn
[s transformer]
(let [vfn (delay (decoder (if (delay? s) (deref s) s) transformer))]
(fn [v] (@vfn v))))
(defn humanize-explain
"Returns a string representation of the explain data structure"
[{:keys [errors value]} & {:keys [length level]}]
@@ -274,38 +275,36 @@
([s] (lookup sr/default-registry s))
([registry s] (schema (mr/schema registry s))))
(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]
(when-not ^boolean (-validate s value)
(let [explain (-explain s value)]
(throw (ex-info hint {:type type
:code code
:hint hint
::explain explain}))))
value)
(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)))
(let [s (schema s)
validator* (delay (m/validator s))
explainer* (delay (m/explainer s))
hint (or ^boolean hint "check error")
type (or ^boolean type :assertion)
code (or ^boolean code :data-validation)]
(fn [value]
(let [validate-fn @validator*]
(when-not ^boolean (validate-fn value)
(let [explain-fn @explainer*
explain (explain-fn value)]
(throw (ex-info hint {:type type
:code code
:hint hint
::explain explain}))))
value))))
(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.
Use only on non-performance sensitive code, because it creates the
check-fn instance all the time it is invoked."
[s value & {:as opts}]
(let [check-fn (check-fn s opts)]
(check-fn value)))
(defn type-schema
[& {:as params}]
@@ -319,11 +318,14 @@
([params]
(cond
(map? params)
(let [type (get params :type)]
(let [mdata (meta params)
type (or (get mdata ::id)
(get mdata ::type)
(get params :type))]
(assert (qualified-keyword? type) "expected qualified keyword for `type`")
(let [s (m/-simple-schema params)]
(swap! sr/registry assoc type s)
nil))
s))
(vector? params)
(let [mdata (meta params)
@@ -331,83 +333,19 @@
(get mdata ::type))]
(assert (qualified-keyword? type) "expected qualified keyword to be on metadata")
(swap! sr/registry assoc type params)
nil)
params)
(m/into-schema? params)
(let [type (m/-type params)]
(swap! sr/registry assoc type params))
(swap! sr/registry assoc type params)
params)
:else
(throw (ex-info "Invalid Arguments" {}))))
([type params]
(let [s (if (map? params)
(cond
(= :set (:type params))
(m/-collection-schema params)
(= :vector (:type params))
(m/-collection-schema params)
:else
(m/-simple-schema params))
params)]
(swap! sr/registry assoc type s)
nil)))
(defn- lazy-schema
"Create ans instance of ILazySchema"
[s]
(let [schema (schema s)
validator (delay (m/validator schema))
explainer (delay (m/explainer schema))]
(reify
m/AST
(-to-ast [_ options] (m/-to-ast schema options))
m/EntrySchema
(-entries [_] (m/-entries schema))
(-entry-parser [_] (m/-entry-parser schema))
m/Cached
(-cache [_] (m/-cache schema))
m/LensSchema
(-keep [_] (m/-keep schema))
(-get [_ key default] (m/-get schema key default))
(-set [_ key value] (m/-set schema key value))
m/Schema
(-validator [_]
(m/-validator schema))
(-explainer [_ path]
(m/-explainer schema path))
(-parser [_]
(m/-parser schema))
(-unparser [_]
(m/-unparser schema))
(-transformer [_ transformer method options]
(m/-transformer schema transformer method options))
(-walk [_ walker path options]
(m/-walk schema walker path options))
(-properties [_]
(m/-properties schema))
(-options [_]
(m/-options schema))
(-children [_]
(m/-children schema))
(-parent [_]
(m/-parent schema))
(-form [_]
(m/-form schema))
ILazySchema
(-validate [_ o]
(@validator o))
(-explain [_ o]
(@explainer o)))))
(swap! sr/registry assoc type params)
params))
;; --- BUILTIN SCHEMAS

View File

@@ -23,28 +23,32 @@
(defn sample-file
[label & {:keys [page-label name view-only?] :as params}]
(binding [ffeat/*current* #{"components/v2"}]
(let [params (cond-> params
label
(assoc :id (thi/new-id! label))
(let [params
(cond-> params
label
(assoc :id (thi/new-id! label))
page-label
(assoc :page-id (thi/new-id! page-label))
(nil? name)
(assoc :name "Test file")
(nil? name)
(assoc :name "Test file"))
:always
(assoc :features ffeat/default-features))
file (-> (ctf/make-file (dissoc params :page-label))
(assoc :features #{"components/v2"})
(assoc :permissions {:can-edit (not (true? view-only?))}))
opts
(cond-> {}
page-label
(assoc :page-id (thi/new-id! page-label)))
page (-> file
:data
(ctpl/pages-seq)
(first))]
file (-> (ctf/make-file params opts)
(assoc :permissions {:can-edit (not (true? view-only?))}))
(with-meta file
{:current-page-id (:id page)}))))
page (-> file
:data
(ctpl/pages-seq)
(first))]
(with-meta file
{:current-page-id (:id page)})))
(defn validate-file!
([file] (validate-file! file {}))

View File

@@ -41,17 +41,18 @@
[o]
(and (string? o) (some? (re-matches rgb-color-re o))))
(def ^:private type:rgb-color
{:type :string
:pred rgb-color-string?
:type-properties
{:title "rgb-color"
:description "RGB Color String"
:error/message "expected a valid RGB color"
:error/code "errors.invalid-rgb-color"
:gen/gen (generate-rgb-color)
::oapi/type "integer"
::oapi/format "int64"}})
(def schema:rgb-color
(sm/register!
{:type ::rgb-color
:pred rgb-color-string?
:type-properties
{:title "rgb-color"
:description "RGB Color String"
:error/message "expected a valid RGB color"
:error/code "errors.invalid-rgb-color"
:gen/gen (generate-rgb-color)
::oapi/type "integer"
::oapi/format "int64"}}))
(def schema:image-color
[:map {:title "ImageColor"}
@@ -76,7 +77,7 @@
[:stops
[:vector {:min 1 :gen/max 2}
[:map {:title "GradientStop"}
[:color ::rgb-color]
[:color schema:rgb-color]
[:opacity {:optional true} [:maybe ::sm/safe-number]]
[:offset ::sm/safe-number]]]]])
@@ -86,7 +87,7 @@
[:name {:optional true} :string]
[:path {:optional true} [:maybe :string]]
[:value {:optional true} [:maybe :string]]
[:color {:optional true} [:maybe ::rgb-color]]
[:color {:optional true} [:maybe schema:rgb-color]]
[:opacity {:optional true} [:maybe ::sm/safe-number]]
[:modified-at {:optional true} ::sm/inst]
[:ref-id {:optional true} ::sm/uuid]
@@ -103,12 +104,17 @@
[:and
[:map {:title "RecentColor"}
[:opacity {:optional true} [:maybe ::sm/safe-number]]
[:color {:optional true} [:maybe ::rgb-color]]
[:color {:optional true} [:maybe schema:rgb-color]]
[:gradient {:optional true} [:maybe schema:gradient]]
[:image {:optional true} [:maybe schema:image-color]]]
[::sm/contains-any {:strict true} [:color :gradient :image]]])
(sm/register! ::rgb-color type:rgb-color)
;; Same as color but with :id prop required
(def schema:library-color
[:and
(sm/required-keys schema:color-attrs [:id])
[::sm/contains-any {:strict true} [:color :gradient :image]]])
(sm/register! ::color schema:color)
(sm/register! ::gradient schema:gradient)
(sm/register! ::image-color schema:image-color)
@@ -119,10 +125,13 @@
(sm/lazy-validator schema:color))
(def check-color
(sm/check-fn schema:color :hint "expected valid color struct"))
(sm/check-fn schema:color :hint "expected valid color"))
(def check-library-color
(sm/check-fn schema:library-color :hint "expected valid library color"))
(def check-recent-color
(sm/check-fn schema:recent-color))
(sm/check-fn schema:recent-color :hint "expected valid recent color"))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS

View File

@@ -18,19 +18,19 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def schema:component
[:merge
[:map
[:id ::sm/uuid]
[:name :string]
[:path {:optional true} [:maybe :string]]
[:modified-at {:optional true} ::sm/inst]
[:objects {:gen/max 10 :optional true} ::ctp/objects]
[:main-instance-id ::sm/uuid]
[:main-instance-page ::sm/uuid]
[:plugin-data {:optional true} ::ctpg/plugin-data]]
::ctv/variant-component])
(sm/register! ::component schema:component)
(sm/register!
^{::sm/type ::component}
[:merge
[:map
[:id ::sm/uuid]
[:name :string]
[:path {:optional true} [:maybe :string]]
[:modified-at {:optional true} ::sm/inst]
[:objects {:gen/max 10 :optional true} ctp/schema:objects]
[:main-instance-id ::sm/uuid]
[:main-instance-page ::sm/uuid]
[:plugin-data {:optional true} ctpg/schema:plugin-data]]
ctv/schema:variant-component]))
(def check-component
(sm/check-fn schema:component))

View File

@@ -41,7 +41,7 @@
[:map-of {:gen/max 10} ::sm/uuid :map]]
[:plugin-data {:optional true} ::ctpg/plugin-data]])
(def check-container!
(def check-container
(sm/check-fn ::container))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -62,9 +62,9 @@
(defn get-container
[file type id]
(dm/assert! (map? file))
(dm/assert! (contains? valid-container-types type))
(dm/assert! (uuid? id))
(assert (map? file))
(assert (contains? valid-container-types type))
(assert (uuid? id))
(-> (if (= type :page)
(ctpl/get-page file id)
@@ -74,13 +74,9 @@
(defn get-shape
[container shape-id]
(dm/assert!
"expected valid container"
(check-container! container))
(dm/assert!
"expected valid uuid for `shape-id`"
(uuid? shape-id))
(assert (check-container container))
(assert (uuid? shape-id)
"expected valid uuid for `shape-id`")
(-> container
(get :objects)

View File

@@ -83,6 +83,7 @@
because sometimes we want to validate file without the data."
[:map {:title "file"}
[:id ::sm/uuid]
[:name :string]
[:revn {:optional true} :int]
[:vern {:optional true} :int]
[:created-at {:optional true} ::sm/inst]
@@ -101,13 +102,15 @@
(sm/register! ::media schema:media)
(sm/register! ::colors schema:colors)
(sm/register! ::typographies schema:typographies)
(sm/register! ::media-object schema:media)
(def check-file-data!
(sm/check-fn ::data))
(def check-file
(sm/check-fn schema:file :hint "check error on validating file"))
(def check-media-object!
(def check-file-data
(sm/check-fn schema:data))
(def check-media-object
(sm/check-fn schema:media))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -134,33 +137,36 @@
(update :options assoc :components-v2 true)))))
(defn make-file
[{:keys [id project-id name revn is-shared features
ignore-sync-until modified-at deleted-at
create-page page-id]
:or {is-shared false revn 0 create-page true}}]
[{:keys [id project-id name revn is-shared features migrations
ignore-sync-until modified-at deleted-at]
:or {is-shared false revn 0}}
& {:keys [create-page page-id]
:or {create-page true}}]
(let [id (or id (uuid/next))
data (if create-page
(if page-id
(make-file-data id page-id)
(make-file-data id))
(make-file-data id nil))
file {:id id
:project-id project-id
:name name
:revn revn
:vern 0
:is-shared is-shared
:version version
:data data
:features features
:ignore-sync-until ignore-sync-until
:modified-at modified-at
:deleted-at deleted-at}]
file (d/without-nils
{:id id
:project-id project-id
:name name
:revn revn
:vern 0
:is-shared is-shared
:version version
:data data
:features features
:migrations migrations
:ignore-sync-until ignore-sync-until
:modified-at modified-at
:deleted-at deleted-at})]
(d/without-nils file)))
(check-file file)))
;; Helpers

View File

@@ -70,7 +70,7 @@
(def valid-guide?
(sm/lazy-validator schema:guide))
(def check-page!
(def check-page
(sm/check-fn schema:page))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -82,8 +82,7 @@
(def root uuid/zero)
(def empty-page-data
{:options {}
:objects {root
{:objects {root
(cts/setup-shape {:id root
:type :frame
:parent-id root
@@ -91,10 +90,12 @@
:name "Root Frame"})}})
(defn make-empty-page
[{:keys [id name]}]
[{:keys [id name background]}]
(-> empty-page-data
(assoc :id (or id (uuid/next)))
(assoc :name (or name "Page 1"))))
(assoc :name (d/nilv name "Page 1"))
(cond-> background
(assoc :background background))))
(defn get-frame-flow
[flows frame-id]

View File

@@ -22,14 +22,13 @@
:keyword])
(def schema:plugin-data
[:map-of {:gen/max 5}
schema:keyword
(sm/register!
^{::sm/type ::plugin-data}
[:map-of {:gen/max 5}
schema:string
schema:string]])
(sm/register! ::plugin-data schema:plugin-data)
schema:keyword
[:map-of {:gen/max 5}
schema:string
schema:string]]))
(def ^:private schema:registry-entry
[:map

View File

@@ -120,35 +120,35 @@
[:vector {:gen/max 4 :gen/min 4} ::gpt/point])
(def schema:fill
[:map {:title "Fill"}
[:fill-color {:optional true} ::ctc/rgb-color]
[:fill-opacity {:optional true} ::sm/safe-number]
[:fill-color-gradient {:optional true} [:maybe ::ctc/gradient]]
[:fill-color-ref-file {:optional true} [:maybe ::sm/uuid]]
[:fill-color-ref-id {:optional true} [:maybe ::sm/uuid]]
[:fill-image {:optional true} ::ctc/image-color]])
(sm/register!
^{::sm/type ::fill}
[:map {:title "Fill"}
[:fill-color {:optional true} ::ctc/rgb-color]
[:fill-opacity {:optional true} ::sm/safe-number]
[:fill-color-gradient {:optional true} [:maybe ::ctc/gradient]]
[:fill-color-ref-file {:optional true} [:maybe ::sm/uuid]]
[:fill-color-ref-id {:optional true} [:maybe ::sm/uuid]]
[:fill-image {:optional true} ::ctc/image-color]]))
(sm/register! ::fill schema:fill)
(def ^:private schema:stroke
[:map {:title "Stroke"}
[:stroke-color {:optional true} :string]
[:stroke-color-ref-file {:optional true} ::sm/uuid]
[:stroke-color-ref-id {:optional true} ::sm/uuid]
[:stroke-opacity {:optional true} ::sm/safe-number]
[:stroke-style {:optional true}
[::sm/one-of #{:solid :dotted :dashed :mixed :none :svg}]]
[:stroke-width {:optional true} ::sm/safe-number]
[:stroke-alignment {:optional true}
[::sm/one-of #{:center :inner :outer}]]
[:stroke-cap-start {:optional true}
[::sm/one-of stroke-caps]]
[:stroke-cap-end {:optional true}
[::sm/one-of stroke-caps]]
[:stroke-color-gradient {:optional true} ::ctc/gradient]
[:stroke-image {:optional true} ::ctc/image-color]])
(sm/register! ::stroke schema:stroke)
(def schema:stroke
(sm/register!
^{::sm/type ::stroke}
[:map {:title "Stroke"}
[:stroke-color {:optional true} :string]
[:stroke-color-ref-file {:optional true} ::sm/uuid]
[:stroke-color-ref-id {:optional true} ::sm/uuid]
[:stroke-opacity {:optional true} ::sm/safe-number]
[:stroke-style {:optional true}
[::sm/one-of #{:solid :dotted :dashed :mixed :none :svg}]]
[:stroke-width {:optional true} ::sm/safe-number]
[:stroke-alignment {:optional true}
[::sm/one-of #{:center :inner :outer}]]
[:stroke-cap-start {:optional true}
[::sm/one-of stroke-caps]]
[:stroke-cap-end {:optional true}
[::sm/one-of stroke-caps]]
[:stroke-color-gradient {:optional true} ::ctc/gradient]
[:stroke-image {:optional true} ::ctc/image-color]]))
(def check-stroke
(sm/check-fn schema:stroke))
@@ -172,8 +172,7 @@
[:width ::sm/safe-number]
[:height ::sm/safe-number]])
;; FIXME: rename to shape-generic-attrs
(def schema:shape-attrs
(def schema:shape-generic-attrs
[:map {:title "ShapeAttrs"}
[:page-id {:optional true} ::sm/uuid]
[:component-id {:optional true} ::sm/uuid]
@@ -277,7 +276,7 @@
[]
(->> (sg/generator schema:shape-base-attrs)
(sg/mcat (fn [{:keys [type] :as shape}]
(sg/let [attrs1 (sg/generator schema:shape-attrs)
(sg/let [attrs1 (sg/generator schema:shape-generic-attrs)
attrs2 (sg/generator schema:shape-geom-attrs)
attrs3 (case type
:text (sg/generator schema:text-attrs)
@@ -295,94 +294,100 @@
(merge attrs1 shape attrs2 attrs3)))))
(sg/fmap create-shape)))
(def schema:shape-attrs
[:multi {:dispatch :type
:decode/json (fn [shape]
(update shape :type keyword))
:title "Shape"}
[:group
[:merge {:title "GroupShape"}
ctsl/schema:layout-attrs
schema:group-attrs
schema:shape-generic-attrs
schema:shape-geom-attrs
schema:shape-base-attrs]]
[:frame
[:merge {:title "FrameShape"}
ctsl/schema:layout-attrs
::ctsl/layout-attrs
schema:frame-attrs
schema:shape-generic-attrs
schema:shape-geom-attrs
schema:shape-base-attrs
::ctv/variant-shape
::ctv/variant-container]]
[:bool
[:merge {:title "BoolShape"}
ctsl/schema:layout-attrs
schema:bool-attrs
schema:shape-generic-attrs
schema:shape-base-attrs]]
[:rect
[:merge {:title "RectShape"}
ctsl/schema:layout-attrs
schema:rect-attrs
schema:shape-generic-attrs
schema:shape-geom-attrs
schema:shape-base-attrs]]
[:circle
[:merge {:title "CircleShape"}
ctsl/schema:layout-attrs
schema:circle-attrs
schema:shape-generic-attrs
schema:shape-geom-attrs
schema:shape-base-attrs]]
[:image
[:merge {:title "ImageShape"}
ctsl/schema:layout-attrs
schema:image-attrs
schema:shape-generic-attrs
schema:shape-geom-attrs
schema:shape-base-attrs]]
[:svg-raw
[:merge {:title "SvgRawShape"}
ctsl/schema:layout-attrs
schema:svg-raw-attrs
schema:shape-generic-attrs
schema:shape-geom-attrs
schema:shape-base-attrs]]
[:path
[:merge {:title "PathShape"}
ctsl/schema:layout-attrs
schema:path-attrs
schema:shape-generic-attrs
schema:shape-base-attrs]]
[:text
[:merge {:title "TextShape"}
ctsl/schema:layout-attrs
schema:text-attrs
schema:shape-generic-attrs
schema:shape-geom-attrs
schema:shape-base-attrs]]])
(def schema:shape
[:and {:title "Shape"
:gen/gen (shape-generator)
:decode/json {:leave decode-shape}}
[:fn shape?]
[:multi {:dispatch :type
:decode/json (fn [shape]
(update shape :type keyword))
:title "Shape"}
[:group
[:merge {:title "GroupShape"}
::ctsl/layout-child-attrs
schema:group-attrs
schema:shape-attrs
schema:shape-geom-attrs
schema:shape-base-attrs]]
(sm/register!
^{::sm/type ::shape}
[:and {:title "Shape"
:gen/gen (shape-generator)
:decode/json {:leave decode-shape}}
[:fn shape?]
schema:shape-attrs]))
[:frame
[:merge {:title "FrameShape"}
::ctsl/layout-child-attrs
::ctsl/layout-attrs
schema:frame-attrs
schema:shape-attrs
schema:shape-geom-attrs
schema:shape-base-attrs
::ctv/variant-shape
::ctv/variant-container]]
(def check-shape-generic-attrs
(sm/check-fn schema:shape-generic-attrs))
[:bool
[:merge {:title "BoolShape"}
::ctsl/layout-child-attrs
schema:bool-attrs
schema:shape-attrs
schema:shape-base-attrs]]
[:rect
[:merge {:title "RectShape"}
::ctsl/layout-child-attrs
schema:rect-attrs
schema:shape-attrs
schema:shape-geom-attrs
schema:shape-base-attrs]]
[:circle
[:merge {:title "CircleShape"}
::ctsl/layout-child-attrs
schema:circle-attrs
schema:shape-attrs
schema:shape-geom-attrs
schema:shape-base-attrs]]
[:image
[:merge {:title "ImageShape"}
::ctsl/layout-child-attrs
schema:image-attrs
schema:shape-attrs
schema:shape-geom-attrs
schema:shape-base-attrs]]
[:svg-raw
[:merge {:title "SvgRawShape"}
::ctsl/layout-child-attrs
schema:svg-raw-attrs
schema:shape-attrs
schema:shape-geom-attrs
schema:shape-base-attrs]]
[:path
[:merge {:title "PathShape"}
::ctsl/layout-child-attrs
schema:path-attrs
schema:shape-attrs
schema:shape-base-attrs]]
[:text
[:merge {:title "TextShape"}
::ctsl/layout-child-attrs
schema:text-attrs
schema:shape-attrs
schema:shape-geom-attrs
schema:shape-base-attrs]]]])
(sm/register! ::shape schema:shape)
(def check-shape-attrs!
(def check-shape-attrs
(sm/check-fn schema:shape-attrs))
(def check-shape!
(def check-shape
(sm/check-fn schema:shape
:hint "expected valid shape"))

View File

@@ -168,25 +168,24 @@
(def item-align-self-types
#{:start :end :center :stretch})
(sm/register!
^{::sm/type ::layout-child-attrs}
[:map {:title "LayoutChildAttrs"}
[:layout-item-margin-type {:optional true} [::sm/one-of item-margin-types]]
[:layout-item-margin {:optional true}
[:map
[:m1 {:optional true} ::sm/safe-number]
[:m2 {:optional true} ::sm/safe-number]
[:m3 {:optional true} ::sm/safe-number]
[:m4 {:optional true} ::sm/safe-number]]]
[:layout-item-max-h {:optional true} ::sm/safe-number]
[:layout-item-min-h {:optional true} ::sm/safe-number]
[:layout-item-max-w {:optional true} ::sm/safe-number]
[:layout-item-min-w {:optional true} ::sm/safe-number]
[:layout-item-h-sizing {:optional true} [::sm/one-of item-h-sizing-types]]
[:layout-item-v-sizing {:optional true} [::sm/one-of item-v-sizing-types]]
[:layout-item-align-self {:optional true} [::sm/one-of item-align-self-types]]
[:layout-item-absolute {:optional true} :boolean]
[:layout-item-z-index {:optional true} ::sm/safe-number]])
(def schema:layout-attrs
[:map {:title "LayoutChildAttrs"}
[:layout-item-margin-type {:optional true} [::sm/one-of item-margin-types]]
[:layout-item-margin {:optional true}
[:map
[:m1 {:optional true} ::sm/safe-number]
[:m2 {:optional true} ::sm/safe-number]
[:m3 {:optional true} ::sm/safe-number]
[:m4 {:optional true} ::sm/safe-number]]]
[:layout-item-max-h {:optional true} ::sm/safe-number]
[:layout-item-min-h {:optional true} ::sm/safe-number]
[:layout-item-max-w {:optional true} ::sm/safe-number]
[:layout-item-min-w {:optional true} ::sm/safe-number]
[:layout-item-h-sizing {:optional true} [::sm/one-of item-h-sizing-types]]
[:layout-item-v-sizing {:optional true} [::sm/one-of item-v-sizing-types]]
[:layout-item-align-self {:optional true} [::sm/one-of item-align-self-types]]
[:layout-item-absolute {:optional true} :boolean]
[:layout-item-z-index {:optional true} ::sm/safe-number]])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMAS

View File

@@ -16,6 +16,8 @@
[app.common.types.shape.layout :as ctl]
[app.common.uuid :as uuid]))
;; FIXME: the order of arguments seems arbitrary, container should be a first artgument
(defn add-shape
"Insert a shape in the tree, at the given index below the given parent or frame.
Update the parent as needed."

View File

@@ -17,25 +17,25 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def schema:typography
[:map {:title "Typography"}
[:id ::sm/uuid]
[:name :string]
[:font-id :string]
[:font-family :string]
[:font-variant-id :string]
[:font-size :string]
[:font-weight :string]
[:font-style :string]
[:line-height :string]
[:letter-spacing :string]
[:text-transform :string]
[:modified-at {:optional true} ::sm/inst]
[:path {:optional true} [:maybe :string]]
[:plugin-data {:optional true} ::ctpg/plugin-data]])
(sm/register!
^{::sm/type ::typography}
[:map {:title "Typography"}
[:id ::sm/uuid]
[:name :string]
[:font-id :string]
[:font-family :string]
[:font-variant-id :string]
[:font-size :string]
[:font-weight :string]
[:font-style :string]
[:line-height :string]
[:letter-spacing :string]
[:text-transform :string]
[:modified-at {:optional true} ::sm/inst]
[:path {:optional true} [:maybe :string]]
[:plugin-data {:optional true} ::ctpg/plugin-data]]))
(sm/register! ::typography schema:typography)
(def check-typography!
(def check-typography
(sm/check-fn ::typography))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -23,9 +23,11 @@
(def schema:variant-component
;; A component that is part of a variant set.
[:map
[:variant-id {:optional true} ::sm/uuid]
[:variant-properties {:optional true} [:vector schema:variant-property]]])
(sm/register!
^{::sm/type ::variant-component}
[:map
[:variant-id {:optional true} ::sm/uuid]
[:variant-properties {:optional true} [:vector schema:variant-property]]]))
(def schema:variant-shape
;; The root shape of the main instance of a variant component.
@@ -40,7 +42,6 @@
[:is-variant-container {:optional true} :boolean]])
(sm/register! ::variant-property schema:variant-property)
(sm/register! ::variant-component schema:variant-component)
(sm/register! ::variant-shape schema:variant-shape)
(sm/register! ::variant-container schema:variant-container)

View File

@@ -42,7 +42,7 @@
:dev
{:extra-paths ["dev"]
:extra-deps
{thheller/shadow-cljs {:mvn/version "2.28.18"}
{thheller/shadow-cljs {:mvn/version "3.0.3"}
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
org.clojure/tools.namespace {:mvn/version "RELEASE"}
criterium/criterium {:mvn/version "RELEASE"}

View File

@@ -25,6 +25,7 @@
"build:app:libs": "node ./scripts/build-libs.js",
"build:app:main": "clojure -M:dev:shadow-cljs release main worker",
"build:app": "yarn run clear:shadow-cache && yarn run build:app:main && yarn run build:app:libs",
"build:library": "yarn run clear:shadow-cache && clojure -M:dev:shadow-cljs release library",
"e2e:server": "node ./scripts/e2e-server.js",
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
"fmt:clj:check": "cljfmt check --parallel=false src/ test/",
@@ -44,6 +45,7 @@
"watch:app:main": "clojure -M:dev:shadow-cljs watch main worker storybook",
"clear:shadow-cache": "rm -rf .shadow-cljs",
"watch:app": "yarn run clear:shadow-cache && concurrently \"yarn run watch:app:main\" \"yarn run watch:app:libs\"",
"watch:library": "yarn run clear:shadow-cache && clojure -M:dev:shadow-cljs watch library",
"watch": "yarn run watch:app:assets",
"watch:storybook": "concurrently \"storybook dev -p 6006 --no-open\" \"yarn run watch:storybook:assets\"",
"watch:storybook:assets": "node ./scripts/watch-storybook.js"
@@ -89,7 +91,7 @@
"rimraf": "^6.0.1",
"sass": "^1.83.4",
"sass-embedded": "^1.83.4",
"shadow-cljs": "2.28.20",
"shadow-cljs": "3.0.3",
"storybook": "^8.5.2",
"svg-sprite": "^2.0.4",
"typescript": "^5.7.3",

View File

@@ -409,7 +409,7 @@ test.describe("Tokens: Tokens Tab", () => {
// Clearing the input field should pick hex
await valueField.fill("");
await expect(
tokensUpdateCreateModal.getByText("Resolved value: -"),
tokensUpdateCreateModal.getByText("Token value cannot be empty"),
).toBeVisible();
await valueSaturationSelector.click({ position: { x: 50, y: 50 } });
await expect(valueField).toHaveValue(/^#[A-Fa-f\d]+$/);

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -79,6 +79,19 @@ function getHeapU32() {
return Module.HEAPU32;
}
export function clearShapeFills() {
Module._clear_shape_fills();
}
export function addShapeSolidFill(argb) {
const ptr = allocBytes(176);
const heap = getHeapU32();
const dv = new DataView(heap.buffer);
dv.setUint8(ptr, 0x00, true);
dv.setUint32(ptr + 4, argb, true);
Module._add_shape_fill();
}
export function setShapeChildren(shapeIds) {
const offset = allocBytes(shapeIds.length * 16);
const heap = getHeapU32();

View File

@@ -25,23 +25,25 @@
<script type="module">
import initWasmModule from '/js/render_wasm.js';
import {
init, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
getRandomFloat, useShape, setShapeChildren, setupInteraction
} from './js/lib.js';
const canvas = document.getElementById("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const shapes = 100;
initWasmModule().then(Module => {
init(Module);
assignCanvas(canvas);
Module._set_canvas_background(hexToU32ARGB("#FABADA", 1));
Module._set_view(1, 0, 0);
Module._init_shapes_pool(shapes + 1);
setupInteraction(canvas);
const children = [];
for (let i = 0; i < 1000; i++) {
for (let i = 0; i < shapes; i++) {
const uuid = crypto.randomUUID();
children.push(uuid);
@@ -56,12 +58,17 @@
const color = getRandomColor();
const argb = hexToU32ARGB(color, getRandomFloat(0.1, 1.0));
Module._add_shape_solid_fill(argb);
addShapeSolidFill(argb)
}
useShape("00000000-0000-0000-0000-000000000000");
setShapeChildren(children);
performance.mark('render:begin');
Module._render(Date.now());
performance.mark('render:end');
const { duration } = performance.measure('render', 'render:begin', 'render:end');
// alert(`render time: ${duration.toFixed(2)}ms`);
});
</script>

View File

@@ -149,13 +149,16 @@
{:test {:init-fn frontend-tests.runner/init
:prepend-js ";if (typeof globalThis.navigator?.userAgent === 'undefined') { globalThis.navigator = {userAgent: ''}; };"}}}
:lib-penpot
:library
{:target :esm
:output-dir "resources/public/libs"
:runtime :custom
:output-dir "target/library"
:devtools {:autoload false}
:modules
{:penpot {:exports {:renderPage app.libs.render/render-page-export
:createFile app.libs.file-builder/create-file-export}}}
{:penpot
{:exports {BuilderError lib.file-builder/BuilderError
createFile lib.file-builder/create-file}}}
:compiler-options
{:output-feature-set :es2020
@@ -165,6 +168,8 @@
:release
{:compiler-options
{:fn-invoke-direct true
:optimizations #shadow/env ["PENPOT_BUILD_OPTIMIZATIONS" :as :keyword :default :advanced]
:pretty-print false
:source-map true
:elide-asserts true
:anon-fn-naming-policy :off

View File

@@ -1,281 +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.libs.file-builder
(:require
[app.common.data :as d]
[app.common.features :as cfeat]
[app.common.files.builder :as fb]
[app.common.media :as cm]
[app.common.types.components-list :as ctkl]
[app.common.uuid :as uuid]
[app.util.json :as json]
[app.util.webapi :as wapi]
[app.util.zip :as uz]
[app.worker.export :as e]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[promesa.core :as p]))
(defn parse-data [data]
(as-> data $
(js->clj $ :keywordize-keys true)
;; Transforms camelCase to kebab-case
(d/deep-mapm
(fn [[key value]]
(let [value (if (= (type value) js/Symbol)
(keyword (js/Symbol.keyFor value))
value)
key (-> key d/name str/kebab keyword)]
[key value])) $)))
(defn data-uri->blob
[data-uri]
(let [[mtype b64-data] (str/split data-uri ";base64,")
mtype (subs mtype (inc (str/index-of mtype ":")))
decoded (.atob js/window b64-data)
size (.-length ^js decoded)
content (js/Uint8Array. size)]
(doseq [i (range 0 size)]
(aset content i (.charCodeAt decoded i)))
(wapi/create-blob content mtype)))
(defn parse-library-media
[[file-id media]]
(rx/merge
(let [markup
(->> (vals media)
(reduce e/collect-media {})
(json/encode))]
(rx/of (vector (str file-id "/media.json") markup)))
(->> (rx/from (vals media))
(rx/map #(assoc % :file-id file-id))
(rx/merge-map
(fn [media]
(let [file-path (str/concat file-id "/media/" (:id media) (cm/mtype->extension (:mtype media)))
blob (data-uri->blob (:uri media))]
(rx/of (vector file-path blob))))))))
(defn export-file
[file]
(let [file (assoc file
:name (:name file)
:file-name (:name file)
:is-shared false)
files-stream (->> (rx/of {(:id file) file})
(rx/share))
manifest-stream
(->> files-stream
(rx/map #(e/create-manifest (uuid/next) (:id file) :all % cfeat/default-features))
(rx/map (fn [a]
(vector "manifest.json" a))))
render-stream
(->> files-stream
(rx/merge-map vals)
(rx/merge-map e/process-pages)
(rx/observe-on :async)
(rx/merge-map e/get-page-data)
(rx/share))
colors-stream
(->> files-stream
(rx/merge-map vals)
(rx/map #(vector (:id %) (get-in % [:data :colors])))
(rx/filter #(d/not-empty? (second %)))
(rx/map e/parse-library-color))
typographies-stream
(->> files-stream
(rx/merge-map vals)
(rx/map #(vector (:id %) (get-in % [:data :typographies])))
(rx/filter #(d/not-empty? (second %)))
(rx/map e/parse-library-typographies))
media-stream
(->> files-stream
(rx/merge-map vals)
(rx/map #(vector (:id %) (get-in % [:data :media])))
(rx/filter #(d/not-empty? (second %)))
(rx/merge-map parse-library-media))
components-stream
(->> files-stream
(rx/merge-map vals)
(rx/filter #(d/not-empty? (ctkl/components-seq (:data %))))
(rx/merge-map e/parse-library-components))
pages-stream
(->> render-stream
(rx/map e/collect-page))]
(rx/merge
(->> render-stream
(rx/map #(hash-map
:type :progress
:file (:id file)
:data (str "Render " (:file-name %) " - " (:name %)))))
(->> (rx/merge
manifest-stream
pages-stream
components-stream
media-stream
colors-stream
typographies-stream)
(rx/reduce conj [])
(rx/with-latest-from files-stream)
(rx/merge-map (fn [[data _]]
(->> (uz/compress-files data)
(rx/map #(vector file %)))))))))
(deftype File [^:mutable file]
Object
(addPage [_ name]
(set! file (fb/add-page file {:name name}))
(str (:current-page-id file)))
(addPage [_ name options]
(set! file (fb/add-page file {:name name :options (parse-data options)}))
(str (:current-page-id file)))
(closePage [_]
(set! file (fb/close-page file)))
(addArtboard [_ data]
(set! file (fb/add-artboard file (parse-data data)))
(str (:last-id file)))
(closeArtboard [_]
(set! file (fb/close-artboard file)))
(addGroup [_ data]
(set! file (fb/add-group file (parse-data data)))
(str (:last-id file)))
(closeGroup [_]
(set! file (fb/close-group file)))
(addBool [_ data]
(set! file (fb/add-bool file (parse-data data)))
(str (:last-id file)))
(closeBool [_]
(set! file (fb/close-bool file)))
(createRect [_ data]
(set! file (fb/create-rect file (parse-data data)))
(str (:last-id file)))
(createCircle [_ data]
(set! file (fb/create-circle file (parse-data data)))
(str (:last-id file)))
(createPath [_ data]
(set! file (fb/create-path file (parse-data data)))
(str (:last-id file)))
(createText [_ data]
(set! file (fb/create-text file (parse-data data)))
(str (:last-id file)))
(createImage [_ data]
(set! file (fb/create-image file (parse-data data)))
(str (:last-id file)))
(createSVG [_ data]
(set! file (fb/create-svg-raw file (parse-data data)))
(str (:last-id file)))
(closeSVG [_]
(set! file (fb/close-svg-raw file)))
(addLibraryColor [_ data]
(set! file (fb/add-library-color file (parse-data data)))
(str (:last-id file)))
(updateLibraryColor [_ data]
(set! file (fb/update-library-color file (parse-data data)))
(str (:last-id file)))
(deleteLibraryColor [_ data]
(set! file (fb/delete-library-color file (parse-data data)))
(str (:last-id file)))
(addLibraryMedia [_ data]
(set! file (fb/add-library-media file (parse-data data)))
(str (:last-id file)))
(deleteLibraryMedia [_ data]
(set! file (fb/delete-library-media file (parse-data data)))
(str (:last-id file)))
(addLibraryTypography [_ data]
(set! file (fb/add-library-typography file (parse-data data)))
(str (:last-id file)))
(deleteLibraryTypography [_ data]
(set! file (fb/delete-library-typography file (parse-data data)))
(str (:last-id file)))
(startComponent [_ data]
(set! file (fb/start-component file (parse-data data)))
(str (:current-component-id file)))
(finishComponent [_]
(set! file (fb/finish-component file)))
(createComponentInstance [_ data]
(set! file (fb/create-component-instance file (parse-data data)))
(str (:last-id file)))
(lookupShape [_ shape-id]
(clj->js (fb/lookup-shape file (uuid/parse shape-id))))
(updateObject [_ id new-obj]
(let [old-obj (fb/lookup-shape file (uuid/parse id))
new-obj (d/deep-merge old-obj (parse-data new-obj))]
(set! file (fb/update-object file old-obj new-obj))))
(deleteObject [_ id]
(set! file (fb/delete-object file (uuid/parse id))))
(getId [_]
(:id file))
(getCurrentPageId [_]
(:current-page-id file))
(asMap [_]
(clj->js file))
(newId [_]
(uuid/next))
(export [_]
(p/create
(fn [resolve reject]
(->> (export-file file)
(rx/filter #(not= (:type %) :progress))
(rx/take 1)
(rx/subs!
(fn [value]
(let [[_ export-blob] value]
(resolve export-blob)))
reject))))))
(defn create-file-export [^string name]
(binding [cfeat/*current* cfeat/default-features]
(File. (fb/create-file name))))
(defn exports []
#js {:createFile create-file-export})

View File

@@ -1,28 +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.libs.render
(:require
[app.common.uuid :as uuid]
[app.main.render :as r]
[beicon.v2.core :as rx]
[promesa.core :as p]))
(defn render-page-export
[file ^string page-id]
;; Better to expose the api as a promise to be consumed from JS
(let [page-id (uuid/parse page-id)
file-data (.-file file)
data (get-in file-data [:data :pages-index page-id])]
(p/create
(fn [resolve reject]
(->> (r/render-page data)
(rx/take 1)
(rx/subs! resolve reject))))))
(defn exports []
#js {:renderPage render-page-export})

View File

@@ -8,7 +8,7 @@
(:refer-clojure :exclude [meta reset!])
(:require
["@penpot/mousetrap$default" :as mousetrap]
[app.common.data.macros :as dm]
[app.common.data :as d]
[app.common.logging :as log]
[app.common.schema :as sm]
[app.config :as cf]
@@ -135,7 +135,7 @@
[:fn {:optional true} fn?]
[:tooltip {:optional true} :string]]])
(def check-shortcuts!
(def ^:private check-shortcuts
(sm/check-fn schema:shortcuts))
(defn- wrap-cb
@@ -167,23 +167,20 @@
(mousetrap/reset)
(bind! shortcuts)))
(def ^:private conj*
(fnil conj (d/ordered-map)))
(defn push-shortcuts
[key shortcuts]
(assert (keyword? key) "expected a keyword for `key`")
(let [shortcuts (check-shortcuts shortcuts)]
(ptk/reify ::push-shortcuts
ptk/UpdateEvent
(update [_ state]
(update state :shortcuts conj* [key shortcuts]))
(dm/assert!
"expected valid parameters"
(and (keyword? key)
(check-shortcuts! shortcuts)))
(ptk/reify ::push-shortcuts
ptk/UpdateEvent
(update [_ state]
(-> state
(update :shortcuts (fnil conj '()) [key shortcuts])))
ptk/EffectEvent
(effect [_ state _]
(let [[_key shortcuts] (peek (:shortcuts state))]
ptk/EffectEvent
(effect [_ _ _]
(reset! shortcuts)))))
(defn pop-shortcuts
@@ -192,12 +189,9 @@
ptk/UpdateEvent
(update [_ state]
(update state :shortcuts (fn [shortcuts]
(let [current-key (first (peek shortcuts))]
(if (= key current-key)
(pop shortcuts)
shortcuts)))))
(dissoc shortcuts key))))
ptk/EffectEvent
(effect [_ state _]
(let [[key* shortcuts] (peek (:shortcuts state))]
(when (not= key key*)
(reset! shortcuts))))))
(let [[_key shortcuts] (last (:shortcuts state))]
(reset! shortcuts)))))

View File

@@ -771,17 +771,16 @@
;; --- Update Shape Attrs
;; FIXME: rename to update-shape-generic-attrs because on the end we
;; only allow here to update generic attrs
(defn update-shape
[id attrs]
(dm/assert!
"expected valid parameters"
(and (cts/check-shape-attrs! attrs)
(uuid? id)))
(ptk/reify ::update-shape
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dwsh/update-shapes [id] #(merge % attrs))))))
(assert (uuid? id) "expected valid uuid for `id`")
(let [attrs (cts/check-shape-generic-attrs attrs)]
(ptk/reify ::update-shape
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dwsh/update-shapes [id] #(merge % attrs)))))))
(defn start-rename-shape
"Start shape renaming process"
@@ -832,10 +831,6 @@
(defn update-selected-shapes
[attrs]
(dm/assert!
"expected valid shape attrs"
(cts/check-shape-attrs! attrs))
(ptk/reify ::update-selected-shapes
ptk/WatchEvent
(watch [_ state _]

View File

@@ -12,7 +12,6 @@
[app.common.geom.shapes :as gsh]
[app.common.types.component :as ctc]
[app.common.types.container :as ctn]
[app.common.types.path :as path]
[app.common.types.path.bool :as bool]
[app.common.types.shape :as cts]
[app.common.types.shape.layout :as ctl]
@@ -30,9 +29,6 @@
(let [shape-id
(or id (uuid/next))
shapes
(mapv #(path/convert-to-path % objects) shapes)
head
(if (= type :difference) (first shapes) (last shapes))
@@ -48,13 +44,13 @@
:frame-id (:frame-id head)
:parent-id (:parent-id head)
:name name
:shapes (mapv :id shapes)}
:shapes (vec shapes)}
shape
(-> shape
(merge (select-keys head bool/style-properties))
(cts/setup-shape)
(gsh/update-bool shapes objects))]
(gsh/update-bool objects))]
[shape (cph/get-position-on-parent objects (:id head))]))
@@ -108,19 +104,16 @@
(defn group->bool
[type group objects]
(let [shapes (->> (:shapes group)
(map #(get objects %))
(mapv #(path/convert-to-path % objects)))
(map (d/getf objects)))
head (if (= type :difference) (first shapes) (last shapes))
head (cond-> head
(and (contains? head :svg-attrs) (empty? (:fills head)))
(assoc :fills bool/default-fills))
head-data (select-keys head bool/style-properties)]
(assoc :fills bool/default-fills))]
(-> group
(assoc :type :bool)
(assoc :bool-type type)
(merge head-data)
(gsh/update-bool shapes objects))))
(merge (select-keys head bool/style-properties))
(gsh/update-bool objects))))
(defn group-to-bool
[shape-id type]

View File

@@ -254,20 +254,17 @@
(defn add-media
[media]
(dm/assert!
"expected valid media object"
(ctf/check-media-object! media))
(let [media (ctf/check-media-object media)]
(ptk/reify ::add-media
ev/Event
(-data [_] media)
(ptk/reify ::add-media
ev/Event
(-data [_] media)
ptk/WatchEvent
(watch [it _ _]
(let [obj (select-keys media [:id :name :width :height :mtype])
changes (-> (pcb/empty-changes it)
(pcb/add-media obj))]
(rx/of (dch/commit-changes changes))))))
ptk/WatchEvent
(watch [it _ _]
(let [obj (select-keys media [:id :name :width :height :mtype])
changes (-> (pcb/empty-changes it)
(pcb/add-media obj))]
(rx/of (dch/commit-changes changes)))))))
(defn rename-media
[id new-name]
@@ -297,10 +294,7 @@
(defn delete-media
[{:keys [id]}]
(dm/assert!
"expected valid uuid for `id`"
(uuid? id))
(assert (uuid? id) "expected valid uuid for `id`")
(ptk/reify ::delete-media
ev/Event
(-data [_] {:id id})
@@ -316,11 +310,8 @@
(defn add-typography
([typography] (add-typography typography true))
([typography edit?]
(let [typography (update typography :id #(or % (uuid/next)))]
(dm/assert!
"expected valid typography"
(ctt/check-typography! typography))
(let [typography (-> (update typography :id #(or % (uuid/next)))
(ctt/check-typography))]
(ptk/reify ::add-typography
ev/Event
(-data [_] typography)
@@ -349,16 +340,12 @@
(defn update-typography
[typography file-id]
(dm/assert!
"expected valid typography and file-id"
(and (ctt/check-typography! typography)
(uuid? file-id)))
(ptk/reify ::update-typography
ptk/WatchEvent
(watch [it state _]
(do-update-tipography it state typography file-id))))
(assert (uuid? file-id) "expected valid uuid for `file-id`")
(let [typography (ctt/check-typography typography)]
(ptk/reify ::update-typography
ptk/WatchEvent
(watch [it state _]
(do-update-tipography it state typography file-id)))))
(defn rename-typography
[file-id id new-name]
@@ -1026,7 +1013,7 @@
(cll/generate-component-swap objects shape ldata page libraries id-new-component index target-cell keep-props-values))
changes (if keep-touched?
(clv/generate-keep-touched changes new-shape shape orig-shapes page)
(clv/generate-keep-touched changes new-shape shape orig-shapes page libraries)
changes)]
(rx/of

View File

@@ -110,9 +110,7 @@
(add-shape shape {}))
([shape {:keys [no-select? no-update-layout?]}]
(dm/assert!
"expected valid shape"
(cts/check-shape! shape))
(cts/check-shape shape)
(ptk/reify ::add-shape
ptk/WatchEvent
@@ -293,30 +291,28 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn update-shape-flags
[ids {:keys [blocked hidden undo-group] :as flags}]
(dm/assert!
"expected valid coll of uuids"
(every? uuid? ids))
[ids flags]
(assert (every? uuid? ids)
"expected valid coll of uuids")
(dm/assert!
"expected valid shape-attrs value for `flags`"
(cts/check-shape-attrs! flags))
(let [{:keys [blocked hidden undo-group]}
(cts/check-shape-generic-attrs flags)]
(ptk/reify ::update-shape-flags
ptk/WatchEvent
(watch [_ state _]
(let [update-fn
(fn [obj]
(cond-> obj
(boolean? blocked) (assoc :blocked blocked)
(boolean? hidden) (assoc :hidden hidden)))
objects (dsh/lookup-page-objects state)
;; We have change only the hidden behaviour, to hide only the
;; selected shape, block behaviour remains the same.
ids (if (boolean? blocked)
(into ids (->> ids (mapcat #(cfh/get-children-ids objects %))))
ids)]
(rx/of (update-shapes ids update-fn {:attrs #{:blocked :hidden} :undo-group undo-group}))))))
(ptk/reify ::update-shape-flags
ptk/WatchEvent
(watch [_ state _]
(let [update-fn
(fn [obj]
(cond-> obj
(boolean? blocked) (assoc :blocked blocked)
(boolean? hidden) (assoc :hidden hidden)))
objects (dsh/lookup-page-objects state)
;; We have change only the hidden behaviour, to hide only the
;; selected shape, block behaviour remains the same.
ids (if (boolean? blocked)
(into ids (->> ids (mapcat #(cfh/get-children-ids objects %))))
ids)]
(rx/of (update-shapes ids update-fn {:attrs #{:blocked :hidden} :undo-group undo-group})))))))
(defn toggle-visibility-selected
[]

View File

@@ -32,6 +32,10 @@
{:error/code :error.import/style-dictionary-reference-errors
:error/fn #(tr "workspace.token.import-error")}
:error.token/empty-input
{:error/code :error.token/empty-input
:error/fn #(tr "workspace.token.empty-input")}
:error.token/direct-self-reference
{:error/code :error.token/direct-self-reference
:error/fn #(tr "workspace.token.self-reference")}

View File

@@ -51,15 +51,13 @@
;; TODO HYMA: Copied over from workspace.cljs
(defn update-shape
[id attrs]
(dm/assert!
"expected valid parameters"
(and (cts/check-shape-attrs! attrs)
(uuid? id)))
(assert (uuid? id) "expected valid uuid for `id`")
(ptk/reify ::update-shape
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dwsh/update-shapes [id] #(merge % attrs))))))
(let [attrs (cts/check-shape-attrs attrs)]
(ptk/reify ::update-shape
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dwsh/update-shapes [id] #(merge % attrs)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKENS Actions

View File

@@ -217,11 +217,14 @@
;; When the horizontal/vertical scale a flex children with auto/fill
;; we change it too fixed
set-fix-width?
change-width?
(not (mth/close? (dm/get-prop scalev :x) 1))
set-fix-height?
(not (mth/close? (dm/get-prop scalev :y) 1))]
change-height?
(not (mth/close? (dm/get-prop scalev :y) 1))
auto-width-text? (and (cfh/text-shape? shape) (= :auto-width (dm/get-prop shape :grow-type)))
auto-height-text? (and (cfh/text-shape? shape) (= :auto-height (dm/get-prop shape :grow-type)))]
(cond-> (ctm/empty)
(some? displacement)
@@ -230,12 +233,18 @@
:always
(ctm/resize scalev resize-origin shape-transform shape-transform-inverse)
^boolean set-fix-width?
^boolean change-width?
(ctm/change-property :layout-item-h-sizing :fix)
^boolean set-fix-height?
^boolean change-height?
(ctm/change-property :layout-item-v-sizing :fix)
(and auto-width-text? (or change-width? change-height?))
(ctm/change-property :grow-type :fixed)
(and auto-height-text? change-height?)
(ctm/change-property :grow-type :fixed)
^boolean scale-text
(ctm/scale-content (dm/get-prop scalev :x)))))

View File

@@ -214,15 +214,15 @@
(nil? font)
(p/resolved font-id)
;; Font already loaded, we just continue
;; Font already loaded, we just continue
(contains? @loaded font-id)
(p/resolved font-id)
;; Font is currently downloading. We attach the caller to the promise
;; Font is currently downloading. We attach the caller to the promise
(contains? @loading font-id)
(get @loading font-id)
;; First caller, we create the promise and then wait
;; First caller, we create the promise and then wait
:else
(let [on-load (fn [resolve]
(swap! loaded conj font-id)

View File

@@ -58,8 +58,7 @@
[file-id revn]
(->> (wrk/ask! {:cmd :thumbnails/generate-for-file
:revn revn
:file-id file-id
:features (get @st/state :features)})
:file-id file-id})
(rx/mapcat (fn [{:keys [fonts] :as result}]
(->> (fonts/render-font-styles fonts)
(rx/map (fn [styles]

View File

@@ -40,7 +40,7 @@ export default {
control: { type: "select" },
},
variant: {
options: ["dense", "comfortable"],
options: ["dense", "comfortable", "seamless"],
control: { type: "select" },
},
disabled: {

View File

@@ -13,6 +13,7 @@
--input-bg-color: var(--color-background-tertiary);
--input-fg-color: var(--color-foreground-primary);
--input-icon-color: var(--color-foreground-secondary);
--input-text-indent: 0;
--input-outline-color: none;
--input-height: #{$sz-32};
--input-margin: unset;
@@ -43,7 +44,8 @@
}
}
.variant-dense {
.variant-dense,
.variant-seamless {
@include use-typography("body-small");
}
@@ -79,6 +81,7 @@
border: none;
background: none;
inline-size: 100%;
text-indent: var(--input-text-indent, 0);
font-family: inherit;
font-size: inherit;

View File

@@ -33,6 +33,7 @@
[app.main.ui.releases.v2-4]
[app.main.ui.releases.v2-5]
[app.main.ui.releases.v2-6]
[app.main.ui.releases.v2-7]
[app.util.object :as obj]
[app.util.timers :as tm]
[rumext.v2 :as mf]))
@@ -97,4 +98,4 @@
(defmethod rc/render-release-notes "0.0"
[params]
(rc/render-release-notes (assoc params :version "2.6")))
(rc/render-release-notes (assoc params :version "2.7")))

View File

@@ -0,0 +1,142 @@
;; 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-7
(: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]))
(defmethod c/render-release-notes "2.7"
[{: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.7-slide-0.jpg"
:class (stl/css :start-image)
:border "0"
:alt "Design Tokens make their debut in Penpot!"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Whats 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 2.7 is out!"]
[:p {:class (stl/css :feature-content)}
"After the huge excitement around our last release. The first-ever native Design Tokens support in a design tool (yay!), were keeping the momentum going with a fresh batch of new features and improvements."]
[:p {:class (stl/css :feature-content)}
"This update brings the first set of upgrades to our new Design Tokens system, a few of the many to come. Weve also expanded who can create sharing prototype links and improved the invitations area. Last but not least, we fixed a bunch of bugs and optimizations that will make the experience more enjoyable for all."]
[:p {:class (stl/css :feature-content)}
"Lets 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.7-duplicate-set.gif"
:class (stl/css :start-image)
:border "0"
:alt "Design Tokens improvements"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Design Tokens improvements"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"It hasnt been long since we launched Design Tokens in Penpot (the first native Design Tokens support in a design tool!), and were already rolling out the first set of improvements."]
[:p {:class (stl/css :feature-content)}
"The highlight: you can now duplicate token sets directly from a menu item. A huge time-saver, especially when working from existing sets. Weve also made it easier to create themes by letting you select their set right away, and weve polished some info indicators to make everything a bit clearer. Plus, weve fixed a bunch of early-stage bugs to keep things running smoothly."]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 3}]
[: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.7-share.gif"
:class (stl/css :start-image)
:border "0"
:alt "Editors and viewers can now create Share prototype links"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Editors and viewers can now create Share prototype links"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"From now on, both editors and viewers can create Share Prototype links. Sharing prototypes is key for better team collaboration, no matter the role. Its a common need, team members often have to share presentations without risking any accidental changes to the designs, which means they dont necessarily need editing permissions. In the future, Penpot will introduce more fine-grained control over these permissions."]
[:p {:class (stl/css :feature-content)}
"This update gives editors and viewers the same ability to configure, create, copy, and delete sharing links. A capability that, until now, was limited to owners and admins."]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 3}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
2
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.7-invitations.gif"
:class (stl/css :start-image)
:border "0"
:alt "A clearer way to invite your first team members"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"A clearer way to invite your first team members"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"Penpot works perfectly for solo projects, but its always more fun with a team. Thats why weve updated the initial state of the invitations area. Instead of starting blank, it now offers clearer guidance to help you invite your first team members."]
[:p {:class (stl/css :feature-content)}
"This improvement in design and UX writing comes from community member Prithvi Tharun (credit where its due!) Not all open source contributions are about code, and this is a fantastic example of how design and writing make a real difference too."]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 3}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))

View File

@@ -0,0 +1,102 @@
// 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;
}

View File

@@ -586,10 +586,10 @@
(mf/with-effect []
(st/emit! (st/emit! (dsc/push-shortcuts ::colorpicker sc/shortcuts)))
#(do
(st/emit! (dsc/pop-shortcuts ::colorpicker))
(when (and @dirty? @last-change on-close)
(on-close @last-change))))
(fn []
(st/emit! (dsc/pop-shortcuts ::colorpicker))
(when (and @dirty? @last-change on-close)
(on-close @last-change))))
[:div {:class (stl/css :colorpicker-tooltip)
:data-testid "colorpicker"

View File

@@ -9,38 +9,37 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.constants :refer [max-input-length]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.controls.utilities.input-field :refer [input-field*]]
[app.main.ui.ds.controls.utilities.label :refer [label*]]
[rumext.v2 :as mf]))
(def ^:private schema::input-tokens-value
[:map
[:label :string]
[:placeholder {:optional true} :string]
[:default-value {:optional true} [:maybe :string]]
[:value {:optional true} [:maybe :string]]
[:class {:optional true} :string]
[:max-length {:optional true} :int]
[:error {:optional true} :boolean]
[:value {:optional true} :string]])
[:error {:optional true} :boolean]])
(mf/defc input-tokens-value*
{::mf/props :obj
::mf/forward-ref true
::mf/schema schema::input-tokens-value}
[{:keys [class label max-length error value children] :rest props} ref]
[{:keys [class label placeholder error value children] :rest props} ref]
(let [id (mf/use-id)
input-ref (mf/use-ref)
props (mf/spread-props props {:id id
:type "text"
:class (stl/css :input)
:aria-invalid error
:max-length (d/nilv max-length max-input-length)
:value value
:placeholder placeholder
:value (d/nilv value "")
:variant "comfortable"
:hint-type (when error "error")
:ref (or ref input-ref)})]
[:div {:class (dm/str class " " (stl/css-case :wrapper true
:input-error error))}
[:label {:for id :class (stl/css :label)} label]
[:> label* {:for id} label]
[:div {:class (stl/css :input-wrapper)}
(when (some? children)
[:div {:class (stl/css :input-swatch)} children])
[:> input* props]]]))
[:> input-field* props]]]))

View File

@@ -41,7 +41,7 @@
position: relative;
& .input {
padding-inline-start: var(--sp-xxxl);
--input-text-indent: var(--sp-xxl);
}
}
}

View File

@@ -28,7 +28,6 @@
[app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
[app.main.ui.workspace.colorpicker :as colorpicker]
[app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector*]]
@@ -112,7 +111,7 @@
token-name (if (str/empty? name-value) "__TOKEN_STUDIO_SYSTEM.TEMP" name-value)]
(cond
(empty? (str/trim value))
(p/rejected {:errors [{:error/code :error/empty-input}]})
(p/rejected {:errors [(wte/get-error-code :error.token/empty-input)]})
(ctob/token-value-self-reference? token-name value)
(p/rejected {:errors [(wte/get-error-code :error.token/direct-self-reference)]})
@@ -217,25 +216,27 @@
:on-finish-drag on-finish-drag
:on-change on-change'}]]))
(mf/defc token-value-or-errors
[{:keys [result-or-errors]}]
(let [{:keys [errors warnings resolved-value]} result-or-errors
empty-message? (or (nil? result-or-errors)
(wte/has-error-code? :error/empty-input errors))
(mf/defc token-value-hint
[{:keys [result]}]
(let [{:keys [errors warnings resolved-value]} result
empty-message? (nil? result)
message (cond
empty-message? (tr "workspace.token.resolved-value" "-")
warnings (wtw/humanize-warnings warnings)
errors (->> (wte/humanize-errors errors)
(str/join "\n"))
:else (tr "workspace.token.resolved-value" (or resolved-value result-or-errors)))]
[:> text* {:as "p"
:typography "body-small"
:class (stl/css-case :resolved-value true
:resolved-value-placeholder empty-message?
:resolved-value-warning (seq warnings)
:resolved-value-error (seq errors))}
message]))
:else (tr "workspace.token.resolved-value" (or resolved-value result)))
type (cond
empty-message? "hint"
errors "error"
warnings "warning"
:else "hint")]
[:> hint-message*
{:id "token-value-hint"
:message message
:class (stl/css-case :resolved-value (not (or empty-message? (seq warnings) (seq errors))))
:type type}]))
(mf/defc form
{::mf/wrap-props false}
@@ -331,7 +332,7 @@
color-ramp-open* (mf/use-state false)
color-ramp-open? (deref color-ramp-open*)
value-input-ref (mf/use-ref nil)
value-ref (mf/use-var (:value token))
value-ref (mf/use-ref (:value token))
token-resolve-result* (mf/use-state (get resolved-tokens (cft/token-identifier token)))
token-resolve-result (deref token-resolve-result*)
@@ -364,7 +365,7 @@
(dom/set-value! (mf/ref-val value-input-ref) hex)
hex)
value)]
(reset! value-ref value')
(mf/set-ref-val! value-ref value')
(on-update-value-debounced value'))))
on-update-color (mf/use-fn
(mf/deps color on-update-value-debounced)
@@ -389,7 +390,7 @@
color-value (-> (tinycolor/valid-color hex-value)
(tinycolor/set-alpha (or alpha 1))
(tinycolor/->string format))]
(reset! value-ref color-value)
(mf/set-ref-val! value-ref color-value)
(dom/set-value! (mf/ref-val value-input-ref) color-value)
(on-update-value-debounced color-value))))
@@ -443,7 +444,7 @@
;; and press enter before the next validations could return.
(let [final-name (finalize-name @token-name-ref)
valid-name?+ (-> (validate-name final-name) schema-validation->promise)
final-value (finalize-value @value-ref)
final-value (finalize-value (mf/ref-val value-ref))
final-description @description-ref
valid-description?+ (some-> final-description validate-descripion schema-validation->promise)]
(-> (p/all [valid-name?+
@@ -558,13 +559,12 @@
[:div {:class (stl/css :input-row)}
[:> input-tokens-value*
{:id "token-value"
:placeholder (tr "workspace.token.token-value-enter")
{:placeholder (tr "workspace.token.token-value-enter")
:label (tr "workspace.token.token-value")
:max-length 256
:default-value @value-ref
:value (mf/ref-val value-ref)
:ref value-input-ref
:on-change on-update-value
:error (not (nil? (:errors token-resolve-result)))
:on-blur on-update-value}
(when color?
[:> input-token-color-bullet*
@@ -573,10 +573,11 @@
(when color-ramp-open?
[:> ramp* {:color (some-> color (tinycolor/valid-color))
:on-change on-update-color}])
[:& token-value-or-errors {:result-or-errors token-resolve-result}]]
[:& token-value-hint {:result token-resolve-result}]]
[:div {:class (stl/css :input-row)}
[:> input* {:label (tr "workspace.token.token-description")
:placeholder (tr "workspace.token.enter-token-description")
:placeholder (tr "workspace.token.token-description")
:is-optional true
:max-length max-input-length
:variant "comfortable"
:default-value @description-ref

View File

@@ -50,14 +50,7 @@
.resolved-value {
--input-hint-color: var(--color-foreground-primary);
margin-bottom: 0;
padding: $s-4 $s-6;
color: var(--input-hint-color);
white-space: pre-wrap;
}
.resolved-value-placeholder {
--input-hint-color: var(--color-foreground-secondary);
}
.resolved-value-error {

View File

@@ -472,16 +472,23 @@
(defn setup-shortcuts
[path-editing? drawing-path? text-editing? grid-editing?]
(hooks/use-shortcuts ::workspace wsc/shortcuts)
(mf/use-effect
(mf/deps path-editing? drawing-path? grid-editing?)
(fn []
(cond
grid-editing?
(do (st/emit! (dsc/push-shortcuts ::grid gsc/shortcuts))
#(st/emit! (dsc/pop-shortcuts ::grid)))
(or drawing-path? path-editing?)
(do (st/emit! (dsc/push-shortcuts ::path psc/shortcuts))
#(st/emit! (dsc/pop-shortcuts ::path)))
text-editing?
(do (st/emit! (dsc/push-shortcuts ::text tsc/shortcuts))
#(st/emit! (dsc/pop-shortcuts ::text)))))))
(mf/with-effect [path-editing? drawing-path? grid-editing?]
(cond
grid-editing?
(do
(st/emit! (dsc/push-shortcuts ::grid gsc/shortcuts))
(fn []
(st/emit! (dsc/pop-shortcuts ::grid))))
(or drawing-path? path-editing?)
(do
(st/emit! (dsc/push-shortcuts ::path psc/shortcuts))
(fn []
(st/emit! (dsc/pop-shortcuts ::path))))
text-editing?
(do
(st/emit! (dsc/push-shortcuts ::text tsc/shortcuts))
(fn []
(st/emit! (dsc/pop-shortcuts ::text)))))))

View File

@@ -842,9 +842,12 @@
(defn initialize
[base-objects zoom vbox background]
(let [rgba (sr-clr/hex->u32argb background 1)]
(let [rgba (sr-clr/hex->u32argb background 1)
shapes (into [] (vals base-objects))
total-shapes (count shapes)]
(h/call wasm/internal-module "_set_canvas_background" rgba)
(h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox)))
(h/call wasm/internal-module "_init_shapes_pool" total-shapes)
(set-objects base-objects)))
(def ^:private canvas-options

View File

@@ -6,7 +6,7 @@
(ns app.util.object
"A collection of helpers for work with javascript objects."
(:refer-clojure :exclude [set! new get merge clone contains? array? into-array reify])
(:refer-clojure :exclude [set! new get merge clone contains? array? into-array reify class])
#?(:cljs (:require-macros [app.util.object]))
(:require
[clojure.core :as c]))
@@ -93,6 +93,51 @@
(when (some? obj)
(js* "Object.entries(~{}).reduce((a, [k,v]) => (v == null ? a : (a[k]=v, a)), {}) " obj))))
#?(:cljs
(defn plain-object?
^boolean
[o]
(and (some? o)
(identical? (.getPrototypeOf js/Object o)
(.-prototype js/Object)))))
;; EXPERIMENTAL: unsafe, does not checks and not validates the input,
;; should be improved over time, for now it works for define a class
;; extending js/Error that is more than enought for a first, quick and
;; dirty macro impl for generating classes.
(defmacro class
"Create a class instance"
[& {:keys [name extends constructor]}]
(let [params
(if (and constructor (= 'fn (first constructor)))
(into [] (drop 1) (second constructor))
[])
constructor-sym
(symbol name)
constructor
(if constructor
constructor
`(fn ~name [~'this]
(.call ~extends ~'this)))]
`(let [konstructor# ~constructor
extends# ~extends
~constructor-sym
(fn ~constructor-sym ~params
(cljs.core/this-as ~'this
(konstructor# ~'this ~@params)))]
(set! (.-prototype ~constructor-sym)
(js/Object.create (.-prototype extends#)))
(set! (.-constructor (.-prototype ~constructor-sym))
konstructor#)
~constructor-sym)))
(defmacro add-properties!
"Adds properties to an object using `.defineProperty`"
[rsym & properties]

View File

@@ -421,11 +421,9 @@
:uri uri}))
(rx/catch
(fn [cause]
(rx/of (ex/raise :type :internal
:code :export-error
:hint "unexpected error on exporting file"
:file-id (:id file)
:cause cause))))))))
(rx/of {:type :error
:file-id (:id file)
:hint (ex-message cause)})))))))
(= format :legacy-zip)
(->> (rx/from files)

View File

@@ -8,16 +8,10 @@
(:refer-clojure :exclude [resolve])
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.files.builder :as fb]
[app.common.geom.point :as gpt]
[app.common.json :as json]
[app.common.logging :as log]
[app.common.media :as cm]
[app.common.schema :as sm]
[app.common.text :as ct]
[app.common.time :as tm]
[app.common.types.path :as path]
[app.common.uuid :as uuid]
[app.main.repo :as rp]
[app.util.http :as http]
@@ -25,10 +19,8 @@
[app.util.sse :as sse]
[app.util.zip :as uz]
[app.worker.impl :as impl]
[app.worker.import.parser :as parser]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[tubax.core :as tubax]))
[cuerdas.core :as str]))
(log/set-level! :warn)
@@ -37,185 +29,12 @@
(def conjv (fnil conj []))
(def ^:private iso-date-rx
"Incomplete ISO regex for detect datetime-like values on strings"
#"^\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d.*")
(defn read-json-key
[m]
(or (uuid/parse m)
(json/read-kebab-key m)))
(defn read-json-val
[m]
(cond
(and (string? m)
(re-matches uuid/regex m))
(uuid/uuid m)
(and (string? m)
(re-matches iso-date-rx m))
(or (ex/ignoring (tm/parse-instant m)) m)
:else
m))
(defn get-file
"Resolves the file inside the context given its id and the
data. LEGACY"
([context type]
(get-file context type nil nil))
([context type id]
(get-file context type id nil))
([context type id media]
(let [file-id (:file-id context)
path (case type
:manifest "manifest.json"
:page (str file-id "/" id ".svg")
:colors-list (str file-id "/colors.json")
:colors (let [ext (cm/mtype->extension (:mtype media))]
(str/concat file-id "/colors/" id ext))
:typographies (str file-id "/typographies.json")
:media-list (str file-id "/media.json")
:media (let [ext (cm/mtype->extension (:mtype media))]
(str/concat file-id "/media/" id ext))
:components (str file-id "/components.svg")
:deleted-components (str file-id "/deleted-components.svg"))
parse-svg? (and (not= type :media) (str/ends-with? path "svg"))
parse-json? (and (not= type :media) (str/ends-with? path "json"))
file-type (if (or parse-svg? parse-json?) "text" "blob")]
(log/debug :action "parsing" :path path)
(let [stream (->> (uz/get-file (:zip context) path file-type)
(rx/map :content))]
(cond
parse-svg?
(rx/map tubax/xml->clj stream)
parse-json?
(rx/map #(json/decode % :key-fn read-json-key :val-fn read-json-val) stream)
:else
stream)))))
(defn- read-zip-manifest
[zipfile]
(->> (uz/get-file zipfile "manifest.json")
(rx/map :content)
(rx/map json/decode)))
(defn progress!
([context type]
(assert (keyword? type))
(progress! context type nil nil nil))
([context type file]
(assert (keyword? type))
(assert (string? file))
(progress! context type file nil nil))
([context type current total]
(assert (keyword? type))
(assert (number? current))
(assert (number? total))
(progress! context type nil current total))
([context type file current total]
(when (and context (contains? context :progress))
(let [progress {:type type
:file file
:current current
:total total}]
(log/debug :status :progress :progress progress)
(rx/push! (:progress context) {:file-id (:file-id context)
:status :progress
:progress progress})))))
(defn resolve-factory
"Creates a wrapper around the atom to remap ids to new ids and keep
their relationship so they ids are coherent."
[]
(let [id-mapping-atom (atom {})
resolve
(fn [id-mapping id]
(assert (uuid? id) (str id))
(get id-mapping id))
set-id
(fn [id-mapping id]
(assert (uuid? id) (str id))
(cond-> id-mapping
(nil? (resolve id-mapping id))
(assoc id (uuid/next))))]
(fn [id]
(when (some? id)
(swap! id-mapping-atom set-id id)
(resolve @id-mapping-atom id)))))
(defn create-file
"Create a new file on the back-end"
[context features]
(let [resolve-fn (:resolve context)
file-id (resolve-fn (:file-id context))]
(rp/cmd! :create-temp-file
{:id file-id
:name (:name context)
:is-shared (:is-shared context)
:project-id (:project-id context)
:create-page false
;; If the features object exists send that. Otherwise we remove the components/v2 because
;; if the features attribute doesn't exist is a version < 2.0. The other features will
;; be kept so the shapes are created full featured
:features (d/nilv (:features context) (disj features "components/v2"))})))
(defn link-file-libraries
"Create a new file on the back-end"
[context]
(let [resolve (:resolve context)
file-id (resolve (:file-id context))
libraries (->> context :libraries (mapv resolve))]
(->> (rx/from libraries)
(rx/map #(hash-map :file-id file-id :library-id %))
(rx/merge-map (partial rp/cmd! :link-file-to-library)))))
(defn send-changes
"Creates batches of changes to be sent to the backend"
[context file]
(let [file-id (:id file)
session-id (uuid/next)
changes (fb/generate-changes file)
batches (->> changes
(partition change-batch-size change-batch-size nil)
(mapv vec))
processed (atom 0)
total (count batches)]
(rx/concat
(->> (rx/from (d/enumerate batches))
(rx/merge-map
(fn [[i change-batch]]
(->> (rp/cmd! :update-temp-file
{:id file-id
:session-id session-id
:revn i
:changes change-batch})
(rx/tap #(do (swap! processed inc)
(progress! context :upload-data @processed total))))))
(rx/map first)
(rx/ignore))
(->> (rp/cmd! :persist-temp-file {:id file-id})
;; We use merge to keep some information not stored in back-end
(rx/map #(merge file %))))))
(defn slurp-uri
([uri] (slurp-uri uri :text))
([uri response-type]
@@ -225,26 +44,6 @@
:method :get})
(rx/map :body))))
(defn upload-media-files
"Upload a image to the backend and returns its id"
[context file-id name data-uri]
(log/debug :action "Uploading" :file-id file-id :name name)
(->> (http/send!
{:uri data-uri
:response-type :blob
:method :get})
(rx/map :body)
(rx/map
(fn [blob]
{:name name
:file-id file-id
:content blob
:is-local true}))
(rx/tap #(progress! context :upload-media name))
(rx/merge-map #(rp/cmd! :upload-file-media-object %))))
(defn resolve-text-content
[node context]
(let [resolve (:resolve context)]
@@ -290,456 +89,6 @@
(uuid? (get fill :stroke-color-ref-file))
(d/update-when :stroke-color-ref-file resolve)))))))
(defn resolve-data-ids
[data type context]
(let [resolve (:resolve context)]
(-> data
(d/update-when :fill-color-ref-id resolve)
(d/update-when :fill-color-ref-file resolve)
(d/update-when :stroke-color-ref-id resolve)
(d/update-when :stroke-color-ref-file resolve)
(d/update-when :component-id resolve)
(d/update-when :component-file resolve)
(d/update-when :shape-ref resolve)
(cond-> (= type :text)
(d/update-when :content resolve-text-content context))
(cond-> (:fills data)
(d/update-when :fills resolve-fills-content context))
(cond-> (:strokes data)
(d/update-when :strokes resolve-strokes-content context))
(cond-> (and (= type :frame) (= :grid (:layout data)))
(update
:layout-grid-cells
(fn [cells]
(->> (vals cells)
(reduce (fn [cells {:keys [id shapes]}]
(assoc-in cells [id :shapes] (mapv resolve shapes)))
cells))))))))
(defn- translate-frame
[data type file]
(let [frame-id (:current-frame-id file)
frame (when (and (some? frame-id) (not= frame-id uuid/zero))
(fb/lookup-shape file frame-id))]
(if (some? frame)
(-> data
(d/update-when :x + (:x frame))
(d/update-when :y + (:y frame))
(cond-> (= :path type)
(update :content path/move-content (gpt/point (:x frame) (:y frame)))))
data)))
(defn process-import-node
[context file node]
(let [type (parser/get-type node)
close? (parser/close? node)]
(if close?
(case type
:frame (fb/close-artboard file)
:group (fb/close-group file)
:bool (fb/close-bool file)
:svg-raw (fb/close-svg-raw file)
#_default file)
(let [resolve (:resolve context)
old-id (parser/get-id node)
interactions (->> (parser/parse-interactions node)
(mapv #(update % :destination resolve)))
data (-> (parser/parse-data type node)
(resolve-data-ids type context)
(cond-> (some? old-id)
(assoc :id (resolve old-id)))
(cond-> (< (:version context 1) 2)
(translate-frame type file))
;; Shapes inside the deleted component should be stored with absolute coordinates
;; so we calculate that with the x and y stored in the context
(cond-> (:x context)
(assoc :x (:x context)))
(cond-> (:y context)
(assoc :y (:y context))))]
(try
(let [file (case type
:frame (fb/add-artboard file data)
:group (fb/add-group file data)
:bool (fb/add-bool file data)
:rect (fb/create-rect file data)
:circle (fb/create-circle file data)
:path (fb/create-path file data)
:text (fb/create-text file data)
:image (fb/create-image file data)
:svg-raw (fb/create-svg-raw file data)
#_default file)]
;; We store this data for post-processing after every shape has been
;; added
(cond-> file
(d/not-empty? interactions)
(assoc-in [:interactions (:id data)] interactions)))
(catch :default err
(log/error :hint (ex-message err) :cause err :js/data data)
(update file :errors conjv data)))))))
(defn setup-interactions
[file]
(letfn [(add-interactions
[file [id interactions]]
(->> interactions
(reduce #(fb/add-interaction %1 id %2) file)))
(process-interactions
[file]
(let [interactions (:interactions file)
file (dissoc file :interactions)]
(->> interactions (reduce add-interactions file))))]
(-> file process-interactions)))
(defn resolve-media
[context file-id node]
(if (or (and (not (parser/close? node))
(parser/has-image? node))
(parser/has-stroke-images? node)
(parser/has-fill-images? node))
(let [name (parser/get-image-name node)
has-image (parser/has-image? node)
image-data (parser/get-image-data node)
image-fill (parser/get-image-fill node)
fill-images-data (->> (parser/get-fill-images-data node)
(map #(assoc % :type :fill)))
stroke-images-data (->> (parser/get-stroke-images-data node)
(map #(assoc % :type :stroke)))
images-data (concat
fill-images-data
stroke-images-data
(when has-image
[{:href image-data}]))]
(->> (rx/from images-data)
(rx/mapcat (fn [image-data]
(->> (upload-media-files context file-id name (:href image-data))
(rx/catch #(do (.error js/console "Error uploading media: " name)
(rx/of node)))
(rx/map (fn [data]
(let [data
(cond-> data
(some? (:keep-aspect-ratio image-data))
(assoc :keep-aspect-ratio (:keep-aspect-ratio image-data)))]
[(:id image-data) data]))))))
(rx/reduce (fn [acc [id data]] (assoc acc id data)) {})
(rx/map
(fn [images]
(let [media (get images nil)]
(-> node
(assoc :images images)
(cond-> (some? media)
(->
(assoc-in [:attrs :penpot:media-id] (:id media))
(assoc-in [:attrs :penpot:media-width] (:width media))
(assoc-in [:attrs :penpot:media-height] (:height media))
(assoc-in [:attrs :penpot:media-mtype] (:mtype media))
(cond-> (some? (:keep-aspect-ratio media))
(assoc-in [:attrs :penpot:media-keep-aspect-ratio] (:keep-aspect-ratio media)))
(assoc-in [:attrs :penpot:fill-color] (:fill image-fill))
(assoc-in [:attrs :penpot:fill-color-ref-file] (:fill-color-ref-file image-fill))
(assoc-in [:attrs :penpot:fill-color-ref-id] (:fill-color-ref-id image-fill))
(assoc-in [:attrs :penpot:fill-opacity] (:fill-opacity image-fill))
(assoc-in [:attrs :penpot:fill-color-gradient] (:fill-color-gradient image-fill))))))))))
;; If the node is not an image just return the node
(->> (rx/of node)
(rx/observe-on :async))))
(defn media-node? [node]
(or (and (parser/shape? node)
(parser/has-image? node)
(not (parser/close? node)))
(parser/has-stroke-images? node)
(parser/has-fill-images? node)))
(defn import-page
[context file [page-id page-name content]]
(let [nodes (parser/node-seq content)
file-id (:id file)
resolve (:resolve context)
page-data (-> (parser/parse-page-data content)
(assoc :name page-name)
(assoc :id (resolve page-id)))
flows (->> (get page-data :flows)
(map #(update % :starting-frame resolve))
(d/index-by :id)
(not-empty))
guides (-> (get page-data :guides)
(update-vals #(update % :frame-id resolve))
(not-empty))
page-data (cond-> page-data
flows
(assoc :flows flows)
guides
(assoc :guides guides))
file (fb/add-page file page-data)
;; Preprocess nodes to parallel upload the images. Store the result in a table
;; old-node => node with image
;; that will be used in the second pass immediately
pre-process-images
(->> (rx/from nodes)
(rx/filter media-node?)
;; TODO: this should be merge-map, but we disable the
;; parallel upload until we resolve resource usage issues
;; on backend.
(rx/mapcat
(fn [node]
(->> (resolve-media context file-id node)
(rx/map (fn [result]
[node result])))))
(rx/reduce conj {}))]
(->> pre-process-images
(rx/merge-map
(fn [pre-proc]
(->> (rx/from nodes)
(rx/filter parser/shape?)
(rx/map (fn [node] (or (get pre-proc node) node)))
(rx/reduce (partial process-import-node context) file)
(rx/map (comp fb/close-page setup-interactions))))))))
(defn import-component [context file node]
(let [resolve (:resolve context)
content (parser/find-node node :g)
file-id (:id file)
old-id (parser/get-id node)
id (resolve old-id)
path (get-in node [:attrs :penpot:path] "")
type (parser/get-type content)
main-instance-id (resolve (uuid/parse (get-in node [:attrs :penpot:main-instance-id] "")))
main-instance-page (resolve (uuid/parse (get-in node [:attrs :penpot:main-instance-page] "")))
data (-> (parser/parse-data type content)
(assoc :path path)
(assoc :id id)
(assoc :main-instance-id main-instance-id)
(assoc :main-instance-page main-instance-page))
file (-> file (fb/start-component data type))
children (parser/node-seq node)]
(->> (rx/from children)
(rx/filter parser/shape?)
(rx/skip 1) ;; Skip the outer component and the respective closint tag
(rx/skip-last 1) ;; because they are handled in start-component an finish-component
(rx/mapcat (partial resolve-media context file-id))
(rx/reduce (partial process-import-node context) file)
(rx/map fb/finish-component))))
(defn import-deleted-component [context file node]
(let [resolve (:resolve context)
content (parser/find-node node :g)
file-id (:id file)
old-id (parser/get-id node)
id (resolve old-id)
path (get-in node [:attrs :penpot:path] "")
main-instance-id (resolve (uuid/parse (get-in node [:attrs :penpot:main-instance-id] "")))
main-instance-page (resolve (uuid/parse (get-in node [:attrs :penpot:main-instance-page] "")))
main-instance-x (-> (get-in node [:attrs :penpot:main-instance-x] "") (d/parse-double))
main-instance-y (-> (get-in node [:attrs :penpot:main-instance-y] "") (d/parse-double))
main-instance-parent (resolve (uuid/parse (get-in node [:attrs :penpot:main-instance-parent] "")))
main-instance-frame (resolve (uuid/parse (get-in node [:attrs :penpot:main-instance-frame] "")))
type (parser/get-type content)
data (-> (parser/parse-data type content)
(assoc :path path)
(assoc :id id)
(assoc :main-instance-id main-instance-id)
(assoc :main-instance-page main-instance-page)
(assoc :main-instance-x main-instance-x)
(assoc :main-instance-y main-instance-y)
(assoc :main-instance-parent main-instance-parent)
(assoc :main-instance-frame main-instance-frame))
file (-> file
(fb/start-component data)
(fb/start-deleted-component data))
component-id (:current-component-id file)
children (parser/node-seq node)
;; Shapes inside the deleted component should be stored with absolute coordinates so we include this info in the context.
context (-> context
(assoc :x main-instance-x)
(assoc :y main-instance-y))]
(->> (rx/from children)
(rx/filter parser/shape?)
(rx/skip 1)
(rx/skip-last 1)
(rx/mapcat (partial resolve-media context file-id))
(rx/reduce (partial process-import-node context) file)
(rx/map fb/finish-component)
(rx/map (partial fb/finish-deleted-component component-id)))))
(defn process-pages
[context file]
(let [index (:pages-index context)
get-page-data
(fn [page-id]
[page-id (get-in index [page-id :name])])
pages (->> (:pages context) (mapv get-page-data))]
(->> (rx/from pages)
(rx/tap (fn [[_ page-name]]
(progress! context :process-page page-name)))
(rx/mapcat
(fn [[page-id page-name]]
(->> (get-file context :page page-id)
(rx/map (fn [page-data] [page-id page-name page-data])))))
(rx/concat-reduce (partial import-page context) file))))
(defn process-library-colors
[context file]
(if (:has-colors context)
(let [resolve (:resolve context)
add-color
(fn [file color]
(let [color (-> color
(d/update-in-when [:gradient :type] keyword)
(d/update-in-when [:image :id] resolve)
(update :id resolve))]
(fb/add-library-color file color)))]
(->> (get-file context :colors-list)
(rx/merge-map identity)
(rx/mapcat
(fn [[id color]]
(let [color (assoc color :id id)
color-image (:image color)
upload-image? (some? color-image)
color-image-id (:id color-image)]
(if upload-image?
(->> (get-file context :colors color-image-id color-image)
(rx/map (fn [blob]
(let [content (.slice blob 0 (.-size blob) (:mtype color-image))]
{:name (:name color-image)
:id (resolve color-image-id)
:file-id (:id file)
:content content
:is-local false})))
(rx/tap #(progress! context :upload-media (:name %)))
(rx/merge-map #(rp/cmd! :upload-file-media-object %))
(rx/map (constantly color))
(rx/catch #(do (.error js/console (str "Error uploading color-image: " (:name color-image)))
(rx/empty))))
(rx/of color)))))
(rx/reduce add-color file)))
(rx/of file)))
(defn process-library-typographies
[context file]
(if (:has-typographies context)
(let [resolve (:resolve context)]
(->> (get-file context :typographies)
(rx/merge-map identity)
(rx/map (fn [[id typography]]
(-> typography
(d/kebab-keys)
(assoc :id (resolve id)))))
(rx/reduce fb/add-library-typography file)))
(rx/of file)))
(defn process-library-media
[context file]
(if (:has-media context)
(let [resolve (:resolve context)]
(->> (get-file context :media-list)
(rx/merge-map identity)
(rx/mapcat
(fn [[id media]]
(let [media (-> media
(assoc :id (resolve id))
(update :name str))]
(->> (get-file context :media id media)
(rx/map (fn [blob]
(let [content (.slice blob 0 (.-size blob) (:mtype media))]
{:name (:name media)
:id (:id media)
:file-id (:id file)
:content content
:is-local false})))
(rx/tap #(progress! context :upload-media (:name %)))
(rx/merge-map #(rp/cmd! :upload-file-media-object %))
(rx/map (constantly media))
(rx/catch #(do (.error js/console (str "Error uploading media: " (:name media)))
(rx/empty)))))))
(rx/reduce fb/add-library-media file)))
(rx/of file)))
(defn process-library-components
[context file]
(if (:has-components context)
(let [split-components
(fn [content] (->> (parser/node-seq content)
(filter #(= :symbol (:tag %)))))]
(->> (get-file context :components)
(rx/merge-map split-components)
(rx/concat-reduce (partial import-component context) file)))
(rx/of file)))
(defn process-deleted-components
[context file]
(if (:has-deleted-components context)
(let [split-components
(fn [content] (->> (parser/node-seq content)
(filter #(= :symbol (:tag %)))))]
(->> (get-file context :deleted-components)
(rx/merge-map split-components)
(rx/concat-reduce (partial import-deleted-component context) file)))
(rx/of file)))
(defn process-file
[context file]
(let [progress-str (rx/subject)
context (assoc context :progress progress-str)]
[progress-str
(->> (rx/of file)
(rx/merge-map (partial process-pages context))
(rx/tap #(progress! context :process-colors))
(rx/merge-map (partial process-library-colors context))
(rx/tap #(progress! context :process-typographies))
(rx/merge-map (partial process-library-typographies context))
(rx/tap #(progress! context :process-media))
(rx/merge-map (partial process-library-media context))
(rx/tap #(progress! context :process-components))
(rx/merge-map (partial process-library-components context))
(rx/tap #(progress! context :process-deleted-components))
(rx/merge-map (partial process-deleted-components context))
(rx/merge-map (partial send-changes context))
(rx/tap #(rx/end! progress-str)))]))
(defn create-files
[{:keys [system-features] :as context} files]
(let [data (group-by :file-id files)]
(rx/concat
(->> (rx/from files)
(rx/map #(merge context %))
(rx/merge-map (fn [context]
(->> (create-file context system-features)
(rx/map #(vector % (first (get data (:file-id context)))))))))
(->> (rx/from files)
(rx/map #(merge context %))
(rx/merge-map link-file-libraries)
(rx/ignore)))))
(defn parse-mtype [ba]
(let [u8 (js/Uint8Array. ba 0 4)
sg (areduce u8 i ret "" (str ret (if (zero? i) "" " ") (.toString (aget u8 i) 8)))]
@@ -748,35 +97,6 @@
"1 13 32 206" "application/octet-stream"
"other")))
(defn- analyze-file-legacy-zip-entry
[features entry]
;; NOTE: LEGACY manifest reading mechanism, we can't
;; reuse the new read-zip-manifest funcion here
(->> (rx/from (uz/load (:body entry)))
(rx/merge-map #(get-file {:zip %} :manifest))
(rx/mapcat
(fn [manifest]
;; Checks if the file is exported with
;; components v2 and the current team
;; only supports components v1
(let [has-file-v2?
(->> (:files manifest)
(d/seek (fn [[_ file]] (contains? (set (:features file)) "components/v2"))))]
(if (and has-file-v2? (not (contains? features "components/v2")))
(rx/of (-> entry
(assoc :error "dashboard.import.analyze-error.components-v2")
(dissoc :body)))
(->> (rx/from (:files manifest))
(rx/map (fn [[file-id data]]
(-> entry
(dissoc :body)
(merge data)
(dissoc :shared)
(assoc :is-shared (:shared data))
(assoc :file-id file-id)
(assoc :status :success)))))))))))
;; NOTE: this is a limited subset schema for the manifest file of
;; binfile-v3 format; is used for partially parse it and read the
;; files referenced inside the exported file
@@ -794,7 +114,7 @@
(sm/decoder schema:manifest sm/json-transformer))
(defn analyze-file
[features {:keys [uri] :as file}]
[{:keys [uri] :as file}]
(let [stream (->> (slurp-uri uri :buffer)
(rx/merge-map
(fn [body]
@@ -819,10 +139,6 @@
(rx/share))]
(->> (rx/merge
(->> stream
(rx/filter (fn [entry] (= :legacy-zip (:type entry))))
(rx/merge-map (partial analyze-file-legacy-zip-entry features)))
(->> stream
(rx/filter (fn [entry] (= :binfile-v1 (:type entry))))
(rx/map (fn [entry]
@@ -855,55 +171,16 @@
(rx/of (assoc file :error error :status :error))))))))
(defmethod impl/handler :analyze-import
[{:keys [files features]}]
[{:keys [files]}]
(->> (rx/from files)
(rx/merge-map (partial analyze-file features))))
(rx/merge-map analyze-file)))
(defmethod impl/handler :import-files
[{:keys [project-id files features]}]
(let [context {:project-id project-id
:resolve (resolve-factory)
:system-features features}
legacy-zip (filter #(= :legacy-zip (:type %)) files)
binfile-v1 (filter #(= :binfile-v1 (:type %)) files)
[{:keys [project-id files]}]
(let [binfile-v1 (filter #(= :binfile-v1 (:type %)) files)
binfile-v3 (filter #(= :binfile-v3 (:type %)) files)]
(rx/merge
;; NOTE: LEGACY, will be removed so no new development should be
;; done for this part
(->> (create-files context legacy-zip)
(rx/merge-map
(fn [[file data]]
(->> (uz/load-from-url (:uri data))
(rx/map #(-> context (assoc :zip %) (merge data)))
(rx/merge-map
(fn [context]
;; process file retrieves a stream that will emit progress notifications
;; and other that will emit the files once imported
(let [[progress-stream file-stream] (process-file context file)]
(rx/merge progress-stream
(->> file-stream
(rx/map
(fn [file]
(if-let [errors (not-empty (:errors file))]
{:status :error
:error (first errors)
:file-id (:file-id data)}
{:status :finish
:file-id (:file-id data)}))))))))
(rx/catch (fn [cause]
(let [data (ex-data cause)]
(log/error :hint (ex-message cause)
:file-id (:file-id data))
(when-let [explain (:explain data)]
(js/console.log explain)))
(rx/of {:status :error
:file-id (:file-id data)
:error (ex-message cause)})))))))
(->> (rx/from binfile-v1)
(rx/merge-map
(fn [data]

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,250 @@
;; 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 lib.file-builder
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.builder :as fb]
[app.common.json :as json]
[app.common.schema :as sm]
[app.common.uuid :as uuid]
[app.util.object :as obj]))
(def BuilderError
(obj/class
:name "BuilderError"
:extends js/Error
:constructor
(fn [this type code hint cause]
(.call js/Error this hint)
(set! (.-name this) (str "Exception: " hint))
(set! (.-type this) type)
(set! (.-code this) code)
(set! (.-hint this) hint)
(when (exists? js/Error.captureStackTrace)
(.captureStackTrace js/Error this))
(obj/add-properties!
this
{:name "cause"
:enumerable true
:this false
:get (fn [] cause)}
{:name "data"
:enumerable true
:this false
:get (fn []
(let [data (ex-data cause)]
(when-let [explain (::sm/explain data)]
(json/->js (sm/simplify explain)))))}))))
(defn- handle-exception
[cause]
(let [data (ex-data cause)]
(throw (new BuilderError
(d/name (get data :type :unknown))
(d/name (get data :code :unknown))
(or (get data :hint) (ex-message cause))
cause))))
(defn- decode-params
[params]
(if (obj/plain-object? params)
(json/->js params)
params))
(defn- create-file*
[file]
(let [state* (volatile! file)]
(obj/reify {:name "File"}
:id
{:get #(dm/str (:id @state*))}
:currentFrameId
{:get #(dm/str (::fb/current-frame-id @state*))}
:currentPageId
{:get #(dm/str (::fb/current-page-id @state*))}
:lastId
{:get #(dm/str (::fb/last-id @state*))}
:addPage
(fn [params]
(try
(let [params (-> params
(decode-params)
(fb/decode-page))]
(vswap! state* fb/add-page params)
(dm/str (::fb/current-page-id @state*)))
(catch :default cause
(handle-exception cause))))
:closePage
(fn []
(vswap! state* fb/close-page))
:addArtboard
(fn [params]
(try
(let [params (-> params
(json/->clj)
(assoc :type :frame)
(fb/decode-shape))]
(vswap! state* fb/add-artboard params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:closeArtboard
(fn []
(vswap! state* fb/close-artboard))
:addGroup
(fn [params]
(try
(let [params (-> params
(json/->clj)
(assoc :type :group)
(fb/decode-shape))]
(vswap! state* fb/add-group params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:closeGroup
(fn []
(vswap! state* fb/close-group))
:addBool
(fn [params]
(try
(let [params (-> params
(json/->clj)
(fb/decode-add-bool))]
(vswap! state* fb/add-bool params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:addRect
(fn [params]
(try
(let [params (-> params
(json/->clj)
(assoc :type :rect)
(fb/decode-shape))]
(vswap! state* fb/add-shape params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:addCircle
(fn [params]
(try
(let [params (-> params
(json/->clj)
(assoc :type :circle)
(fb/decode-shape))]
(vswap! state* fb/add-shape params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:addPath
(fn [params]
(try
(let [params (-> params
(json/->clj)
(assoc :type :path)
(fb/decode-shape))]
(vswap! state* fb/add-shape params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:addText
(fn [params]
(try
(let [params (-> params
(json/->clj)
(assoc :type :text)
(fb/decode-shape))]
(vswap! state* fb/add-shape params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:addLibraryColor
(fn [params]
(try
(let [params (-> params
(json/->clj)
(fb/decode-library-color)
(d/without-nils))]
(vswap! state* fb/add-library-color params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:addLibraryTypography
(fn [params]
(try
(let [params (-> params
(json/->clj)
(fb/decode-library-typography)
(d/without-nils))]
(vswap! state* fb/add-library-typography params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:addComponent
(fn [params]
(try
(let [params (-> params
(json/->clj)
(fb/decode-component)
(d/without-nils))]
(vswap! state* fb/add-component params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:addComponentInstance
(fn [params]
(try
(let [params (-> params
(json/->clj)
(fb/decode-add-component-instance)
(d/without-nils))]
(vswap! state* fb/add-component-instance params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:getShape
(fn [shape-id]
(let [shape-id (uuid/parse shape-id)]
(some-> (fb/lookup-shape @state* shape-id)
(json/->js))))
:toMap
(fn []
(-> @state*
(d/without-qualified)
(json/->js))))))
(defn create-file
[params]
(try
(let [params (-> params json/->clj fb/decode-file)
file (fb/create-file params)]
(create-file* file))
(catch :default cause
(handle-exception cause))))

View File

@@ -0,0 +1,30 @@
import * as penpot from "../../../target/library/penpot.js";
console.log(penpot);
try {
const file = penpot.createFile({name: "Test"});
file.addPage({name: "Foo Page"})
const boardId = file.addArtboard({name: "Foo Board"})
const rectId = file.addRect({name: "Foo Rect", width:100, height: 200})
file.addLibraryColor({color: "#fabada", opacity: 0.5})
console.log("created board", boardId);
console.log("created rect", rectId);
const board = file.getShape(boardId);
console.log("=========== BOARD =============")
console.dir(board, {depth: 10});
const rect = file.getShape(rectId);
console.log("=========== RECT =============")
console.dir(rect, {depth: 10});
// console.dir(file.toMap(), {depth:10});
} catch (e) {
console.log(e);
// console.log(e.data);
}
process.exit(0);

View File

@@ -16,8 +16,7 @@
(t/deftest test-common-shape-properties
(let [;; ==== Setup
store (ths/setup-store
(cthf/sample-file :file1 :page-label :page1))
store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
^js context (api/create-context "TEST")

View File

@@ -13,13 +13,21 @@
[cljs.pprint :refer [pprint]]
[cljs.test :as t :include-macros true]))
(def uuid-counter 1)
(defn get-mocked-uuid
[]
(let [counter (atom 0)]
(fn []
(uuid/custom 123456789 (swap! counter inc)))))
(t/deftest test-create-index
(t/testing "Create empty data"
(let [data (sd/make-snap-data)]
(t/is (some? data))))
(t/testing "Add empty page (only root-frame)"
(let [page (-> (fb/create-file "Test")
(let [page (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"})
(fb/get-current-page))
@@ -28,10 +36,11 @@
(t/is (some? data))))
(t/testing "Create simple shape on root"
(let [file (-> (fb/create-file "Test")
(let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"})
(fb/create-rect
{:x 0
(fb/add-shape
{:type :rect
:x 0
:y 0
:width 100
:height 100}))
@@ -57,7 +66,7 @@
(t/is (= (first (nth result-x 2)) 100))))
(t/testing "Add page with single empty frame"
(let [file (-> (fb/create-file "Test")
(let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"})
(fb/add-artboard
{:x 0
@@ -66,10 +75,10 @@
:height 100})
(fb/close-artboard))
frame-id (:last-id file)
frame-id (::fb/last-id file)
page (fb/get-current-page file)
;; frame-id (:last-id file)
;; frame-id (::fb/last-id file)
data (-> (sd/make-snap-data)
(sd/add-page page))
@@ -81,47 +90,49 @@
(t/is (= (count result-frame-x) 3))))
(t/testing "Add page with some shapes inside frames"
(let [file (-> (fb/create-file "Test")
(fb/add-page {:name "Page 1"})
(fb/add-artboard
{:x 0
:y 0
:width 100
:height 100}))
frame-id (:last-id file)
(with-redefs [uuid/next (get-mocked-uuid)]
(let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"})
(fb/add-artboard
{:x 0
:y 0
:width 100
:height 100}))
file (-> file
(fb/create-rect
{:x 25
:y 25
:width 50
:height 50})
(fb/close-artboard))
frame-id (::fb/last-id file)
page (fb/get-current-page file)
file (-> file
(fb/add-shape
{:type :rect
:x 25
:y 25
:width 50
:height 50})
(fb/close-artboard))
;; frame-id (:last-id file)
data (-> (sd/make-snap-data)
(sd/add-page page))
page (fb/get-current-page file)
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
result-frame-x (sd/query data (:id page) frame-id :x [0 100])]
data (-> (sd/make-snap-data)
(sd/add-page page))
(t/is (some? data))
(t/is (= (count result-zero-x) 3))
(t/is (= (count result-frame-x) 5))))
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
result-frame-x (sd/query data (:id page) frame-id :x [0 100])]
(t/is (some? data))
(t/is (= (count result-zero-x) 3))
(t/is (= (count result-frame-x) 5)))))
(t/testing "Add a global guide"
(let [file (-> (fb/create-file "Test")
(let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"})
(fb/add-guide {:position 50 :axis :x})
(fb/add-artboard {:x 200 :y 200 :width 100 :height 100})
(fb/close-artboard))
frame-id (:last-id file)
frame-id (::fb/last-id file)
page (fb/get-current-page file)
;; frame-id (:last-id file)
;; frame-id (::fb/last-id file)
data (-> (sd/make-snap-data)
(sd/add-page page))
@@ -140,26 +151,26 @@
(t/is (= (count result-frame-y) 0))))
(t/testing "Add a frame guide"
(let [file (-> (fb/create-file "Test")
(let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"})
(fb/add-artboard {:x 200 :y 200 :width 100 :height 100})
(fb/close-artboard))
frame-id (:last-id file)
frame-id (::fb/last-id file)
file (-> file
(fb/add-guide {:position 50 :axis :x :frame-id frame-id}))
page (fb/get-current-page file)
;; frame-id (:last-id file)
data (-> (sd/make-snap-data)
(sd/add-page page))
data (-> (sd/make-snap-data)
(sd/add-page page))
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
result-zero-y (sd/query data (:id page) uuid/zero :y [0 100])
result-frame-x (sd/query data (:id page) frame-id :x [0 100])
result-frame-y (sd/query data (:id page) frame-id :y [0 100])]
(t/is (some? data))
;; We can snap in the root
(t/is (= (count result-zero-x) 0))
@@ -171,7 +182,7 @@
(t/deftest test-update-index
(t/testing "Create frame on root and then remove it."
(let [file (-> (fb/create-file "Test")
(let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"})
(fb/add-artboard
{:x 0
@@ -180,15 +191,15 @@
:height 100})
(fb/close-artboard))
shape-id (:last-id file)
shape-id (::fb/last-id file)
page (fb/get-current-page file)
;; frame-id (:last-id file)
;; frame-id (::fb/last-id file)
data (-> (sd/make-snap-data)
(sd/add-page page))
file (-> file
(fb/delete-object shape-id))
(fb/delete-shape shape-id))
new-page (fb/get-current-page file)
data (sd/update-page data page new-page)
@@ -201,22 +212,23 @@
(t/is (= (count result-y) 0))))
(t/testing "Create simple shape on root. Then remove it"
(let [file (-> (fb/create-file "Test")
(let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"})
(fb/create-rect
{:x 0
(fb/add-shape
{:type :rect
:x 0
:y 0
:width 100
:height 100}))
shape-id (:last-id file)
shape-id (::fb/last-id file)
page (fb/get-current-page file)
;; frame-id (:last-id file)
;; frame-id (::fb/last-id file)
data (-> (sd/make-snap-data)
(sd/add-page page))
file (fb/delete-object file shape-id)
file (fb/delete-shape file shape-id)
new-page (fb/get-current-page file)
data (sd/update-page data page new-page)
@@ -229,17 +241,17 @@
(t/is (= (count result-y) 0))))
(t/testing "Create shape inside frame, then remove it"
(let [file (-> (fb/create-file "Test")
(let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"})
(fb/add-artboard
{:x 0
:y 0
:width 100
:height 100}))
frame-id (:last-id file)
frame-id (::fb/last-id file)
file (fb/create-rect file {:x 25 :y 25 :width 50 :height 50})
shape-id (:last-id file)
file (fb/add-shape file {:type :rect :x 25 :y 25 :width 50 :height 50})
shape-id (::fb/last-id file)
file (fb/close-artboard file)
@@ -247,7 +259,7 @@
data (-> (sd/make-snap-data)
(sd/add-page page))
file (fb/delete-object file shape-id)
file (fb/delete-shape file shape-id)
new-page (fb/get-current-page file)
data (sd/update-page data page new-page)
@@ -260,16 +272,16 @@
(t/is (= (count result-frame-x) 3))))
(t/testing "Create global guide then remove it"
(let [file (-> (fb/create-file "Test")
(let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"})
(fb/add-guide {:position 50 :axis :x}))
guide-id (:last-id file)
guide-id (::fb/last-id file)
file (-> (fb/add-artboard file {:x 200 :y 200 :width 100 :height 100})
(fb/close-artboard))
frame-id (:last-id file)
frame-id (::fb/last-id file)
page (fb/get-current-page file)
data (-> (sd/make-snap-data) (sd/add-page page))
@@ -293,14 +305,14 @@
(t/is (= (count result-frame-y) 0))))
(t/testing "Create frame guide then remove it"
(let [file (-> (fb/create-file "Test")
(let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"})
(fb/add-artboard {:x 200 :y 200 :width 100 :height 100})
(fb/close-artboard))
frame-id (:last-id file)
frame-id (::fb/last-id file)
file (fb/add-guide file {:position 50 :axis :x :frame-id frame-id})
guide-id (:last-id file)
guide-id (::fb/last-id file)
page (fb/get-current-page file)
data (-> (sd/make-snap-data) (sd/add-page page))
@@ -324,7 +336,7 @@
(t/is (= (count result-frame-y) 0))))
(t/testing "Update frame coordinates"
(let [file (-> (fb/create-file "Test")
(let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"})
(fb/add-artboard
{:x 0
@@ -333,17 +345,18 @@
:height 100})
(fb/close-artboard))
frame-id (:last-id file)
frame-id (::fb/last-id file)
page (fb/get-current-page file)
data (-> (sd/make-snap-data) (sd/add-page page))
frame (fb/lookup-shape file frame-id)
new-frame (-> frame
(dissoc :selrect :points)
(assoc :x 200 :y 200)
(cts/setup-shape))
file (fb/update-shape file frame-id
(fn [shape]
(-> shape
(dissoc :selrect :points)
(assoc :x 200 :y 200)
(cts/setup-shape))))
file (fb/update-object file frame new-frame)
new-page (fb/get-current-page file)
data (sd/update-page data page new-page)
@@ -360,27 +373,30 @@
(t/is (= (count result-frame-x-2) 3))))
(t/testing "Update shape coordinates"
(let [file (-> (fb/create-file "Test")
(let [file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"})
(fb/create-rect
{:x 0
(fb/add-shape
{:type :rect
:x 0
:y 0
:width 100
:height 100}))
shape-id (:last-id file)
shape-id (::fb/last-id file)
page (fb/get-current-page file)
data (-> (sd/make-snap-data) (sd/add-page page))
data (-> (sd/make-snap-data)
(sd/add-page page))
shape (fb/lookup-shape file shape-id)
new-shape (-> shape
(dissoc :selrect :points)
(assoc :x 200 :y 200))
file (fb/update-shape file shape-id
(fn [shape]
(-> shape
(dissoc :selrect :points)
(assoc :x 200 :y 200)
(cts/setup-shape))))
file (fb/update-object file shape new-shape)
new-page (fb/get-current-page file)
data (sd/update-page data page new-page)
;; FIXME: update
data (sd/update-page data page new-page)
result-zero-x-1 (sd/query data (:id page) uuid/zero :x [0 100])
result-zero-x-2 (sd/query data (:id page) uuid/zero :x [200 300])]
@@ -391,17 +407,17 @@
(t/testing "Update global guide"
(let [guide {:position 50 :axis :x}
file (-> (fb/create-file "Test")
file (-> (fb/create-file {:name "Test"})
(fb/add-page {:name "Page 1"})
(fb/add-guide guide))
guide-id (:last-id file)
guide-id (::fb/last-id file)
guide (assoc guide :id guide-id)
file (-> (fb/add-artboard file {:x 500 :y 500 :width 100 :height 100})
(fb/close-artboard))
frame-id (:last-id file)
frame-id (::fb/last-id file)
page (fb/get-current-page file)
data (-> (sd/make-snap-data) (sd/add-page page))

View File

@@ -6832,8 +6832,8 @@ msgid "workspace.token.edit-token"
msgstr "Edit token"
#: src/app/main/ui/workspace/tokens/form.cljs:544
msgid "workspace.token.enter-token-description"
msgstr "Add a description (optional)"
msgid "workspace.token.token-description"
msgstr "Add a description"
#: src/app/main/ui/workspace/tokens/form.cljs:498
msgid "workspace.token.enter-token-name"
@@ -6869,6 +6869,10 @@ msgstr "Import Error:"
msgid "workspace.token.import-tooltip"
msgstr "Importing a JSON file will override all your current tokens, sets and themes"
#: src/app/main/ui/workspace/tokens/errors.cljs:29
msgid "workspace.token.empty-input"
msgstr "Token value cannot be empty"
#: src/app/main/ui/workspace/tokens/errors.cljs:29
msgid "workspace.token.invalid-color"
msgstr "Invalid color value: %s"

View File

@@ -6854,8 +6854,8 @@ msgid "workspace.token.edit-token"
msgstr "Editar token"
#: src/app/main/ui/workspace/tokens/form.cljs:544
msgid "workspace.token.enter-token-description"
msgstr "Añade una Descripción (opcional)"
msgid "workspace.token.token-description"
msgstr "Añade una descripción"
#: src/app/main/ui/workspace/tokens/form.cljs:498
msgid "workspace.token.enter-token-name"
@@ -6887,9 +6887,13 @@ msgstr "Error al importar:"
msgid "workspace.token.import-tooltip"
msgstr "Al importar un fichero JSON sobreescribirás todos tus tokens, sets y themes"
#: src/app/main/ui/workspace/tokens/errors.cljs:29
msgid "workspace.token.empty-input"
msgstr "El valor del token no puede estar vacío"
#: src/app/main/ui/workspace/tokens/errors.cljs:29
msgid "workspace.token.invalid-color"
msgstr "Valor de color no válido: %s"
msgstr "Valor de color inválido: %s"
#: src/app/main/ui/workspace/tokens/errors.cljs:13
msgid "workspace.token.invalid-json"

View File

File diff suppressed because it is too large Load Diff

View File

@@ -150,6 +150,13 @@ pub extern "C" fn set_view(zoom: f32, x: f32, y: f32) {
});
}
#[no_mangle]
pub extern "C" fn init_shapes_pool(capacity: usize) {
with_state!(state, {
state.init_shapes_pool(capacity);
});
}
#[no_mangle]
pub extern "C" fn use_shape(a: u32, b: u32, c: u32, d: u32) {
with_state!(state, {

View File

@@ -81,6 +81,8 @@ pub(crate) struct RenderState {
pub surfaces: Surfaces,
pub fonts: FontStore,
pub viewbox: Viewbox,
pub cached_viewbox: Viewbox,
pub cached_target_snapshot: Option<skia::Image>,
pub images: ImageStore,
pub background_color: skia::Color,
// Identifier of the current requestAnimationFrame call, if any.
@@ -96,6 +98,22 @@ pub(crate) struct RenderState {
pub pending_tiles: Vec<tiles::TileWithDistance>,
}
pub fn get_cache_size(viewbox: Viewbox) -> skia::ISize {
// First we retrieve the extended area of the viewport that we could render.
let (isx, isy, iex, iey) =
tiles::get_tiles_for_viewbox_with_interest(viewbox, VIEWPORT_INTEREST_AREA_THRESHOLD);
let dx = if isx.signum() != iex.signum() { 1 } else { 0 };
let dy = if isy.signum() != iey.signum() { 1 } else { 0 };
let tile_size = tiles::TILE_SIZE;
(
((iex - isx).abs() + dx) * tile_size as i32,
((iey - isy).abs() + dy) * tile_size as i32,
)
.into()
}
impl RenderState {
pub fn new(width: i32, height: i32) -> RenderState {
// This needs to be done once per WebGL context.
@@ -122,6 +140,8 @@ impl RenderState {
surfaces,
fonts,
viewbox: Viewbox::new(width as f32, height as f32),
cached_viewbox: Viewbox::new(0., 0.),
cached_target_snapshot: None,
images: ImageStore::new(),
background_color: skia::Color::TRANSPARENT,
render_request_id: None,
@@ -172,7 +192,6 @@ impl RenderState {
pub fn resize(&mut self, width: i32, height: i32) {
let dpr_width = (width as f32 * self.options.dpr()).floor() as i32;
let dpr_height = (height as f32 * self.options.dpr()).floor() as i32;
self.surfaces
.resize(&mut self.gpu_state, dpr_width, dpr_height);
self.viewbox.set_wh(width as f32, height as f32);
@@ -191,7 +210,8 @@ impl RenderState {
let x = self.current_tile.unwrap().0;
let y = self.current_tile.unwrap().1;
self.surfaces.cache_current_tile_texture((x, y));
let tile_rect = self.get_current_aligned_tile_bounds();
self.surfaces.cache_current_tile_texture((x, y), tile_rect);
self.surfaces
.draw_cached_tile_surface(self.current_tile.unwrap(), rect);
@@ -432,9 +452,37 @@ impl RenderState {
.update_render_context(self.render_area, self.viewbox);
}
fn render_from_cache(&mut self) {
if let Some(snapshot) = &self.cached_target_snapshot {
let canvas = self.surfaces.canvas(SurfaceId::Target);
canvas.save();
// Scale and translate the target according to the cached data
let navigate_zoom = self.viewbox.zoom / self.cached_viewbox.zoom;
canvas.scale((navigate_zoom, navigate_zoom));
let (start_tile_x, start_tile_y, _, _) = tiles::get_tiles_for_viewbox_with_interest(
self.cached_viewbox,
VIEWPORT_INTEREST_AREA_THRESHOLD,
);
let offset_x = self.viewbox.area.left * self.cached_viewbox.zoom;
let offset_y = self.viewbox.area.top * self.cached_viewbox.zoom;
canvas.translate((
(start_tile_x as f32 * tiles::TILE_SIZE) - offset_x,
(start_tile_y as f32 * tiles::TILE_SIZE) - offset_y,
));
canvas.clear(self.background_color);
canvas.draw_image(&snapshot.clone(), (0, 0), Some(&skia::Paint::default()));
canvas.restore();
}
}
pub fn start_render_loop(
&mut self,
tree: &mut HashMap<Uuid, Shape>,
tree: &mut HashMap<Uuid, &mut Shape>,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
timestamp: i32,
@@ -444,8 +492,13 @@ impl RenderState {
wapi::cancel_animation_frame!(frame_id);
}
}
performance::begin_measure!("render");
performance::begin_measure!("start_render_loop");
// If we have cached data let's do a fast render from it
self.render_from_cache();
let scale = self.get_scale();
self.reset_canvas();
self.surfaces.apply_mut(
@@ -465,6 +518,17 @@ impl RenderState {
self.viewbox,
VIEWPORT_INTEREST_AREA_THRESHOLD,
);
let viewbox_cache_size = get_cache_size(self.viewbox);
let cached_viewbox_cache_size = get_cache_size(self.cached_viewbox);
if viewbox_cache_size != cached_viewbox_cache_size {
self.surfaces.resize_cache(
&mut self.gpu_state,
viewbox_cache_size,
VIEWPORT_INTEREST_AREA_THRESHOLD,
);
}
// Then we get the real amount of tiles rendered for the current viewbox.
let (sx, sy, ex, ey) = tiles::get_tiles_for_viewbox(self.viewbox);
debug::render_debug_tiles_for_viewbox(self, isx, isy, iex, iey);
@@ -501,7 +565,7 @@ impl RenderState {
pub fn process_animation_frame(
&mut self,
tree: &mut HashMap<Uuid, Shape>,
tree: &mut HashMap<Uuid, &mut Shape>,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
timestamp: i32,
@@ -598,9 +662,32 @@ impl RenderState {
)
}
// Returns the bounds of the current tile relative to the viewbox,
// aligned to the nearest tile grid origin.
//
// Unlike `get_current_tile_bounds`, which calculates bounds using the exact
// scaled offset of the viewbox, this method snaps the origin to the nearest
// lower multiple of `TILE_SIZE`. This ensures the tile bounds are aligned
// with the global tile grid, which is useful for rendering tiles in a
/// consistent and predictable layout.
pub fn get_current_aligned_tile_bounds(&mut self) -> Rect {
let (tile_x, tile_y) = self.current_tile.unwrap();
let scale = self.get_scale();
let start_tile_x =
(self.viewbox.area.left * scale / tiles::TILE_SIZE).floor() * tiles::TILE_SIZE;
let start_tile_y =
(self.viewbox.area.top * scale / tiles::TILE_SIZE).floor() * tiles::TILE_SIZE;
Rect::from_xywh(
(tile_x as f32 * tiles::TILE_SIZE) - start_tile_x,
(tile_y as f32 * tiles::TILE_SIZE) - start_tile_y,
tiles::TILE_SIZE,
tiles::TILE_SIZE,
)
}
pub fn render_shape_tree(
&mut self,
tree: &mut HashMap<Uuid, Shape>,
tree: &mut HashMap<Uuid, &mut Shape>,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
timestamp: i32,
@@ -814,6 +901,11 @@ impl RenderState {
}
}
self.render_in_progress = false;
// Cache target surface in a texture
self.cached_viewbox = self.viewbox.clone();
self.cached_target_snapshot = Some(self.surfaces.snapshot(SurfaceId::Cache));
if self.options.is_debug_visible() {
debug::render(self);
}
@@ -855,7 +947,7 @@ impl RenderState {
pub fn rebuild_tiles_shallow(
&mut self,
tree: &mut HashMap<Uuid, Shape>,
tree: &mut HashMap<Uuid, &mut Shape>,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) {
@@ -864,7 +956,7 @@ impl RenderState {
self.surfaces.remove_cached_tiles();
let mut nodes = vec![Uuid::nil()];
while let Some(shape_id) = nodes.pop() {
if let Some(shape) = tree.get(&shape_id) {
if let Some(shape) = tree.get_mut(&shape_id) {
let mut shape = shape.clone();
if shape_id != Uuid::nil() {
if let Some(modifier) = modifiers.get(&shape_id) {
@@ -885,7 +977,7 @@ impl RenderState {
pub fn rebuild_tiles(
&mut self,
tree: &mut HashMap<Uuid, Shape>,
tree: &mut HashMap<Uuid, &mut Shape>,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) {
@@ -894,7 +986,7 @@ impl RenderState {
self.surfaces.remove_cached_tiles();
let mut nodes = vec![Uuid::nil()];
while let Some(shape_id) = nodes.pop() {
if let Some(shape) = tree.get(&shape_id) {
if let Some(shape) = tree.get_mut(&shape_id) {
let mut shape = shape.clone();
if shape_id != Uuid::nil() {
if let Some(modifier) = modifiers.get(&shape_id) {
@@ -914,11 +1006,11 @@ impl RenderState {
pub fn rebuild_modifier_tiles(
&mut self,
tree: &mut HashMap<Uuid, Shape>,
tree: &mut HashMap<Uuid, &mut Shape>,
modifiers: &HashMap<Uuid, Matrix>,
) {
for (uuid, matrix) in modifiers {
if let Some(shape) = tree.get(uuid) {
if let Some(shape) = tree.get_mut(uuid) {
let mut shape: Shape = shape.clone();
shape.apply_transform(matrix);
self.update_tile_for(&shape);

View File

@@ -30,6 +30,16 @@ fn render_debug_view(render_state: &mut RenderState) {
.draw_rect(rect, &paint);
}
pub fn render_debug_cache_surface(render_state: &mut RenderState) {
let canvas = render_state.surfaces.canvas(SurfaceId::Debug);
canvas.save();
canvas.scale((0.1, 0.1));
render_state
.surfaces
.draw_into(SurfaceId::Cache, SurfaceId::Debug, None);
render_state.surfaces.canvas(SurfaceId::Debug).restore();
}
pub fn render_wasm_label(render_state: &mut RenderState) {
let canvas = render_state.surfaces.canvas(SurfaceId::Debug);
let skia::ISize { width, height } = canvas.base_layer_size();
@@ -164,6 +174,7 @@ pub fn render(render_state: &mut RenderState) {
render_debug_view(render_state);
render_debug_viewbox_tiles(render_state);
render_debug_tiles(render_state);
render_debug_cache_surface(render_state);
render_state.surfaces.draw_into(
SurfaceId::Debug,
SurfaceId::Target,

View File

@@ -2,7 +2,7 @@ use crate::shapes::Shape;
use crate::view::Viewbox;
use skia_safe::{self as skia, IRect, Paint, RRect};
use super::{gpu_state::GpuState, tiles::Tile};
use super::{gpu_state::GpuState, tiles::Tile, tiles::TILE_SIZE};
use base64::{engine::general_purpose, Engine as _};
use std::collections::HashMap;
@@ -16,6 +16,7 @@ const TILE_SIZE_MULTIPLIER: i32 = 2;
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum SurfaceId {
Target,
Cache,
Current,
Fills,
Strokes,
@@ -27,6 +28,7 @@ pub enum SurfaceId {
pub struct Surfaces {
// is the final destination surface, the one that it is represented in the canvas element.
target: skia::Surface,
cache: skia::Surface,
// keeps the current render
current: skia::Surface,
// keeps the current shape's fills
@@ -60,7 +62,7 @@ impl Surfaces {
let margins = skia::ISize::new(extra_tile_dims.width / 4, extra_tile_dims.height / 4);
let target = gpu_state.create_target_surface(width, height);
let cache = gpu_state.create_surface_with_dimensions("cache".to_string(), width, height);
let current = gpu_state.create_surface_with_isize("current".to_string(), extra_tile_dims);
let drop_shadows =
gpu_state.create_surface_with_isize("drop_shadows".to_string(), extra_tile_dims);
@@ -75,6 +77,7 @@ impl Surfaces {
let tiles = TileTextureCache::new();
Surfaces {
target,
cache,
current,
drop_shadows,
inner_shadows,
@@ -91,6 +94,11 @@ impl Surfaces {
self.reset_from_target(gpu_state.create_target_surface(new_width, new_height));
}
pub fn snapshot(&mut self, id: SurfaceId) -> skia::Image {
let surface = self.get_mut(id);
surface.image_snapshot()
}
pub fn base64_snapshot(&mut self, id: SurfaceId) -> String {
let surface = self.get_mut(id);
let image = surface.image_snapshot();
@@ -160,6 +168,7 @@ impl Surfaces {
fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface {
match id {
SurfaceId::Target => &mut self.target,
SurfaceId::Cache => &mut self.cache,
SurfaceId::Current => &mut self.current,
SurfaceId::DropShadows => &mut self.drop_shadows,
SurfaceId::InnerShadows => &mut self.inner_shadows,
@@ -176,6 +185,20 @@ impl Surfaces {
// The rest are tile size surfaces
}
pub fn resize_cache(
&mut self,
gpu_state: &mut GpuState,
cache_dims: skia::ISize,
interest_area_threshold: i32,
) {
self.cache = gpu_state.create_surface_with_isize("cache".to_string(), cache_dims);
self.cache.canvas().reset_matrix();
self.cache.canvas().translate((
(interest_area_threshold as f32 * TILE_SIZE),
(interest_area_threshold as f32 * TILE_SIZE),
));
}
pub fn draw_rect_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) {
if let Some(corners) = shape.shape_type.corners() {
let rrect = RRect::new_rect_radii(shape.selrect, &corners);
@@ -227,18 +250,22 @@ impl Surfaces {
self.tiles.visit(tile);
}
pub fn cache_current_tile_texture(&mut self, tile: Tile) {
let snapshot = self.current.image_snapshot();
pub fn cache_current_tile_texture(&mut self, tile: Tile, tile_rect: skia::Rect) {
let rect = IRect::from_xywh(
self.margins.width,
self.margins.height,
snapshot.width() - TILE_SIZE_MULTIPLIER * self.margins.width,
snapshot.height() - TILE_SIZE_MULTIPLIER * self.margins.height,
self.current.width() - TILE_SIZE_MULTIPLIER * self.margins.width,
self.current.height() - TILE_SIZE_MULTIPLIER * self.margins.height,
);
let mut context = self.current.direct_context();
if let Some(snapshot) = snapshot.make_subset(&mut context, &rect) {
self.tiles.add(tile, snapshot);
if let Some(snapshot) = self.current.image_snapshot_with_bounds(&rect) {
self.tiles.add(tile, snapshot.clone());
self.cache.canvas().draw_image_rect(
&snapshot.clone(),
None,
tile_rect,
&skia::Paint::default(),
);
}
}

View File

@@ -6,17 +6,17 @@ mod grid_layout;
use common::GetBounds;
use crate::math::{identitish, Bounds, Matrix, Point};
use crate::math::{self as math, identitish, Bounds, Matrix, Point};
use crate::shapes::{
modified_children_ids, ConstraintH, ConstraintV, Frame, Group, Layout, Modifier, Shape,
StructureEntry, TransformEntry, Type,
auto_height, modified_children_ids, set_paragraphs_width, ConstraintH, ConstraintV, Frame,
Group, GrowType, Layout, Modifier, Shape, StructureEntry, TransformEntry, Type,
};
use crate::state::State;
use crate::uuid::Uuid;
fn propagate_children(
shape: &Shape,
shapes: &HashMap<Uuid, Shape>,
shapes: &HashMap<Uuid, &mut Shape>,
parent_bounds_before: &Bounds,
parent_bounds_after: &Bounds,
transform: Matrix,
@@ -83,7 +83,7 @@ fn propagate_children(
fn calculate_group_bounds(
shape: &Shape,
shapes: &HashMap<Uuid, Shape>,
shapes: &HashMap<Uuid, &mut Shape>,
bounds: &HashMap<Uuid, Bounds>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> Option<Bounds> {
@@ -106,6 +106,7 @@ fn calculate_group_bounds(
pub fn propagate_modifiers(state: &State, modifiers: Vec<TransformEntry>) -> Vec<TransformEntry> {
let shapes = &state.shapes;
let font_col = state.render_state.fonts.font_collection();
let mut entries: VecDeque<_> = modifiers
.iter()
.map(|entry| Modifier::Transform(entry.clone()))
@@ -137,7 +138,25 @@ pub fn propagate_modifiers(state: &State, modifiers: Vec<TransformEntry>) -> Vec
};
let shape_bounds_before = bounds.find(&shape);
let shape_bounds_after = shape_bounds_before.transform(&entry.transform);
let mut shape_bounds_after = shape_bounds_before.transform(&entry.transform);
let mut transform = entry.transform;
if let Type::Text(content) = &shape.shape_type {
if content.grow_type() == GrowType::AutoHeight {
let mut paragraphs = content.get_skia_paragraphs(font_col);
set_paragraphs_width(shape_bounds_after.width(), &mut paragraphs);
let height = auto_height(&paragraphs);
let resize_transform = math::resize_matrix(
&shape_bounds_after,
&shape_bounds_after,
shape_bounds_after.width(),
height,
);
shape_bounds_after = shape_bounds_after.transform(&resize_transform);
transform.post_concat(&resize_transform);
}
}
if entry.propagate {
let mut children = propagate_children(
@@ -145,11 +164,10 @@ pub fn propagate_modifiers(state: &State, modifiers: Vec<TransformEntry>) -> Vec
shapes,
&shape_bounds_before,
&shape_bounds_after,
entry.transform,
transform,
&bounds,
&state.structure,
);
entries.append(&mut children);
}
@@ -158,7 +176,7 @@ pub fn propagate_modifiers(state: &State, modifiers: Vec<TransformEntry>) -> Vec
let default_matrix = Matrix::default();
let mut shape_modif =
modifiers.get(&shape.id).unwrap_or(&default_matrix).clone();
shape_modif.post_concat(&entry.transform);
shape_modif.post_concat(&transform);
modifiers.insert(shape.id, shape_modif);
if let Some(parent) = shape.parent_id.and_then(|id| shapes.get(&id)) {
@@ -303,19 +321,20 @@ mod tests {
#[test]
fn test_propagate_shape() {
let mut shapes = HashMap::<Uuid, Shape>::new();
let mut shapes = HashMap::<Uuid, &mut Shape>::new();
let child_id = Uuid::new_v4();
let mut child = Shape::new(child_id);
child.set_selrect(3.0, 3.0, 2.0, 2.0);
shapes.insert(child_id, child);
shapes.insert(child_id, &mut child);
let parent_id = Uuid::new_v4();
let mut parent = Shape::new(parent_id);
parent.set_shape_type(Type::Group(Group::default()));
parent.add_child(child_id);
parent.set_selrect(1.0, 1.0, 5.0, 5.0);
shapes.insert(parent_id, parent.clone());
let mut parent_clone = parent.clone();
shapes.insert(parent_id, &mut parent_clone);
let mut transform = Matrix::scale((2.0, 1.5));
let x = parent.selrect.x();
@@ -341,17 +360,17 @@ mod tests {
#[test]
fn test_group_bounds() {
let mut shapes = HashMap::<Uuid, Shape>::new();
let mut shapes = HashMap::<Uuid, &mut Shape>::new();
let child1_id = Uuid::new_v4();
let mut child1 = Shape::new(child1_id);
child1.set_selrect(3.0, 3.0, 2.0, 2.0);
shapes.insert(child1_id, child1);
shapes.insert(child1_id, &mut child1);
let child2_id = Uuid::new_v4();
let mut child2 = Shape::new(child2_id);
child2.set_selrect(0.0, 0.0, 1.0, 1.0);
shapes.insert(child2_id, child2);
shapes.insert(child2_id, &mut child2);
let parent_id = Uuid::new_v4();
let mut parent = Shape::new(parent_id);
@@ -359,7 +378,8 @@ mod tests {
parent.add_child(child1_id);
parent.add_child(child2_id);
parent.set_selrect(0.0, 0.0, 3.0, 3.0);
shapes.insert(parent_id, parent.clone());
let mut parent_clone = parent.clone();
shapes.insert(parent_id, &mut parent_clone);
let bounds =
calculate_group_bounds(&parent, &shapes, &HashMap::new(), &HashMap::new()).unwrap();

View File

@@ -178,7 +178,7 @@ fn initialize_tracks(
layout_bounds: &Bounds,
layout_axis: &LayoutAxis,
flex_data: &FlexData,
shapes: &HashMap<Uuid, Shape>,
shapes: &HashMap<Uuid, &mut Shape>,
bounds: &HashMap<Uuid, Bounds>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> Vec<TrackData> {
@@ -420,7 +420,7 @@ fn calculate_track_data(
layout_data: &LayoutData,
flex_data: &FlexData,
layout_bounds: &Bounds,
shapes: &HashMap<Uuid, Shape>,
shapes: &HashMap<Uuid, &mut Shape>,
bounds: &HashMap<Uuid, Bounds>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> Vec<TrackData> {
@@ -551,7 +551,7 @@ pub fn reflow_flex_layout(
shape: &Shape,
layout_data: &LayoutData,
flex_data: &FlexData,
shapes: &HashMap<Uuid, Shape>,
shapes: &HashMap<Uuid, &mut Shape>,
bounds: &mut HashMap<Uuid, Bounds>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> VecDeque<Modifier> {

View File

@@ -40,7 +40,7 @@ fn calculate_tracks(
grid_data: &GridData,
layout_bounds: &Bounds,
cells: &Vec<GridCell>,
shapes: &HashMap<Uuid, Shape>,
shapes: &HashMap<Uuid, &mut Shape>,
bounds: &HashMap<Uuid, Bounds>,
) -> Vec<TrackData> {
let layout_size = if is_column {
@@ -105,7 +105,7 @@ fn set_auto_base_size(
column: bool,
tracks: &mut Vec<TrackData>,
cells: &Vec<GridCell>,
shapes: &HashMap<Uuid, Shape>,
shapes: &HashMap<Uuid, &mut Shape>,
bounds: &HashMap<Uuid, Bounds>,
) {
for cell in cells {
@@ -156,7 +156,7 @@ fn set_auto_multi_span(
column: bool,
tracks: &mut Vec<TrackData>,
cells: &Vec<GridCell>,
shapes: &HashMap<Uuid, Shape>,
shapes: &HashMap<Uuid, &mut Shape>,
bounds: &HashMap<Uuid, Bounds>,
) {
// Remove groups with flex (will be set in flex_multi_span)
@@ -230,7 +230,7 @@ fn set_flex_multi_span(
column: bool,
tracks: &mut Vec<TrackData>,
cells: &Vec<GridCell>,
shapes: &HashMap<Uuid, Shape>,
shapes: &HashMap<Uuid, &mut Shape>,
bounds: &HashMap<Uuid, Bounds>,
) {
// Remove groups without flex
@@ -509,7 +509,7 @@ fn cell_bounds(
fn create_cell_data<'a>(
layout_bounds: &Bounds,
children: &IndexSet<Uuid>,
shapes: &'a HashMap<Uuid, Shape>,
shapes: &'a HashMap<Uuid, &mut Shape>,
cells: &Vec<GridCell>,
column_tracks: &Vec<TrackData>,
row_tracks: &Vec<TrackData>,
@@ -618,7 +618,7 @@ pub fn reflow_grid_layout<'a>(
shape: &Shape,
layout_data: &LayoutData,
grid_data: &GridData,
shapes: &'a HashMap<Uuid, Shape>,
shapes: &'a HashMap<Uuid, &mut Shape>,
bounds: &mut HashMap<Uuid, Bounds>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> VecDeque<Modifier> {

View File

@@ -40,7 +40,11 @@ pub struct TextContent {
pub fn set_paragraphs_width(width: f32, paragraphs: &mut Vec<Vec<skia::textlayout::Paragraph>>) {
for group in paragraphs {
for paragraph in group {
paragraph.layout(width)
// We first set max so we can get the min_intrinsic_width (this is the min word size)
// then after we set either the real with or the min.
// This is done this way so the words are not break into lines.
paragraph.layout(f32::MAX);
paragraph.layout(f32::max(width, paragraph.min_intrinsic_width().ceil()));
}
}
}

View File

@@ -7,6 +7,54 @@ use crate::shapes::Shape;
use crate::shapes::StructureEntry;
use crate::uuid::Uuid;
/// A pool allocator for `Shape` objects that attempts to minimize memory reallocations.
///
/// `ShapesPool` pre-allocates a contiguous vector of boxed `Shape` instances,
/// which can be reused and indexed efficiently. This design helps avoid
/// memory reallocation overhead by reserving enough space in advance.
///
/// # Memory Layout
///
/// Shapes are stored in a `Vec<Box<Shape>>`, which keeps the `Box` pointers
/// in a contiguous memory block. The actual `Shape` instances are heap-allocated,
/// and this approach ensures that pushing new shapes does not invalidate
/// previously returned mutable references.
///
/// This is especially important because references to `Shape` are also held in the
/// state shapes attribute
pub(crate) struct ShapesPool {
// We need a box so that pushing here doesn't invalidate state.shapes references
shapes: Vec<Box<Shape>>,
counter: usize,
}
impl ShapesPool {
pub fn new() -> Self {
ShapesPool {
shapes: vec![],
counter: 0,
}
}
pub fn initialize(&mut self, capacity: usize) {
self.counter = 0;
self.shapes = Vec::with_capacity(capacity);
for _ in 0..capacity {
self.shapes.push(Box::new(Shape::new(Uuid::nil())));
}
}
pub fn add_shape(&mut self, id: Uuid) -> &mut Shape {
if self.counter >= self.shapes.len() {
self.shapes.push(Box::new(Shape::new(Uuid::nil())));
}
let new_shape = &mut self.shapes[self.counter];
new_shape.id = id;
self.counter += 1;
new_shape
}
}
/// This struct holds the state of the Rust application between JS calls.
///
/// It is created by [init] and passed to the other exported functions.
@@ -16,9 +64,10 @@ pub(crate) struct State<'a> {
pub render_state: RenderState,
pub current_id: Option<Uuid>,
pub current_shape: Option<&'a mut Shape>,
pub shapes: HashMap<Uuid, Shape>,
pub shapes: HashMap<Uuid, &'a mut Shape>,
pub modifiers: HashMap<Uuid, skia::Matrix>,
pub structure: HashMap<Uuid, Vec<StructureEntry>>,
pub shapes_pool: ShapesPool,
}
impl<'a> State<'a> {
@@ -30,6 +79,7 @@ impl<'a> State<'a> {
shapes: HashMap::with_capacity(capacity),
modifiers: HashMap::new(),
structure: HashMap::new(),
shapes_pool: ShapesPool::new(),
}
}
@@ -61,13 +111,17 @@ impl<'a> State<'a> {
Ok(())
}
pub fn init_shapes_pool(&mut self, capacity: usize) {
self.shapes_pool.initialize(capacity);
}
pub fn use_shape(&'a mut self, id: Uuid) {
if !self.shapes.contains_key(&id) {
let new_shape = Shape::new(id);
let new_shape = self.shapes_pool.add_shape(id);
self.shapes.insert(id, new_shape);
}
self.current_id = Some(id);
self.current_shape = self.shapes.get_mut(&id);
self.current_shape = self.shapes.get_mut(&id).map(|r| &mut **r);
}
pub fn delete_shape(&mut self, id: Uuid) {