diff --git a/common/src/app/common/files/builder.cljc b/common/src/app/common/files/builder.cljc index 56ad3fa903..89e3d0fe49 100644 --- a/common/src/app/common/files/builder.cljc +++ b/common/src/app/common/files/builder.cljc @@ -10,15 +10,21 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - ;; [app.common.features :as cfeat] + [app.common.exceptions :as ex] [app.common.files.changes :as ch] + ;; [app.common.features :as cfeat] + [app.common.files.helpers :as cph] [app.common.files.migrations :as fmig] [app.common.geom.shapes :as gsh] [app.common.schema :as sm] [app.common.svg :as csvg] + [app.common.time :as dt] [app.common.types.color :as types.color] + [app.common.types.component :as types.comp] + [app.common.types.container :as types.cont] [app.common.types.file :as types.file] [app.common.types.page :as types.page] + [app.common.types.path :as types.path] [app.common.types.shape :as types.shape] [app.common.types.typography :as types.typography] [app.common.uuid :as uuid] @@ -293,7 +299,7 @@ (defn close-group [state] - (let [group-id (-> state :parent-stack peek) + (let [group-id (-> state ::parent-stack peek) group (get-shape state group-id) children (->> (get group :shapes) (into [] (keep (partial get-shape state))) @@ -329,6 +335,35 @@ (commit-change state change :add-container true)))] (update state ::parent-stack pop)))) +(defn- update-bool-style-properties + [bool-shape objects] + (let [xform + (comp + (map (d/getf objects)) + (remove cph/frame-shape?) + (remove types.comp/is-variant?) + (remove (partial types.cont/has-any-copy-parent? objects))) + + children + (->> (get bool-shape :shapes) + (into [] xform) + (not-empty))] + + (when-not children + (ex/raise :type :validation + :code :empty-children + :hint "expected a group with at least one shape for creating a bool")) + + (let [head (if (= type :difference) + (first children) + (last children)) + fills (if (and (contains? head :svg-attrs) (empty? (:fills head))) + types.path/default-bool-fills + (get head :fills))] + (-> bool-shape + (assoc :fills fills) + (assoc :stroks (get head :strokes)))))) + (defn add-bool [state params] (let [{:keys [group-id type]} @@ -337,32 +372,40 @@ group (get-shape state group-id) - children - (->> (get group :shapes) - (not-empty))] + objects + (get-current-objects state) - (assert (some? children) "expect group to have at least 1 element") + bool + (-> group + (assoc :type :bool) + (assoc :bool-type type) + (update-bool-style-properties objects) + (types.path/update-bool-shape objects)) - (let [objects (get-current-objects state) - bool (-> group - (assoc :type :bool) - (gsh/update-bool objects)) - change {:type :mod-obj - :id (:id bool) - :operations - [{:type :set :attr :content :val (:content bool) :ignore-touched true} - {:type :set :attr :type :val :bool :ignore-touched true} - {:type :set :attr :bool-type :val type :ignore-touched true} - {:type :set :attr :selrect :val (:selrect bool) :ignore-touched true} - {:type :set :attr :points :val (:points bool) :ignore-touched true} - {:type :set :attr :x :val (-> bool :selrect :x) :ignore-touched true} - {:type :set :attr :y :val (-> bool :selrect :y) :ignore-touched true} - {:type :set :attr :width :val (-> bool :selrect :width) :ignore-touched true} - {:type :set :attr :height :val (-> bool :selrect :height) :ignore-touched true}]}] + selrect + (get bool :selrect) - (-> state - (commit-change change :add-container true) - (assoc ::last-id group-id))))) + operations + [{:type :set :attr :content :val (:content bool) :ignore-touched true} + {:type :set :attr :type :val :bool :ignore-touched true} + {:type :set :attr :bool-type :val type :ignore-touched true} + {:type :set :attr :selrect :val selrect :ignore-touched true} + {:type :set :attr :points :val (:points bool) :ignore-touched true} + {:type :set :attr :x :val (get selrect :x) :ignore-touched true} + {:type :set :attr :y :val (get selrect :y) :ignore-touched true} + {:type :set :attr :width :val (get selrect :width) :ignore-touched true} + {:type :set :attr :height :val (get selrect :height) :ignore-touched true} + {:type :set :attr :fills :val (:fills bool) :ignore-touched true} + {:type :set :attr :strokes :val (:strokes bool) :ignore-touched true}] + + change + {:type :mod-obj + :id (:id bool) + :operations operations}] + + (-> state + (commit-change change :add-container true) + (assoc ::last-id group-id)))) (defn add-shape [state params] @@ -533,6 +576,7 @@ :size (get blob :size)}) (update ::file-media assoc id {:id id + :created-at (dt/now) :name name :width width :height height diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc index 64beb388da..db7a87460b 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -24,6 +24,7 @@ [app.common.types.grid :as ctg] [app.common.types.page :as ctp] [app.common.types.pages-list :as ctpl] + [app.common.types.path :as path] [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] [app.common.types.tokens-lib :as ctob] @@ -744,7 +745,7 @@ group (= :bool (:type group)) - (gsh/update-bool group objects) + (path/update-bool-shape group objects) (:masked-group group) (->> (map lookup children) diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index 440b79216a..55a65b20ca 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -18,6 +18,7 @@ [app.common.schema :as sm] [app.common.types.component :as ctk] [app.common.types.file :as ctf] + [app.common.types.path :as path] [app.common.types.shape.layout :as ctl] [app.common.types.tokens-lib :as ctob] [app.common.uuid :as uuid])) @@ -685,10 +686,10 @@ (empty? children) ;; a parent with no children will be deleted, nil ;; so it does not need resize - (= (:type parent) :bool) - (gsh/update-bool parent objects) + (cfh/bool-shape? parent) + (path/update-bool-shape parent objects) - (= (:type parent) :group) + (cfh/group-shape? parent) ;; FIXME: this functions should be ;; normalized in the same way as ;; update-bool in order to make all @@ -1174,4 +1175,4 @@ (update :undo-changes conj {:type :set-base-font-size :base-font-size previous-font-size}) - (apply-changes-local)))) \ No newline at end of file + (apply-changes-local)))) diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index c63973fc6a..dce125f366 100644 --- a/common/src/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -164,7 +164,6 @@ (dm/export gtr/calculate-geometry) (dm/export gtr/update-group-selrect) (dm/export gtr/update-mask-selrect) -(dm/export gtr/update-bool) (dm/export gtr/apply-transform) (dm/export gtr/transform-shape) (dm/export gtr/transform-selrect) diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc index 379a016c89..adff9643ff 100644 --- a/common/src/app/common/geom/shapes/transforms.cljc +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -456,13 +456,6 @@ (assoc :flip-x (-> mask :flip-x)) (assoc :flip-y (-> mask :flip-y))))) -(defn update-bool - "Calculates the selrect+points for the boolean shape" - [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] @@ -477,7 +470,7 @@ (update-mask-selrect shape children) (cfh/bool-shape? shape) - (update-bool shape objects) + (path/update-bool-shape shape objects) (cfh/group-shape? shape) (update-group-selrect shape children) diff --git a/common/src/app/common/types/path.cljc b/common/src/app/common/types/path.cljc index 27e0136cff..65336fe2d5 100644 --- a/common/src/app/common/types/path.cljc +++ b/common/src/app/common/types/path.cljc @@ -22,6 +22,14 @@ #?(:clj (set! *warn-on-reflection* true)) +(def ^:cosnt bool-group-style-properties bool/group-style-properties) +(def ^:const bool-style-properties bool/style-properties) +(def ^:const default-bool-fills bool/default-fills) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TRANSFORMATIONS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (defn content? [o] (impl/path-data? o)) @@ -197,6 +205,13 @@ (-> (calc-bool-content* shape objects) (impl/path-data))) +(defn update-bool-shape + "Calculates the selrect+points for the boolean shape" + [shape objects] + (let [content (calc-bool-content shape objects) + shape (assoc shape :content content)] + (update-geometry shape))) + (defn shape-with-open-path? [shape] (let [svg? (contains? shape :svg-attrs) diff --git a/common/src/app/common/types/path/bool.cljc b/common/src/app/common/types/path/bool.cljc index 3afb3242fc..9be2c5a792 100644 --- a/common/src/app/common/types/path/bool.cljc +++ b/common/src/app/common/types/path/bool.cljc @@ -18,28 +18,13 @@ (def default-fills [{:fill-color clr/black}]) -(def style-group-properties - [:shadow :blur]) +(def group-style-properties + #{:shadow :blur}) +;; FIXME: revisit (def style-properties - (into style-group-properties - [:fill-color - :fill-opacity - :fill-color-gradient - :fill-color-ref-file - :fill-color-ref-id - :fill-image - :fills - :stroke-color - :stroke-color-ref-file - :stroke-color-ref-id - :stroke-opacity - :stroke-style - :stroke-width - :stroke-alignment - :stroke-cap-start - :stroke-cap-end - :strokes])) + (into group-style-properties + [:fills :strokes])) (defn add-previous ([content] diff --git a/frontend/src/app/main/data/workspace/bool.cljs b/frontend/src/app/main/data/workspace/bool.cljs index e2b85feb19..9819813869 100644 --- a/frontend/src/app/main/data/workspace/bool.cljs +++ b/frontend/src/app/main/data/workspace/bool.cljs @@ -12,6 +12,7 @@ [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] @@ -35,7 +36,7 @@ head (cond-> head (and (contains? head :svg-attrs) (empty? (:fills head))) - (assoc :fills bool/default-fills)) + (assoc :fills path/default-bool-fills)) shape {:id shape-id @@ -48,12 +49,26 @@ shape (-> shape - (merge (select-keys head bool/style-properties)) + (merge (select-keys head path/bool-style-properties)) (cts/setup-shape) - (gsh/update-bool objects))] + (path/update-bool-shape objects))] [shape (cph/get-position-on-parent objects (:id head))])) +(defn- group->bool + [type group objects] + (let [shapes (->> (:shapes group) + (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 path/default-bool-fills))] + (-> group + (assoc :type :bool) + (assoc :bool-type type) + (merge (select-keys head bool/style-properties)) + (path/update-bool-shape objects)))) + (defn create-bool [type & {:keys [ids force-shape-id]}] @@ -101,20 +116,6 @@ (rx/of (dch/commit-changes changes) (dws/select-shapes (d/ordered-set shape-id))))))))) -(defn group->bool - [type group objects] - (let [shapes (->> (:shapes group) - (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))] - (-> group - (assoc :type :bool) - (assoc :bool-type type) - (merge (select-keys head bool/style-properties)) - (gsh/update-bool objects)))) - (defn group-to-bool [shape-id type] (ptk/reify ::group-to-bool @@ -130,7 +131,7 @@ (-> shape (assoc :type :group) (dissoc :bool-type) - (d/without-keys bool/style-group-properties) + (d/without-keys path/bool-group-style-properties) (gsh/update-group-selrect (mapv (d/getf objects) (:shapes shape))))) diff --git a/library/CHANGES.md b/library/CHANGES.md index 80f1b50ca9..6601125e39 100644 --- a/library/CHANGES.md +++ b/library/CHANGES.md @@ -1,5 +1,12 @@ # CHANGELOG +## 1.0.2 + +- Fix incorrect boolean type assignation +- Fix fill and stroke handling on boolean shape creation +- Add sample-bool.js to the playground directory +- Fix compatibility issue on file media with penpot 2.7.x + ## 1.0.1 - Make the library generate a .penpot file compatible with penpot 2.7.x diff --git a/library/README.md b/library/README.md index 584aa09baf..2243f8c2fd 100644 --- a/library/README.md +++ b/library/README.md @@ -52,7 +52,7 @@ await penpot.exportStream(context, writable); Build the library: ```bash -yarn run build +./scripts/build ``` Login on npm: diff --git a/library/package.json b/library/package.json index c3ac96ac45..6f86b35ffb 100644 --- a/library/package.json +++ b/library/package.json @@ -1,6 +1,6 @@ { "name": "@penpot/library", - "version": "1.0.1", + "version": "1.0.2", "license": "MPL-2.0", "author": "Kaleidos INC", "packageManager": "yarn@4.9.1+sha512.f95ce356460e05be48d66401c1ae64ef84d163dd689964962c6888a9810865e39097a5e9de748876c2e0bf89b232d583c33982773e9903ae7a76257270986538", diff --git a/library/playground/components.js b/library/playground/components.js deleted file mode 100644 index 8b400b761d..0000000000 --- a/library/playground/components.js +++ /dev/null @@ -1,103 +0,0 @@ -import * as penpot from "#self"; -import { createWriteStream } from 'fs'; -import { Writable } from "stream"; - -// Example of creating component and instance out of order - -(async function() { - const context = penpot.createBuildContext(); - - { - context.addFile({name: "Test File 1"}); - context.addPage({name: "Foo Page"}) - - const mainBoardId = context.genId(); - const mainRectId = context.genId(); - - // First create instance (just for with the purpose of teaching - // that it can be done, without putting that under obligation to - // do it in this order or the opposite) - - context.addBoard({ - name: "Board Instance 1", - x: 700, - y: 0, - width: 500, - height: 300, - shapeRef: mainBoardId, - touched: ["name-group"] - }) - - context.addRect({ - name: "Rect Instance 1", - x: 800, - y: 20, - width:100, - height:200, - shapeRef: mainRectId, - touched: ["name-group"] - }); - - // this function call takes the current board from context, but it - // also can be passed as parameter on an explicit way if you - // prefer - context.addComponentInstance({ - componentId: "00000000-0000-0000-0000-000000000001" - }); - - context.closeBoard(); - - // Then, create the main instance - context.addBoard({ - id: mainBoardId, - name: "Board", - x: 0, - y: 0, - width: 500, - height: 300, - }) - - context.addRect({ - id: mainRectId, - name: "Rect 1", - x: 20, - y: 20, - width:100, - height:200, - }); - - context.addComponent({ - componentId: "00000000-0000-0000-0000-000000000001", - name: "Component 1", - }); - - context.closeBoard(); - context.closeFile(); - } - - { - // Create a file stream to write the zip to - const output = createWriteStream('sample-with-components.zip'); - // Wrap Node's stream in a WHATWG WritableStream - const writable = Writable.toWeb(output); - await penpot.exportStream(context, writable); - } - -})().catch((cause) => { - console.error(cause); - - const causeExplain = cause.explain; - if (causeExplain) { - console.log("EXPLAIN:") - console.error(cause.explain); - } - - // const innerCause = cause.cause; - // if (innerCause) { - // console.log("INNER:"); - // console.error(innerCause); - // } - process.exit(-1); -}).finally(() => { - process.exit(0); -}) diff --git a/library/playground/sample-bool.js b/library/playground/sample-bool.js new file mode 100644 index 0000000000..5b2bd7ff7e --- /dev/null +++ b/library/playground/sample-bool.js @@ -0,0 +1,58 @@ +import * as penpot from "#self"; +import { writeFile, readFile } from "fs/promises"; + +(async function () { + const context = penpot.createBuildContext(); + + { + context.addFile({ name: "Test File 1" }); + context.addPage({ name: "Foo Page" }); + + const groupId = context.addGroup({ + name: "Bool Group" + }) + + context.addRect({ + name: "Rect 1", + x: 20, + y: 20, + width:100, + height:100, + }); + + context.addRect({ + name: "Rect 2", + x: 90, + y: 90, + width:100, + height:100, + fills: [{fillColor: "#fabada", fillOpacity:1}] + }); + + context.closeGroup(); + context.addBool({ + groupId: groupId, + type: "union" + }); + + context.closeBoard(); + context.closeFile(); + } + + { + let result = await penpot.exportAsBytes(context); + await writeFile("sample-bool.zip", result); + } +})() + .catch((cause) => { + console.error(cause); + + const innerCause = cause.cause; + if (innerCause) { + console.error("Inner cause:", innerCause); + } + process.exit(-1); + }) + .finally(() => { + process.exit(0); + }); diff --git a/library/playground/sample2.js b/library/playground/sample-fill-stroke-and-media.js similarity index 100% rename from library/playground/sample2.js rename to library/playground/sample-fill-stroke-and-media.js diff --git a/library/playground/sample1.js b/library/playground/sample-fill-with-media.js similarity index 100% rename from library/playground/sample1.js rename to library/playground/sample-fill-with-media.js diff --git a/library/playground/sample3.js b/library/playground/sample-path.js similarity index 100% rename from library/playground/sample3.js rename to library/playground/sample-path.js diff --git a/library/scripts/build b/library/scripts/build new file mode 100755 index 0000000000..3e5e68ed5a --- /dev/null +++ b/library/scripts/build @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# NOTE: this script should be called from the parent directory to +# properly work. + +export CURRENT_VERSION=$(node -p "require('./package.json').version"); +export NODE_ENV=production; + +set -ex + +yarn run build + +sed -i -re "s/\%version\%/$CURRENT_VERSION/g" target/library/penpot.js diff --git a/library/src/lib/export.cljs b/library/src/lib/export.cljs index 5d9ee3efa0..1b81e3aa9f 100644 --- a/library/src/lib/export.cljs +++ b/library/src/lib/export.cljs @@ -41,6 +41,9 @@ (def ^:private encode-component (sm/encoder types.component/schema:component sm/json-transformer)) +(def encode-file-media + (sm/encoder types.file/schema:media sm/json-transformer)) + (def encode-color (sm/encoder types.color/schema:color sm/json-transformer)) @@ -175,6 +178,7 @@ [(str "files/" (:file-id file-media) "/media/" file-media-id ".json") (delay (-> file-media (dissoc :file-id) + (encode-file-media) (json/encode)))])))))) (defn- generate-manifest-procs @@ -186,8 +190,7 @@ :features (:features file)}))) params {:type "penpot/export-files" :version 1 - ;; FIXME: set proper placeholder for replacement on build - :generated-by "penpot-lib/develop" + :generated-by "penpot-library/%version%" :files files :relations []}] ["manifest.json" (delay (json/encode params))]))