From 89a09346a52f90d8ae7a4ba778c1d837d3597ea3 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Sun, 1 Jun 2025 11:06:00 +0200 Subject: [PATCH] :bug: Fix incorrect boolean shapes generation on builder --- common/src/app/common/files/builder.cljc | 90 +++++++++++++------ .../src/app/main/data/workspace/bool.cljs | 2 +- library/CHANGES.md | 6 ++ library/playground/sample-bool.js | 58 ++++++++++++ 4 files changed, 130 insertions(+), 26 deletions(-) create mode 100644 library/playground/sample-bool.js diff --git a/common/src/app/common/files/builder.cljc b/common/src/app/common/files/builder.cljc index c0d423a9d7..61ee4007b3 100644 --- a/common/src/app/common/files/builder.cljc +++ b/common/src/app/common/files/builder.cljc @@ -10,13 +10,17 @@ (: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.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] @@ -330,6 +334,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]} @@ -338,33 +371,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) - (assoc :bool-type type) - (types.path/update-bool-shape 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] diff --git a/frontend/src/app/main/data/workspace/bool.cljs b/frontend/src/app/main/data/workspace/bool.cljs index 5726d77d7c..9819813869 100644 --- a/frontend/src/app/main/data/workspace/bool.cljs +++ b/frontend/src/app/main/data/workspace/bool.cljs @@ -55,7 +55,7 @@ [shape (cph/get-position-on-parent objects (:id head))])) -(defn group->bool +(defn- group->bool [type group objects] (let [shapes (->> (:shapes group) (map (d/getf objects))) diff --git a/library/CHANGES.md b/library/CHANGES.md index 80f1b50ca9..b78f0dcb29 100644 --- a/library/CHANGES.md +++ b/library/CHANGES.md @@ -1,5 +1,11 @@ # 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 + ## 1.0.1 - Make the library generate a .penpot file compatible with penpot 2.7.x 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); + });