Merge pull request #6606 from penpot/niwinz-develop-fixes-2

 Fix several issues on penpot library
This commit is contained in:
Alejandro Alonso 2025-06-02 07:04:22 +02:00 committed by GitHub
commit 08aeb93710
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 200 additions and 184 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -52,7 +52,7 @@ await penpot.exportStream(context, writable);
Build the library:
```bash
yarn run build
./scripts/build
```
Login on npm:

View file

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

View file

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

View file

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

12
library/scripts/build Executable file
View file

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

View file

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