* 🐛 Fix crash in apply-text-modifier with nil selrect or modifier Guard apply-text-modifier against nil text-modifier and nil selrect to prevent the 'invalid arguments (on pointer constructor)' error thrown by gpt/point when called with an invalid map. - In text-wrapper: only call apply-text-modifier when text-modifier is not nil (avoids unnecessary processing) - In apply-text-modifier: handle nil text-modifier by returning shape unchanged; guard selrect access before calling gpt/point * 📚 Add tests for apply-text-modifier in workspace texts Add exhaustive unit tests covering all paths of apply-text-modifier: - nil modifier returns shape unchanged (identity) - modifier with no recognised keys leaves shape unchanged - :width / :height modifiers resize shape correctly - nil :width / :height keys are skipped - both dimensions applied simultaneously - :position-data is set and nil-guarded - position-data coordinates translated by delta on resize - shape with nil selrect + nil modifier does not throw - position-data-only modifier on shape without selrect is safe - selrect origin preserved when no dimension changes - result always carries required shape keys * 🐛 Fix zero-dimension selrect crash in change-dimensions-modifiers When a text shape is decoded from the server via map->Rect (which bypasses make-rect's 0.01 minimum enforcement), its selrect can have width or height of exactly 0. change-dimensions-modifiers and change-size were dividing by these values, producing Infinity scale factors that propagated through the transform pipeline until calculate-selrect / center->rect returned nil, causing gpt/point to throw 'invalid arguments (on pointer constructor)'. Fix: before computing scale factors, guard sr-width / sr-height (and old-width / old-height in change-size) against zero/negative and non-finite values. When degenerate, fall back to the shape's own top-level :width/:height so the denominator and proportion-lock base remain consistent. Also simplify apply-text-modifier's delta calculation now that the transform pipeline is guaranteed to produce a valid selrect, and update the test suite to test the exact degenerate-selrect scenario that triggered the original crash. Signed-off-by: Andrey Antukh <niwi@niwi.nz> * ♻️ Simplify change-dimensions-modifiers internal logic - Remove the intermediate 'size' map ({:width sr-width :height sr-height}) that was built only to be assoc'd and immediately destructured back into width/height; compute both values directly instead. - Replace the double-negated condition 'if-not (and (not ignore-lock?) …)' with a clear positive 'locked?' binding, and flatten the three-branch if-not/if tree into two independent if expressions keyed on 'attr'. - Call safe-size-rect once and reuse its result for both the fallback sizes and the scale computation, eliminating a redundant call. - Access :transform and :transform-inverse via direct map lookup rather than destructuring in the function signature, consistent with how the rest of the let-block reads shape keys. - Clean up change-size to use the same destructuring style as the updated function ({sr-width :width sr-height :height}). - Fix typo in comment: 'havig' -> 'having'. * ✨ Add tests for change-size and change-dimensions-modifiers Cover the main behavioural contract of both functions: change-size: - Scales both axes to the requested target dimensions. - Sets the resize origin to the shape's top-left point. - Nil width/height each fall back to the current dimension (scale 1 on that axis); both nil produces an identity resize that is optimised away. - Propagates the shape's transform and transform-inverse matrices into the resulting GeometricOperation. change-dimensions-modifiers: - Changing :width without proportion-lock only scales the x-axis (y scale stays 1), and vice-versa for :height. - With proportion-lock enabled, changing :width adjusts height via the inverse proportion, and changing :height adjusts width via the proportion. - ignore-lock? true bypasses proportion-lock regardless of shape state. - Values below 0.01 are clamped to 0.01 before computing the scale. - End-to-end: applying the returned modifiers via gsh/transform-shape yields the expected selrect dimensions. * ✨ Harden safe-size-rect with additional fallbacks The previous implementation could still return an invalid rect in several edge cases. The new version tries four sources in order, accepting each only if it passes a dedicated safe-size-rect? predicate: 1. :selrect – used when width and height are finite, positive and within [-max-safe-int, max-safe-int]. 2. points->rect – computed from the shape corner points; subject to the same predicate. 3. Top-level shape fields (:x :y :width :height) – present on all rect, frame, image, and component shape types. 4. grc/empty-rect – a 0,0 0.01×0.01 unit rect used as last resort so callers always receive a usable, non-crashing value. The out-of-range check (> max-safe-int) is new: it rejects coordinates that pass d/num? (finite) but exceed the platform integer boundary defined in app.common.schema, which previously slipped through undetected. Tests cover all four fallback paths, including the NaN, zero-dimension, and max-safe-int overflow cases. * ⚡ Optimise safe-size-rect for ClojureScript performance - Replace (when (some? rect) ...) with (and ^boolean (some? rect) ...) to keep the entire predicate as a single boolean expression without introducing an implicit conditional branch. - Replace keyword access (:width rect) / (:height rect) with dm/get-prop calls, consistent with the hot-path style used throughout the rest of the namespace. - Add ^boolean type hints to every sub-expression of the and chain in safe-size-rect? (d/num?, pos?, <=) so the ClojureScript compiler emits raw JS boolean operations instead of boxing the results through cljs.core/truth_. - Replace (when (safe-size-rect? ...) value) in safe-size-rect with (and ^boolean (safe-size-rect? ...) value), avoiding an extra conditional and keeping the or fallback chain free of allocated intermediate objects. * ✨ Use safe-size-rect in apply-text-modifier delta-move computation safe-size-rect was already used inside change-dimensions-modifiers to guard the resize scale computation. However, apply-text-modifier in texts.cljs was still reading (:selrect shape) and (:selrect new-shape) directly to build the delta-move vector via gpt/point. gpt/point raises "invalid arguments (on pointer constructor)" when given a nil value or a map with non-finite :x/:y, which can happen when a shape's selrect is missing or degenerate (e.g. decoded from the server via map->Rect, bypassing make-rect's 0.01 floor). Changes: - Promote safe-size-rect from defn- to defn in app.common.types.modifiers so it can be reused by consumers outside the namespace. - Replace the two raw (:selrect …) accesses in the delta-move computation with (ctm/safe-size-rect …), which always returns a valid, finite rect through the established four-step fallback chain. - Add two frontend tests covering the delta-move path with a fully degenerate (zero-dimension) selrect, ensuring neither a bare position-data modifier nor a combined width+position-data modifier throws. * ♻️ Ensure all test shapes are proper Shape records in modifiers-test All shapes in safe-size-rect-fallbacks tests now start from a proper Shape record built by cts/setup-shape (via make-shape) instead of plain hash-maps. Each test that mutates geometry fields (selrect, points, width, height) does so via assoc on the already-initialised record, which preserves the correct type while isolating the field under test. A (cts/shape? shape) assertion is added to each fallback test to make the type guarantee explicit and guard against regressions. The unused shape-with-selrect helper (which built a bare map) is removed. * 🔥 Remove dead code and tighten visibility in app.common.types.modifiers Dead functions removed (zero callers across the entire codebase): - modifiers->transform-old: superseded by modifiers->transform; only ever appeared in a commented-out dev/bench.cljs entry. - change-recursive-property: no callers anywhere. - move-parent-modifiers, resize-parent-modifiers: convenience wrappers for the parent-geometry builder functions; never called. - remove-children-modifiers, add-children-modifiers, scale-content-modifiers: single-op convenience builders; never called. - select-structure: projection helper; only referenced by select-child-geometry-modifiers which is itself dead. - select-child-geometry-modifiers: no callers anywhere. Functions narrowed from defn to defn- (used only within this namespace): - valid-vector?: assertion helper called only by move/resize builders. - increase-order: called only by add-modifiers. - transform-move!, transform-resize!, transform-rotate!, transform!: steps of the modifiers->transform pipeline. - modifiers->transform1: immediate helper for modifiers->transform; the doc-string describing it as 'multiplatform' was also removed since it is an implementation detail. - transform-text-node, transform-paragraph-node: leaf helpers for scale-text-content. - update-text-content, scale-text-content, apply-scale-content: internal scale-content pipeline; all called only by apply-modifier. - remove-children-set: called only by apply-modifier. - select-structure: demoted to defn- rather than deleted because it is still called by select-child-structre-modifiers, which has external callers. * ✨ Add more tests for modifiers --------- Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Website • User Guide • Learning Center • Community
Youtube • Peertube • Linkedin • Instagram • Mastodon • Bluesky • X
Penpot is the first open-source design tool for design and code collaboration. Designers can create stunning designs, interactive prototypes, design systems at scale, while developers enjoy ready-to-use code and make their workflow easy and fast. And all of this with no handoff drama.
Available on browser or self-hosted, Penpot works with open standards like SVG, CSS, HTML and JSON, and it’s free!
The latest updates take Penpot even further. It’s the first design tool to integrate native design tokens—a single source of truth to improve efficiency and collaboration between product design and development. With the huge 2.0 release, Penpot took the platform to a whole new level. This update introduces the ground-breaking CSS Grid Layout feature, a complete UI redesign, a new Components system, and much more. For organizations that need extra service for its teams, get in touch
🎇 Design, code, and Open Source meet at Penpot Fest! Be part of the 2025 edition in Madrid, Spain, on October 9-10.
Table of contents
Why Penpot
Penpot expresses designs as code. Designers can do their best work and see it will be beautifully implemented by developers in a two-way collaboration.
Plugin system
Penpot plugins let you expand the platform's capabilities, give you the flexibility to integrate it with other apps, and design custom solutions.
Designed for developers
Penpot was built to serve both designers and developers and create a fluid design-code process. You have the choice to enjoy real-time collaboration or play "solo".
Inspect mode
Work with ready-to-use code and make your workflow easy and fast. The inspect tab gives instant access to SVG, CSS and HTML code.
Self host your own instance
Provide your team or organization with a completely owned collaborative design tool. Use Penpot's cloud service or deploy your own Penpot server.
Integrations
Penpot offers integration into the development toolchain, thanks to its support for webhooks and an API accessible through access tokens.
Building Design Systems: design tokens, components and variants
Penpot brings design systems to code-minded teams: a single source of truth with native Design Tokens, Components, and Variants for scalable, reusable, and consistent UI across projects and platforms.
Getting started
Penpot is the only design & prototype platform that is deployment agnostic. You can use it in our SAAS or deploy it anywhere.
Learn how to install it with Docker, Kubernetes, Elestio or other options on our website.
Community
We love the Open Source software community. Contributing is our passion and if it’s yours too, participate and improve Penpot. All your designs, code and ideas are welcome!
If you need help or have any questions; if you’d like to share your experience using Penpot or get inspired; if you’d rather meet our community of developers and designers, join our Community!
You will find the following categories:
- Ask the Community
- Troubleshooting
- Help us Improve Penpot
- #MadeWithPenpot
- Events and Announcements
- Inside Penpot
- Penpot in your language
- Design and Code Essentials
Code of Conduct
Anyone who contributes to Penpot, whether through code, in the community, or at an event, must adhere to the code of conduct and foster a positive and safe environment.
Contributing
Any contribution will make a difference to improve Penpot. How can you get involved?
Choose your way:
- Create and share Libraries & Templates that will be helpful for the community
- Invite your team to join
- Give this repo a star and follow us on Social Media: Mastodon, Youtube, Instagram, Linkedin, Peertube, X and BlueSky
- Participate in the Community space by asking and answering questions; reacting to others’ articles; opening your own conversations and following along on decisions affecting the project.
- Report bugs with our easy guide for bugs hunting or GitHub issues
- Become a translator
- Give feedback: Email us
- Contribute to Penpot's code: Watch this video by Alejandro Alonso, CIO and developer at Penpot, where he gives us a hands-on demo of how to use Penpot’s repository and make changes in both front and back end
To find (almost) everything you need to know on how to contribute to Penpot, refer to the contributing guide.
Resources
You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project.
✏️ Tutorials
🏘️ Architecture
License
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
Penpot is a Kaleidos’ open source project