mirror of
https://github.com/penpot/penpot.git
synced 2026-03-31 05:31:07 -04:00
Merge remote-tracking branch 'origin/main' into staging
This commit is contained in:
@@ -31,3 +31,7 @@ Requirements:
|
||||
exact commands).
|
||||
* Make small and logical commits following the commit guideline described in
|
||||
`CONTRIBUTING.md`. Commit only when explicitly asked.
|
||||
- Do not guess or hallucinate git author information (Name or Email). Never include the
|
||||
`--author` flag in git commands unless specifically instructed by the user for a unique
|
||||
case; assume the local environment is already configured. Allow git commit to
|
||||
automatically pull the identity from the local git config `user.name` and `user.email`.
|
||||
|
||||
@@ -31,3 +31,7 @@ Requirements:
|
||||
commands).
|
||||
* Make small and logical commits following the commit guideline described in
|
||||
`CONTRIBUTING.md`. Commit only when explicitly asked.
|
||||
- Do not guess or hallucinate git author information (Name or Email). Never include the
|
||||
`--author` flag in git commands unless specifically instructed by the user for a unique
|
||||
case; assume the local environment is already configured. Allow git commit to
|
||||
automatically pull the identity from the local git config `user.name` and `user.email`.
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
name: penpot-backend
|
||||
description: Guidelines and workflows for the Penpot Clojure JVM backend.
|
||||
---
|
||||
|
||||
# Penpot Backend Skill
|
||||
|
||||
This skill provides guidelines and workflows for the Penpot Clojure JVM backend.
|
||||
|
||||
## Testing & Validation
|
||||
- **Isolated tests:** `clojure -M:dev:test --focus backend-tests.my-ns-test` (for a specific test namespace)
|
||||
- **Regression tests:** `clojure -M:dev:test` (ensure the suite passes without regressions)
|
||||
- **Eval expresion:** `clojure -M:dev -e "(here-the-expresion)"`
|
||||
|
||||
## Code Quality
|
||||
- **Linting:** `pnpm run lint:clj`
|
||||
- **Formatting:**
|
||||
- Check: `pnpm run check-fmt`
|
||||
- Fix: `pnpm run fmt`
|
||||
- **Type Hinting:** Use explicit JVM type hints (e.g., `^String`, `^long`) in performance-critical paths to avoid reflection overhead.
|
||||
|
||||
## Architecture & Conventions
|
||||
- Uses Integrant for dependency injection (`src/app/main.clj`).
|
||||
- PostgreSQL for storage, Redis for messaging/caching.
|
||||
- **RPC:** Commands are under `app.rpc.commands.*`. Use the `get-` prefix on RPC names when we want READ operations.
|
||||
- **Database:** `app.db` wraps next.jdbc. Queries use a SQL builder.
|
||||
- Helpers: `db/get`, `db/query`, `db/insert!`, `db/update!`, `db/delete!`
|
||||
- **Performance Macros:** Always prefer these macros from `app.common.data.macros` over `clojure.core` equivalents: `dm/select-keys`, `dm/get-in`, `dm/str`.
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
name: penpot-common
|
||||
description: Guidelines and workflows for the Penpot Common shared module.
|
||||
---
|
||||
|
||||
# Penpot Common Skill
|
||||
|
||||
This skill provides guidelines and workflows for the Penpot Common shared module (Clojure/ClojureScript/JS).
|
||||
|
||||
## Testing & Validation
|
||||
- **JS (Node) Isolated tests:** Edit `test/common_tests/runner.cljs` then run `pnpm run test:js`
|
||||
- **JS (Node) Regression tests:** `pnpm run test:js`
|
||||
- **JVM Isolated tests:** `pnpm run test:jvm --focus common-tests.my-ns-test`
|
||||
- **JVM Regression tests:** `pnpm run test:jvm`
|
||||
|
||||
## Code Quality
|
||||
- **Linting:** `pnpm run lint:clj`
|
||||
- **Formatting:**
|
||||
- Check: `pnpm run check-fmt:clj`, `pnpm run check-fmt:js`
|
||||
- Fix: `pnpm run fmt:clj`, `pnpm run fmt:js`
|
||||
|
||||
## Architecture & Conventions
|
||||
- Multiplatform code used by frontend, backend, and exporter.
|
||||
- Uses Clojure reader conditionals (`#?(:clj ... :cljs ...)`).
|
||||
- Modifying common code requires testing across consumers (frontend, backend, exporter).
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
name: penpot-frontend
|
||||
description: Guidelines and workflows for the Penpot ClojureScript React frontend.
|
||||
---
|
||||
|
||||
# Penpot Frontend Skill
|
||||
|
||||
This skill provides guidelines and workflows for the Penpot ClojureScript React frontend.
|
||||
|
||||
## Testing & Validation
|
||||
- **Isolated tests:** Edit `test/frontend_tests/runner.cljs` to narrow the test suite, then run `pnpm run test`
|
||||
- **Regression tests:** `pnpm run test` (without modifications on the runner)
|
||||
- **Integration tests:** `pnpm run test:e2e` or `pnpm run test:e2e --grep "pattern"` (do not modify e2e tests unless explicitly asked).
|
||||
|
||||
## Code Quality
|
||||
- **Linting:**
|
||||
- `pnpm run lint:clj`
|
||||
- `pnpm run lint:js`
|
||||
- `pnpm run lint:scss`
|
||||
- **Formatting:**
|
||||
- Check: `pnpm run check-fmt:clj`, `pnpm run check-fmt:js`, `pnpm run check-fmt:scss`
|
||||
- Fix: `pnpm run fmt:clj`, `pnpm run fmt:js`, `pnpm run fmt:scss`
|
||||
|
||||
## Architecture & Conventions
|
||||
- Uses React and RxJS (Potok for state management).
|
||||
- Modern components use the `*` suffix (e.g., `my-component*`) and the `mf/defc` macro.
|
||||
- Hooks: `mf/use-state`, `mf/use-effect`, `mf/use-memo`, `mf/use-fn`. Prefer macros `mf/with-effect` and `mf/with-memo`.
|
||||
- Styles: Use CSS custom properties from `_sizes.scss` and tokens from `ds/colors.scss`. Avoid deep selector nesting.
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
name: penpot-render-wasm
|
||||
description: Guidelines and workflows for the Penpot Rust to WebAssembly renderer.
|
||||
---
|
||||
|
||||
# Penpot Render-WASM Skill
|
||||
|
||||
This skill provides guidelines and workflows for the Penpot Rust to WebAssembly renderer.
|
||||
|
||||
## Commands
|
||||
- **Build:** `./build` (Compiles Rust → WASM. Requires Emscripten environment. Automatically sources `_build_env`)
|
||||
- **Watch:** `./watch` (Incremental rebuild on file change)
|
||||
- **Test (All):** `./test` (Runs cargo test)
|
||||
- **Test (Single):** `cargo test my_test_name` or `cargo test shapes::`
|
||||
- **Lint:** `./lint` (`clippy -D warnings`)
|
||||
- **Format:** `cargo fmt --check`
|
||||
|
||||
## Architecture & Conventions
|
||||
- **Global state:** Accessed EXCLUSIVELY through `with_state!` / `with_state_mut!` macros. Never access `unsafe static mut State` directly.
|
||||
- **Tile-based rendering:** Only 512×512 tiles within the viewport are drawn each frame.
|
||||
- **Two-phase updates:** Shape data is written via exported setter functions, then a single `render_frame()` triggers the actual Skia draw calls.
|
||||
- **Frontend Integration:** The WASM module is loaded by `app.render-wasm.*` namespaces. Do not change export function signatures without updating the corresponding ClojureScript bridge.
|
||||
@@ -220,12 +220,14 @@
|
||||
(assoc :hint (ex-message error)))}))))
|
||||
|
||||
(defmethod handle-exception java.io.IOException
|
||||
[cause _ _]
|
||||
(l/wrn :hint "io exception" :cause cause)
|
||||
{::yres/status 500
|
||||
::yres/body {:type :server-error
|
||||
:code :io-exception
|
||||
:hint (ex-message cause)}})
|
||||
[cause request _]
|
||||
(binding [l/*context* (request->context request)]
|
||||
(l/wrn :hint "io exception" :cause cause)
|
||||
{::yres/status 500
|
||||
::yres/body {:type :server-error
|
||||
:code :io-exception
|
||||
:hint (ex-message cause)
|
||||
:path (:path request)}}))
|
||||
|
||||
(defmethod handle-exception java.util.concurrent.CompletionException
|
||||
[cause request _]
|
||||
|
||||
@@ -603,7 +603,7 @@
|
||||
(defmethod generate-sync-shape :components
|
||||
[_ changes _library-id container shape libraries current-file-id]
|
||||
(let [shape-id (:id shape)
|
||||
file (get current-file-id libraries)]
|
||||
file (get libraries current-file-id)]
|
||||
(generate-sync-shape-direct changes file libraries container shape-id false)))
|
||||
|
||||
(defmethod generate-sync-shape :colors
|
||||
@@ -837,7 +837,7 @@
|
||||
shape-inst' (ctn/get-head-shape (:objects container-inst) parent)
|
||||
component' (or (ctkl/get-component library (:component-id shape-inst'))
|
||||
(ctkl/get-deleted-component library (:component-id shape-inst')))]
|
||||
(if (some? component)
|
||||
(if (some? component')
|
||||
(recur shape-inst'
|
||||
component')
|
||||
nil))))))
|
||||
@@ -2241,7 +2241,7 @@
|
||||
(contains? #{:auto :fix} (:layout-item-h-sizing shape-main))
|
||||
(propagate-attrs shape-main #{:layout-item-h-sizing} omit-touched?)
|
||||
|
||||
(contains? #{:auto :fix} (:layout-item-h-sizing shape-main))
|
||||
(contains? #{:auto :fix} (:layout-item-v-sizing shape-main))
|
||||
(propagate-attrs shape-main #{:layout-item-v-sizing} omit-touched?)))
|
||||
{:ignore-touched true})
|
||||
|
||||
@@ -2271,7 +2271,7 @@
|
||||
(contains? #{:auto :fix} (:layout-item-h-sizing shape-copy))
|
||||
(propagate-attrs shape-copy #{:layout-item-h-sizing} omit-touched?)
|
||||
|
||||
(contains? #{:auto :fix} (:layout-item-h-sizing shape-copy))
|
||||
(contains? #{:auto :fix} (:layout-item-v-sizing shape-copy))
|
||||
(propagate-attrs shape-copy #{:layout-item-v-sizing} omit-touched?)))
|
||||
{:ignore-touched true})
|
||||
|
||||
|
||||
@@ -12,11 +12,13 @@
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes.common :as gco]
|
||||
[app.common.geom.shapes.corners :as gsc]
|
||||
[app.common.geom.shapes.effects :as gse]
|
||||
[app.common.geom.shapes.strokes :as gss]
|
||||
[app.common.math :as mth]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.types.text :as txt]
|
||||
[clojure.core :as c]))
|
||||
@@ -117,6 +119,33 @@
|
||||
(or (not ^boolean (mth/almost-zero? (- (dm/get-prop vector :x) 1)))
|
||||
(not ^boolean (mth/almost-zero? (- (dm/get-prop vector :y) 1)))))
|
||||
|
||||
(defn- safe-size-rect?
|
||||
"Returns true when `rect` has finite, in-range, positive width and height."
|
||||
[rect]
|
||||
(and ^boolean (some? rect)
|
||||
(let [w (dm/get-prop rect :width)
|
||||
h (dm/get-prop rect :height)]
|
||||
(and ^boolean (d/num? w h)
|
||||
^boolean (pos? w)
|
||||
^boolean (pos? h)
|
||||
^boolean (<= w sm/max-safe-int)
|
||||
^boolean (<= h sm/max-safe-int)))))
|
||||
|
||||
(defn safe-size-rect
|
||||
"Returns the best available size rect for a shape, trying several
|
||||
fallbacks in order:
|
||||
1. `:selrect` — if it has valid, in-range, positive dimensions.
|
||||
2. `points->rect` — computed from the shape's corner points.
|
||||
3. Top-level `:x :y :width :height` shape fields.
|
||||
4. `grc/empty-rect` — a unit rect (0,0,0.01,0.01) of last resort."
|
||||
[{:keys [selrect points x y width height]}]
|
||||
(or (and ^boolean (safe-size-rect? selrect) selrect)
|
||||
(let [from-points (grc/points->rect points)]
|
||||
(and ^boolean (safe-size-rect? from-points) from-points))
|
||||
(let [from-shape (grc/make-rect x y width height)]
|
||||
(and ^boolean (safe-size-rect? from-shape) from-shape))
|
||||
grc/empty-rect))
|
||||
|
||||
(defn- mergeable-move?
|
||||
[op1 op2]
|
||||
(let [type-op1 (dm/get-prop op1 :type)
|
||||
@@ -195,7 +224,7 @@
|
||||
(conj item)))
|
||||
(conj operations op))))))
|
||||
|
||||
(defn valid-vector?
|
||||
(defn- valid-vector?
|
||||
[vector]
|
||||
(let [x (dm/get-prop vector :x)
|
||||
y (dm/get-prop vector :y)]
|
||||
@@ -309,11 +338,6 @@
|
||||
(-> (or modifiers (empty))
|
||||
(update :structure-child conj (scale-content-op value))))
|
||||
|
||||
(defn change-recursive-property
|
||||
[modifiers property value]
|
||||
(-> (or modifiers (empty))
|
||||
(update :structure-child conj (change-property-op property value))))
|
||||
|
||||
(defn change-property
|
||||
[modifiers property value]
|
||||
(-> (or modifiers (empty))
|
||||
@@ -348,7 +372,7 @@
|
||||
|
||||
(recur result (rest operations)))))))
|
||||
|
||||
(defn increase-order
|
||||
(defn- increase-order
|
||||
[operations last-order]
|
||||
(->> operations
|
||||
(mapv #(update % :order + last-order))))
|
||||
@@ -390,13 +414,6 @@
|
||||
([vector]
|
||||
(move (empty) vector)))
|
||||
|
||||
(defn move-parent-modifiers
|
||||
([x y]
|
||||
(move-parent (empty) (gpt/point x y)))
|
||||
|
||||
([vector]
|
||||
(move-parent (empty) vector)))
|
||||
|
||||
(defn resize-modifiers
|
||||
([vector origin]
|
||||
(resize (empty) vector origin))
|
||||
@@ -404,13 +421,6 @@
|
||||
([vector origin transform transform-inverse]
|
||||
(resize (empty) vector origin transform transform-inverse)))
|
||||
|
||||
(defn resize-parent-modifiers
|
||||
([vector origin]
|
||||
(resize-parent (empty) vector origin))
|
||||
|
||||
([vector origin transform transform-inverse]
|
||||
(resize-parent (empty) vector origin transform transform-inverse)))
|
||||
|
||||
(defn rotation-modifiers
|
||||
[shape center angle]
|
||||
(let [shape-center (gco/shape->center shape)
|
||||
@@ -426,73 +436,54 @@
|
||||
(rotation shape-center angle)
|
||||
(move move-vec))))
|
||||
|
||||
(defn remove-children-modifiers
|
||||
[shapes]
|
||||
(-> (empty)
|
||||
(remove-children shapes)))
|
||||
|
||||
(defn add-children-modifiers
|
||||
[shapes index]
|
||||
(-> (empty)
|
||||
(add-children shapes index)))
|
||||
|
||||
(defn reflow-modifiers
|
||||
[]
|
||||
(-> (empty)
|
||||
(reflow)))
|
||||
|
||||
(defn scale-content-modifiers
|
||||
[value]
|
||||
(-> (empty)
|
||||
(scale-content value)))
|
||||
|
||||
(defn change-size
|
||||
[{:keys [selrect points transform transform-inverse] :as shape} width height]
|
||||
(let [old-width (-> selrect :width)
|
||||
old-height (-> selrect :height)
|
||||
width (or width old-width)
|
||||
height (or height old-height)
|
||||
origin (first points)
|
||||
scalex (/ width old-width)
|
||||
scaley (/ height old-height)]
|
||||
[{:keys [points transform transform-inverse] :as shape} width height]
|
||||
(let [{sr-width :width sr-height :height} (safe-size-rect shape)
|
||||
width (or width sr-width)
|
||||
height (or height sr-height)
|
||||
origin (first points)
|
||||
scalex (/ width sr-width)
|
||||
scaley (/ height sr-height)]
|
||||
(resize-modifiers (gpt/point scalex scaley) origin transform transform-inverse)))
|
||||
|
||||
(defn change-dimensions-modifiers
|
||||
([shape attr value]
|
||||
(change-dimensions-modifiers shape attr value nil))
|
||||
|
||||
([{:keys [transform transform-inverse] :as shape} attr value {:keys [ignore-lock?] :or {ignore-lock? false}}]
|
||||
([shape attr value {:keys [ignore-lock?] :or {ignore-lock? false}}]
|
||||
(dm/assert! (map? shape))
|
||||
(dm/assert! (#{:width :height} attr))
|
||||
(dm/assert! (number? value))
|
||||
|
||||
(let [;; Avoid havig shapes with zero size
|
||||
value (if (< (mth/abs value) 0.01)
|
||||
0.01
|
||||
value)
|
||||
(let [;; Avoid having shapes with zero size
|
||||
value (if (< (mth/abs value) 0.01) 0.01 value)
|
||||
|
||||
{:keys [proportion proportion-lock]} shape
|
||||
size (select-keys (:selrect shape) [:width :height])
|
||||
new-size (if-not (and (not ignore-lock?) proportion-lock)
|
||||
(assoc size attr value)
|
||||
(if (= attr :width)
|
||||
(-> size
|
||||
(assoc :width value)
|
||||
(assoc :height (/ value proportion)))
|
||||
(-> size
|
||||
(assoc :height value)
|
||||
(assoc :width (* value proportion)))))
|
||||
{sr-width :width sr-height :height} (safe-size-rect shape)
|
||||
locked? (and (not ignore-lock?) proportion-lock)
|
||||
|
||||
width (:width new-size)
|
||||
height (:height new-size)
|
||||
width (if (= attr :width)
|
||||
value
|
||||
(if locked? (* value proportion) sr-width))
|
||||
|
||||
{sr-width :width sr-height :height} (:selrect shape)
|
||||
height (if (= attr :height)
|
||||
value
|
||||
(if locked? (/ value proportion) sr-height))
|
||||
|
||||
origin (-> shape :points first)
|
||||
scalex (/ width sr-width)
|
||||
scaley (/ height sr-height)]
|
||||
|
||||
(resize-modifiers (gpt/point scalex scaley) origin transform transform-inverse))))
|
||||
(resize-modifiers
|
||||
(gpt/point scalex scaley)
|
||||
origin
|
||||
(:transform shape)
|
||||
(:transform-inverse shape)))))
|
||||
|
||||
(defn change-orientation-modifiers
|
||||
[shape orientation]
|
||||
@@ -566,7 +557,7 @@
|
||||
[modifiers]
|
||||
(assoc (or modifiers (empty)) :geometry-child [] :structure-child []))
|
||||
|
||||
(defn select-structure
|
||||
(defn- select-structure
|
||||
[modifiers]
|
||||
(assoc (or modifiers (empty)) :geometry-child [] :geometry-parent []))
|
||||
|
||||
@@ -574,10 +565,6 @@
|
||||
[modifiers]
|
||||
(assoc (or modifiers (empty)) :structure-child [] :structure-parent []))
|
||||
|
||||
(defn select-child-geometry-modifiers
|
||||
[modifiers]
|
||||
(-> modifiers select-child select-geometry))
|
||||
|
||||
(defn select-child-structre-modifiers
|
||||
[modifiers]
|
||||
(-> modifiers select-child select-structure))
|
||||
@@ -601,7 +588,7 @@
|
||||
|
||||
;; Main transformation functions
|
||||
|
||||
(defn transform-move!
|
||||
(defn- transform-move!
|
||||
"Transforms a matrix by the translation modifier"
|
||||
[matrix modifier]
|
||||
(-> (dm/get-prop modifier :vector)
|
||||
@@ -609,7 +596,7 @@
|
||||
(gmt/multiply! matrix)))
|
||||
|
||||
|
||||
(defn transform-resize!
|
||||
(defn- transform-resize!
|
||||
"Transforms a matrix by the resize modifier"
|
||||
[matrix modifier]
|
||||
(let [tf (dm/get-prop modifier :transform)
|
||||
@@ -631,7 +618,7 @@
|
||||
(gmt/multiply! tfi)))
|
||||
matrix)))
|
||||
|
||||
(defn transform-rotate!
|
||||
(defn- transform-rotate!
|
||||
"Transforms a matrix by the rotation modifier"
|
||||
[matrix modifier]
|
||||
(let [center (dm/get-prop modifier :center)
|
||||
@@ -643,7 +630,7 @@
|
||||
(gmt/translate! (gpt/negate center)))
|
||||
matrix)))
|
||||
|
||||
(defn transform!
|
||||
(defn- transform!
|
||||
"Returns a matrix transformed by the modifier"
|
||||
[matrix modifier]
|
||||
(let [type (dm/get-prop modifier :type)]
|
||||
@@ -652,8 +639,7 @@
|
||||
:resize (transform-resize! matrix modifier)
|
||||
:rotation (transform-rotate! matrix modifier))))
|
||||
|
||||
(defn modifiers->transform1
|
||||
"A multiplatform version of modifiers->transform."
|
||||
(defn- modifiers->transform1
|
||||
[modifiers]
|
||||
(reduce transform! (gmt/matrix) modifiers))
|
||||
|
||||
@@ -665,80 +651,28 @@
|
||||
modifiers (sort-by #(dm/get-prop % :order) modifiers)]
|
||||
(modifiers->transform1 modifiers)))
|
||||
|
||||
(defn modifiers->transform-old
|
||||
"Given a set of modifiers returns its transformation matrix"
|
||||
[modifiers]
|
||||
(let [modifiers (->> (concat (dm/get-prop modifiers :geometry-parent)
|
||||
(dm/get-prop modifiers :geometry-child))
|
||||
(sort-by :order))]
|
||||
|
||||
(loop [matrix (gmt/matrix)
|
||||
modifiers (seq modifiers)]
|
||||
(if (c/empty? modifiers)
|
||||
matrix
|
||||
(let [modifier (first modifiers)
|
||||
type (dm/get-prop modifier :type)
|
||||
|
||||
matrix
|
||||
(case type
|
||||
:move
|
||||
(-> (dm/get-prop modifier :vector)
|
||||
(gmt/translate-matrix)
|
||||
(gmt/multiply! matrix))
|
||||
|
||||
:resize
|
||||
(let [tf (dm/get-prop modifier :transform)
|
||||
tfi (dm/get-prop modifier :transform-inverse)
|
||||
vector (dm/get-prop modifier :vector)
|
||||
origin (dm/get-prop modifier :origin)
|
||||
origin (if ^boolean (some? tfi)
|
||||
(gpt/transform origin tfi)
|
||||
origin)]
|
||||
|
||||
(gmt/multiply!
|
||||
(-> (gmt/matrix)
|
||||
(cond-> ^boolean (some? tf)
|
||||
(gmt/multiply! tf))
|
||||
(gmt/translate! origin)
|
||||
(gmt/scale! vector)
|
||||
(gmt/translate! (gpt/negate origin))
|
||||
(cond-> ^boolean (some? tfi)
|
||||
(gmt/multiply! tfi)))
|
||||
matrix))
|
||||
|
||||
:rotation
|
||||
(let [center (dm/get-prop modifier :center)
|
||||
rotation (dm/get-prop modifier :rotation)]
|
||||
(gmt/multiply!
|
||||
(-> (gmt/matrix)
|
||||
(gmt/translate! center)
|
||||
(gmt/multiply! (gmt/rotate-matrix rotation))
|
||||
(gmt/translate! (gpt/negate center)))
|
||||
matrix)))]
|
||||
(recur matrix (next modifiers)))))))
|
||||
|
||||
(defn transform-text-node [value attrs]
|
||||
(defn- transform-text-node [value attrs]
|
||||
(let [font-size (-> (get attrs :font-size 14) d/parse-double (* value) str)
|
||||
letter-spacing (-> (get attrs :letter-spacing 0) d/parse-double (* value) str)]
|
||||
(d/txt-merge attrs {:font-size font-size
|
||||
:letter-spacing letter-spacing})))
|
||||
|
||||
(defn transform-paragraph-node [value attrs]
|
||||
(defn- transform-paragraph-node [value attrs]
|
||||
(let [font-size (-> (get attrs :font-size 14) d/parse-double (* value) str)]
|
||||
(d/txt-merge attrs {:font-size font-size})))
|
||||
|
||||
|
||||
(defn update-text-content
|
||||
(defn- update-text-content
|
||||
[shape scale-text-content value]
|
||||
(update shape :content scale-text-content value))
|
||||
|
||||
(defn scale-text-content
|
||||
(defn- scale-text-content
|
||||
[content value]
|
||||
(->> content
|
||||
(txt/transform-nodes txt/is-text-node? (partial transform-text-node value))
|
||||
(txt/transform-nodes txt/is-paragraph-node? (partial transform-paragraph-node value))))
|
||||
|
||||
(defn apply-scale-content
|
||||
(defn- apply-scale-content
|
||||
[shape value]
|
||||
;; Scale can only be positive
|
||||
(let [value (mth/abs value)]
|
||||
@@ -767,7 +701,7 @@
|
||||
:always
|
||||
(ctl/update-flex-child value))))
|
||||
|
||||
(defn remove-children-set
|
||||
(defn- remove-children-set
|
||||
[shapes children-to-remove]
|
||||
(let [remove? (set children-to-remove)]
|
||||
(d/removev remove? shapes)))
|
||||
|
||||
@@ -47,8 +47,10 @@
|
||||
[common-tests.types.path-data-test]
|
||||
[common-tests.types.shape-decode-encode-test]
|
||||
[common-tests.types.shape-interactions-test]
|
||||
[common-tests.types.shape-layout-test]
|
||||
[common-tests.types.token-test]
|
||||
[common-tests.types.tokens-lib-test]
|
||||
[common-tests.undo-stack-test]
|
||||
[common-tests.uuid-test]))
|
||||
|
||||
#?(:cljs (enable-console-print!))
|
||||
@@ -93,6 +95,7 @@
|
||||
'common-tests.svg-test
|
||||
'common-tests.text-test
|
||||
'common-tests.time-test
|
||||
'common-tests.undo-stack-test
|
||||
'common-tests.types.absorb-assets-test
|
||||
'common-tests.types.components-test
|
||||
'common-tests.types.container-test
|
||||
@@ -102,6 +105,7 @@
|
||||
'common-tests.types.path-data-test
|
||||
'common-tests.types.shape-decode-encode-test
|
||||
'common-tests.types.shape-interactions-test
|
||||
'common-tests.types.shape-layout-test
|
||||
'common-tests.types.tokens-lib-test
|
||||
'common-tests.types.token-test
|
||||
'common-tests.uuid-test))
|
||||
|
||||
@@ -8,7 +8,13 @@
|
||||
(:require
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/deftest modifiers->transform
|
||||
@@ -24,3 +30,630 @@
|
||||
transform (ctm/modifiers->transform modifiers)]
|
||||
|
||||
(t/is (not (gmt/close? (gmt/matrix) transform)))))
|
||||
|
||||
;; ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
(defn- make-shape
|
||||
"Build a minimal axis-aligned rect shape with the given geometry."
|
||||
([width height]
|
||||
(make-shape 0 0 width height))
|
||||
([x y width height]
|
||||
(cts/setup-shape {:type :rect :x x :y y :width width :height height})))
|
||||
|
||||
(defn- make-shape-with-proportion
|
||||
"Build a shape with a fixed proportion ratio and proportion-lock enabled."
|
||||
[width height]
|
||||
(assoc (make-shape width height)
|
||||
:proportion (/ (float width) (float height))
|
||||
:proportion-lock true))
|
||||
|
||||
(defn- resize-op
|
||||
"Extract the single resize GeometricOperation from geometry-child."
|
||||
[modifiers]
|
||||
(first (:geometry-child modifiers)))
|
||||
|
||||
;; ─── change-size ──────────────────────────────────────────────────────────────
|
||||
|
||||
(t/deftest change-size-basic
|
||||
(t/testing "scales both axes to the requested dimensions"
|
||||
(let [shape (make-shape 100 50)
|
||||
mods (ctm/change-size shape 200 100)
|
||||
op (resize-op mods)]
|
||||
(t/is (mth/close? 2.0 (-> op :vector :x)))
|
||||
(t/is (mth/close? 2.0 (-> op :vector :y)))))
|
||||
|
||||
(t/testing "origin is the first point of the shape (top-left)"
|
||||
(let [shape (make-shape 10 20 100 50)
|
||||
mods (ctm/change-size shape 200 50)
|
||||
origin (:origin (resize-op mods))]
|
||||
(t/is (mth/close? 10.0 (:x origin)))
|
||||
(t/is (mth/close? 20.0 (:y origin)))))
|
||||
|
||||
(t/testing "nil width falls back to current width, keeping x-scale at 1"
|
||||
(let [shape (make-shape 100 50)
|
||||
mods (ctm/change-size shape nil 100)
|
||||
op (resize-op mods)]
|
||||
(t/is (mth/close? 1.0 (-> op :vector :x)))
|
||||
(t/is (mth/close? 2.0 (-> op :vector :y)))))
|
||||
|
||||
(t/testing "nil height falls back to current height, keeping y-scale at 1"
|
||||
(let [shape (make-shape 100 50)
|
||||
mods (ctm/change-size shape 200 nil)
|
||||
op (resize-op mods)]
|
||||
(t/is (mth/close? 2.0 (-> op :vector :x)))
|
||||
(t/is (mth/close? 1.0 (-> op :vector :y)))))
|
||||
|
||||
(t/testing "both nil produces an identity resize (scale 1,1)"
|
||||
(let [shape (make-shape 100 50)
|
||||
mods (ctm/change-size shape nil nil)
|
||||
op (resize-op mods)]
|
||||
;; Identity resize is optimized away; geometry-child should be empty.
|
||||
(t/is (empty? (:geometry-child mods)))))
|
||||
|
||||
(t/testing "transform and transform-inverse on a plain shape are both the identity matrix"
|
||||
(let [shape (make-shape 100 50)
|
||||
mods (ctm/change-size shape 200 100)
|
||||
op (resize-op mods)]
|
||||
(t/is (gmt/close? (gmt/matrix) (:transform op)))
|
||||
(t/is (gmt/close? (gmt/matrix) (:transform-inverse op))))))
|
||||
|
||||
;; ─── change-dimensions-modifiers ──────────────────────────────────────────────
|
||||
|
||||
(t/deftest change-dimensions-modifiers-no-lock
|
||||
(t/testing "changing width only scales x-axis; y-scale stays 1"
|
||||
(let [shape (make-shape 100 50)
|
||||
mods (ctm/change-dimensions-modifiers shape :width 200)
|
||||
op (resize-op mods)]
|
||||
(t/is (mth/close? 2.0 (-> op :vector :x)))
|
||||
(t/is (mth/close? 1.0 (-> op :vector :y)))))
|
||||
|
||||
(t/testing "changing height only scales y-axis; x-scale stays 1"
|
||||
(let [shape (make-shape 100 50)
|
||||
mods (ctm/change-dimensions-modifiers shape :height 100)
|
||||
op (resize-op mods)]
|
||||
(t/is (mth/close? 1.0 (-> op :vector :x)))
|
||||
(t/is (mth/close? 2.0 (-> op :vector :y)))))
|
||||
|
||||
(t/testing "origin is always the top-left point of the shape"
|
||||
(let [shape (make-shape 30 40 100 50)
|
||||
mods (ctm/change-dimensions-modifiers shape :width 200)
|
||||
origin (:origin (resize-op mods))]
|
||||
(t/is (mth/close? 30.0 (:x origin)))
|
||||
(t/is (mth/close? 40.0 (:y origin))))))
|
||||
|
||||
(t/deftest change-dimensions-modifiers-with-proportion-lock
|
||||
(t/testing "locking width also adjusts height by the inverse proportion"
|
||||
;; shape 100x50 → proportion = 100/50 = 2
|
||||
;; new width 200 → expected height = 200/2 = 100 → scaley = 100/50 = 2
|
||||
(let [shape (make-shape-with-proportion 100 50)
|
||||
mods (ctm/change-dimensions-modifiers shape :width 200)
|
||||
op (resize-op mods)]
|
||||
(t/is (mth/close? 2.0 (-> op :vector :x)))
|
||||
(t/is (mth/close? 2.0 (-> op :vector :y)))))
|
||||
|
||||
(t/testing "locking height also adjusts width by the proportion"
|
||||
;; shape 100x50 → proportion = 100/50 = 2
|
||||
;; new height 100 → expected width = 100*2 = 200 → scalex = 200/100 = 2
|
||||
(let [shape (make-shape-with-proportion 100 50)
|
||||
mods (ctm/change-dimensions-modifiers shape :height 100)
|
||||
op (resize-op mods)]
|
||||
(t/is (mth/close? 2.0 (-> op :vector :x)))
|
||||
(t/is (mth/close? 2.0 (-> op :vector :y)))))
|
||||
|
||||
(t/testing "ignore-lock? true bypasses proportion lock"
|
||||
(let [shape (make-shape-with-proportion 100 50)
|
||||
mods (ctm/change-dimensions-modifiers shape :width 200 {:ignore-lock? true})
|
||||
op (resize-op mods)]
|
||||
(t/is (mth/close? 2.0 (-> op :vector :x)))
|
||||
;; Height should remain unchanged (scale = 1).
|
||||
(t/is (mth/close? 1.0 (-> op :vector :y))))))
|
||||
|
||||
(t/deftest change-dimensions-modifiers-value-clamping
|
||||
(t/testing "value below 0.01 is clamped to 0.01"
|
||||
(let [shape (make-shape 100 50)
|
||||
mods (ctm/change-dimensions-modifiers shape :width 0.001)
|
||||
op (resize-op mods)]
|
||||
;; 0.01 / 100 = 0.0001
|
||||
(t/is (mth/close? 0.0001 (-> op :vector :x)))))
|
||||
|
||||
(t/testing "value of exactly 0 is clamped to 0.01"
|
||||
(let [shape (make-shape 100 50)
|
||||
mods (ctm/change-dimensions-modifiers shape :height 0)
|
||||
op (resize-op mods)]
|
||||
;; 0.01 / 50 = 0.0002
|
||||
(t/is (mth/close? 0.0002 (-> op :vector :y))))))
|
||||
|
||||
(t/deftest change-dimensions-modifiers-end-to-end
|
||||
(t/testing "applying change-width modifier produces the expected selrect width"
|
||||
(let [shape (make-shape 100 50)
|
||||
mods (ctm/change-dimensions-modifiers shape :width 300)
|
||||
result (gsh/transform-shape (assoc shape :modifiers mods))]
|
||||
(t/is (mth/close? 300.0 (-> result :selrect :width)))
|
||||
(t/is (mth/close? 50.0 (-> result :selrect :height)))))
|
||||
|
||||
(t/testing "applying change-height modifier produces the expected selrect height"
|
||||
(let [shape (make-shape 100 50)
|
||||
mods (ctm/change-dimensions-modifiers shape :height 200)
|
||||
result (gsh/transform-shape (assoc shape :modifiers mods))]
|
||||
(t/is (mth/close? 100.0 (-> result :selrect :width)))
|
||||
(t/is (mth/close? 200.0 (-> result :selrect :height))))))
|
||||
|
||||
;; ─── safe-size-rect fallbacks ─────────────────────────────────────────────────
|
||||
|
||||
(t/deftest safe-size-rect-fallbacks
|
||||
(t/testing "valid selrect is returned as-is"
|
||||
(let [shape (make-shape 100 50)
|
||||
mods (ctm/change-size shape 200 100)
|
||||
op (resize-op mods)]
|
||||
;; scale 2,2 means the selrect was valid
|
||||
(t/is (mth/close? 2.0 (-> op :vector :x)))
|
||||
(t/is (mth/close? 2.0 (-> op :vector :y)))))
|
||||
|
||||
(t/testing "zero-width selrect falls back to points, producing a valid rect"
|
||||
;; Corrupt only the selrect dimensions; the shape's points remain valid.
|
||||
(let [base (make-shape 100 50)
|
||||
bad-selrect (assoc (:selrect base) :width 0 :height 0)
|
||||
shape (assoc base :selrect bad-selrect)
|
||||
mods (ctm/change-size shape 200 100)
|
||||
op (resize-op mods)]
|
||||
(t/is (cts/shape? shape))
|
||||
(t/is (mth/close? 2.0 (-> op :vector :x)))
|
||||
(t/is (mth/close? 2.0 (-> op :vector :y)))))
|
||||
|
||||
(t/testing "NaN selrect falls back to points"
|
||||
(let [base (make-shape 100 50)
|
||||
bad-selrect (assoc (:selrect base) :width ##NaN :height ##NaN)
|
||||
shape (assoc base :selrect bad-selrect)
|
||||
mods (ctm/change-size shape 200 100)
|
||||
op (resize-op mods)]
|
||||
(t/is (cts/shape? shape))
|
||||
(t/is (mth/close? 2.0 (-> op :vector :x)))
|
||||
(t/is (mth/close? 2.0 (-> op :vector :y)))))
|
||||
|
||||
(t/testing "selrect with dimensions exceeding max-safe-int falls back to points"
|
||||
(let [base (make-shape 100 50)
|
||||
bad-selrect (assoc (:selrect base) :width (inc sm/max-safe-int) :height (inc sm/max-safe-int))
|
||||
shape (assoc base :selrect bad-selrect)
|
||||
mods (ctm/change-size shape 200 100)
|
||||
op (resize-op mods)]
|
||||
(t/is (cts/shape? shape))
|
||||
(t/is (mth/close? 2.0 (-> op :vector :x)))
|
||||
(t/is (mth/close? 2.0 (-> op :vector :y)))))
|
||||
|
||||
(t/testing "invalid selrect and no points falls back to top-level shape fields"
|
||||
;; Null out both selrect and points; the top-level :x/:y/:width/:height
|
||||
;; fields on the Shape record are still valid and serve as fallback 3.
|
||||
(let [shape (-> (make-shape 100 50)
|
||||
(assoc :selrect nil)
|
||||
(assoc :points nil))
|
||||
mods (ctm/change-size shape 200 100)
|
||||
op (resize-op mods)]
|
||||
(t/is (cts/shape? shape))
|
||||
(t/is (mth/close? 2.0 (-> op :vector :x)))
|
||||
(t/is (mth/close? 2.0 (-> op :vector :y)))))
|
||||
|
||||
(t/testing "all geometry missing: falls back to empty-rect (0.01 x 0.01)"
|
||||
;; Null out selrect, points and the top-level dimension fields so that
|
||||
;; every fallback is exhausted and empty-rect (0.01×0.01) is used.
|
||||
(let [shape (-> (make-shape 100 50)
|
||||
(assoc :selrect nil)
|
||||
(assoc :points nil)
|
||||
(assoc :width nil)
|
||||
(assoc :height nil))
|
||||
mods (ctm/change-size shape 200 100)
|
||||
op (resize-op mods)]
|
||||
(t/is (cts/shape? shape))
|
||||
(t/is (mth/close? (/ 200 0.01) (-> op :vector :x)))
|
||||
(t/is (mth/close? (/ 100 0.01) (-> op :vector :y))))))
|
||||
|
||||
;; ─── Builder functions: geometry-child ────────────────────────────────────────
|
||||
|
||||
(t/deftest move-builder
|
||||
(t/testing "move adds an operation to geometry-child"
|
||||
(let [mods (ctm/move (ctm/empty) (gpt/point 10 20))]
|
||||
(t/is (= 1 (count (:geometry-child mods))))
|
||||
(t/is (= :move (-> mods :geometry-child first :type)))))
|
||||
|
||||
(t/testing "move with zero vector is optimised away"
|
||||
(let [mods (ctm/move (ctm/empty) (gpt/point 0 0))]
|
||||
(t/is (empty? (:geometry-child mods)))))
|
||||
|
||||
(t/testing "two consecutive moves on the same axis are merged into one"
|
||||
(let [mods (-> (ctm/empty)
|
||||
(ctm/move (gpt/point 10 0))
|
||||
(ctm/move (gpt/point 5 0)))]
|
||||
(t/is (= 1 (count (:geometry-child mods))))
|
||||
(t/is (mth/close? 15.0 (-> mods :geometry-child first :vector :x)))))
|
||||
|
||||
(t/testing "move with x y arity delegates to vector arity"
|
||||
(let [mods (ctm/move (ctm/empty) 3 7)]
|
||||
(t/is (= 1 (count (:geometry-child mods))))
|
||||
(t/is (mth/close? 3.0 (-> mods :geometry-child first :vector :x)))
|
||||
(t/is (mth/close? 7.0 (-> mods :geometry-child first :vector :y))))))
|
||||
|
||||
(t/deftest resize-builder
|
||||
(t/testing "resize adds an operation to geometry-child"
|
||||
(let [mods (ctm/resize (ctm/empty) (gpt/point 2 3) (gpt/point 0 0))]
|
||||
(t/is (= 1 (count (:geometry-child mods))))
|
||||
(t/is (= :resize (-> mods :geometry-child first :type)))))
|
||||
|
||||
(t/testing "identity resize (scale 1,1) is optimised away"
|
||||
(let [mods (ctm/resize (ctm/empty) (gpt/point 1 1) (gpt/point 0 0))]
|
||||
(t/is (empty? (:geometry-child mods)))))
|
||||
|
||||
(t/testing "precise? flag keeps near-identity resize"
|
||||
(let [mods (ctm/resize (ctm/empty) (gpt/point 1 1) (gpt/point 0 0)
|
||||
nil nil {:precise? true})]
|
||||
(t/is (= 1 (count (:geometry-child mods))))))
|
||||
|
||||
(t/testing "resize stores origin, transform and transform-inverse"
|
||||
(let [tf (gmt/matrix)
|
||||
tfi (gmt/matrix)
|
||||
mods (ctm/resize (ctm/empty) (gpt/point 2 2) (gpt/point 5 10) tf tfi)
|
||||
op (-> mods :geometry-child first)]
|
||||
(t/is (mth/close? 5.0 (-> op :origin :x)))
|
||||
(t/is (mth/close? 10.0 (-> op :origin :y)))
|
||||
(t/is (gmt/close? tf (:transform op)))
|
||||
(t/is (gmt/close? tfi (:transform-inverse op))))))
|
||||
|
||||
(t/deftest rotation-builder
|
||||
(t/testing "rotation adds ops to both geometry-child and structure-child"
|
||||
(let [mods (ctm/rotation (ctm/empty) (gpt/point 50 50) 45)]
|
||||
(t/is (= 1 (count (:geometry-child mods))))
|
||||
(t/is (= 1 (count (:structure-child mods))))
|
||||
(t/is (= :rotation (-> mods :geometry-child first :type)))
|
||||
(t/is (= :rotation (-> mods :structure-child first :type)))))
|
||||
|
||||
(t/testing "zero-angle rotation is optimised away"
|
||||
(let [mods (ctm/rotation (ctm/empty) (gpt/point 50 50) 0)]
|
||||
(t/is (empty? (:geometry-child mods)))
|
||||
(t/is (empty? (:structure-child mods))))))
|
||||
|
||||
;; ─── Builder functions: geometry-parent ───────────────────────────────────────
|
||||
|
||||
(t/deftest move-parent-builder
|
||||
(t/testing "move-parent adds an operation to geometry-parent, not geometry-child"
|
||||
(let [mods (ctm/move-parent (ctm/empty) (gpt/point 10 20))]
|
||||
(t/is (= 1 (count (:geometry-parent mods))))
|
||||
(t/is (empty? (:geometry-child mods)))
|
||||
(t/is (= :move (-> mods :geometry-parent first :type)))))
|
||||
|
||||
(t/testing "move-parent with zero vector is optimised away"
|
||||
(let [mods (ctm/move-parent (ctm/empty) (gpt/point 0 0))]
|
||||
(t/is (empty? (:geometry-parent mods))))))
|
||||
|
||||
(t/deftest resize-parent-builder
|
||||
(t/testing "resize-parent adds an operation to geometry-parent, not geometry-child"
|
||||
(let [mods (ctm/resize-parent (ctm/empty) (gpt/point 2 3) (gpt/point 0 0))]
|
||||
(t/is (= 1 (count (:geometry-parent mods))))
|
||||
(t/is (empty? (:geometry-child mods)))
|
||||
(t/is (= :resize (-> mods :geometry-parent first :type))))))
|
||||
|
||||
;; ─── Builder functions: structure ─────────────────────────────────────────────
|
||||
|
||||
(t/deftest structure-builders
|
||||
(t/testing "add-children appends an add-children op to structure-parent"
|
||||
(let [id1 (uuid/next)
|
||||
id2 (uuid/next)
|
||||
mods (ctm/add-children (ctm/empty) [id1 id2] nil)]
|
||||
(t/is (= 1 (count (:structure-parent mods))))
|
||||
(t/is (= :add-children (-> mods :structure-parent first :type)))))
|
||||
|
||||
(t/testing "add-children with empty list is a no-op"
|
||||
(let [mods (ctm/add-children (ctm/empty) [] nil)]
|
||||
(t/is (empty? (:structure-parent mods)))))
|
||||
|
||||
(t/testing "remove-children appends a remove-children op to structure-parent"
|
||||
(let [id (uuid/next)
|
||||
mods (ctm/remove-children (ctm/empty) [id])]
|
||||
(t/is (= 1 (count (:structure-parent mods))))
|
||||
(t/is (= :remove-children (-> mods :structure-parent first :type)))))
|
||||
|
||||
(t/testing "remove-children with empty list is a no-op"
|
||||
(let [mods (ctm/remove-children (ctm/empty) [])]
|
||||
(t/is (empty? (:structure-parent mods)))))
|
||||
|
||||
(t/testing "reflow appends a reflow op to structure-parent"
|
||||
(let [mods (ctm/reflow (ctm/empty))]
|
||||
(t/is (= 1 (count (:structure-parent mods))))
|
||||
(t/is (= :reflow (-> mods :structure-parent first :type)))))
|
||||
|
||||
(t/testing "scale-content appends a scale-content op to structure-child"
|
||||
(let [mods (ctm/scale-content (ctm/empty) 2.0)]
|
||||
(t/is (= 1 (count (:structure-child mods))))
|
||||
(t/is (= :scale-content (-> mods :structure-child first :type)))))
|
||||
|
||||
(t/testing "change-property appends a change-property op to structure-parent"
|
||||
(let [mods (ctm/change-property (ctm/empty) :opacity 0.5)]
|
||||
(t/is (= 1 (count (:structure-parent mods))))
|
||||
(t/is (= :change-property (-> mods :structure-parent first :type)))
|
||||
(t/is (= :opacity (-> mods :structure-parent first :property)))
|
||||
(t/is (= 0.5 (-> mods :structure-parent first :value))))))
|
||||
|
||||
;; ─── Convenience builders ─────────────────────────────────────────────────────
|
||||
|
||||
(t/deftest convenience-builders
|
||||
(t/testing "move-modifiers returns a fresh Modifiers with a move in geometry-child"
|
||||
(let [mods (ctm/move-modifiers (gpt/point 5 10))]
|
||||
(t/is (= 1 (count (:geometry-child mods))))
|
||||
(t/is (= :move (-> mods :geometry-child first :type)))))
|
||||
|
||||
(t/testing "move-modifiers accepts x y arity"
|
||||
(let [mods (ctm/move-modifiers 5 10)]
|
||||
(t/is (= 1 (count (:geometry-child mods))))))
|
||||
|
||||
(t/testing "resize-modifiers returns a fresh Modifiers with a resize in geometry-child"
|
||||
(let [mods (ctm/resize-modifiers (gpt/point 2 2) (gpt/point 0 0))]
|
||||
(t/is (= 1 (count (:geometry-child mods))))
|
||||
(t/is (= :resize (-> mods :geometry-child first :type)))))
|
||||
|
||||
(t/testing "reflow-modifiers returns a fresh Modifiers with a reflow in structure-parent"
|
||||
(let [mods (ctm/reflow-modifiers)]
|
||||
(t/is (= 1 (count (:structure-parent mods))))
|
||||
(t/is (= :reflow (-> mods :structure-parent first :type)))))
|
||||
|
||||
(t/testing "rotation-modifiers returns move + rotation in geometry-child"
|
||||
(let [shape (make-shape 100 100)
|
||||
mods (ctm/rotation-modifiers shape (gpt/point 50 50) 90)]
|
||||
;; rotation adds a :rotation and a :move to compensate for off-center
|
||||
(t/is (pos? (count (:geometry-child mods))))
|
||||
(t/is (some #(= :rotation (:type %)) (:geometry-child mods))))))
|
||||
|
||||
;; ─── add-modifiers ────────────────────────────────────────────────────────────
|
||||
|
||||
(t/deftest add-modifiers-combinator
|
||||
(t/testing "combining two disjoint move modifiers sums the vectors"
|
||||
(let [m1 (ctm/move-modifiers (gpt/point 10 0))
|
||||
m2 (ctm/move-modifiers (gpt/point 5 0))
|
||||
result (ctm/add-modifiers m1 m2)]
|
||||
;; Both are pure geometry-child moves → they get merged
|
||||
(t/is (= 1 (count (:geometry-child result))))
|
||||
(t/is (mth/close? 15.0 (-> result :geometry-child first :vector :x)))))
|
||||
|
||||
(t/testing "nil first argument is treated as empty"
|
||||
(let [m2 (ctm/move-modifiers (gpt/point 3 4))
|
||||
result (ctm/add-modifiers nil m2)]
|
||||
(t/is (= 1 (count (:geometry-child result))))))
|
||||
|
||||
(t/testing "nil second argument is treated as empty"
|
||||
(let [m1 (ctm/move-modifiers (gpt/point 3 4))
|
||||
result (ctm/add-modifiers m1 nil)]
|
||||
(t/is (= 1 (count (:geometry-child result))))))
|
||||
|
||||
(t/testing "last-order is the sum of both modifiers' orders"
|
||||
(let [m1 (ctm/move-modifiers (gpt/point 1 0))
|
||||
m2 (ctm/move-modifiers (gpt/point 2 0))
|
||||
result (ctm/add-modifiers m1 m2)]
|
||||
(t/is (= (+ (:last-order m1) (:last-order m2))
|
||||
(:last-order result))))))
|
||||
|
||||
;; ─── Predicates ───────────────────────────────────────────────────────────────
|
||||
|
||||
(t/deftest predicate-empty?
|
||||
(t/testing "fresh empty modifiers is empty?"
|
||||
(t/is (ctm/empty? (ctm/empty))))
|
||||
|
||||
(t/testing "modifiers with a move are not empty?"
|
||||
(t/is (not (ctm/empty? (ctm/move-modifiers (gpt/point 1 0))))))
|
||||
|
||||
(t/testing "modifiers with only a structure op are not empty?"
|
||||
(t/is (not (ctm/empty? (ctm/reflow-modifiers))))))
|
||||
|
||||
(t/deftest predicate-child-modifiers?
|
||||
(t/testing "move in geometry-child → child-modifiers? true"
|
||||
(t/is (ctm/child-modifiers? (ctm/move-modifiers (gpt/point 1 0)))))
|
||||
|
||||
(t/testing "scale-content in structure-child → child-modifiers? true"
|
||||
(t/is (ctm/child-modifiers? (ctm/scale-content (ctm/empty) 2.0))))
|
||||
|
||||
(t/testing "move-parent in geometry-parent only → child-modifiers? false"
|
||||
(t/is (not (ctm/child-modifiers? (ctm/move-parent (ctm/empty) (gpt/point 1 0)))))))
|
||||
|
||||
(t/deftest predicate-has-geometry?
|
||||
(t/testing "move in geometry-child → has-geometry? true"
|
||||
(t/is (ctm/has-geometry? (ctm/move-modifiers (gpt/point 1 0)))))
|
||||
|
||||
(t/testing "move-parent in geometry-parent → has-geometry? true"
|
||||
(t/is (ctm/has-geometry? (ctm/move-parent (ctm/empty) (gpt/point 1 0)))))
|
||||
|
||||
(t/testing "only structure ops → has-geometry? false"
|
||||
(t/is (not (ctm/has-geometry? (ctm/reflow-modifiers))))))
|
||||
|
||||
(t/deftest predicate-has-structure?
|
||||
(t/testing "reflow op in structure-parent → has-structure? true"
|
||||
(t/is (ctm/has-structure? (ctm/reflow-modifiers))))
|
||||
|
||||
(t/testing "scale-content in structure-child → has-structure? true"
|
||||
(t/is (ctm/has-structure? (ctm/scale-content (ctm/empty) 2.0))))
|
||||
|
||||
(t/testing "only geometry ops → has-structure? false"
|
||||
(t/is (not (ctm/has-structure? (ctm/move-modifiers (gpt/point 1 0)))))))
|
||||
|
||||
(t/deftest predicate-has-structure-child?
|
||||
(t/testing "scale-content in structure-child → has-structure-child? true"
|
||||
(t/is (ctm/has-structure-child? (ctm/scale-content (ctm/empty) 2.0))))
|
||||
|
||||
(t/testing "reflow in structure-parent only → has-structure-child? false"
|
||||
(t/is (not (ctm/has-structure-child? (ctm/reflow-modifiers))))))
|
||||
|
||||
(t/deftest predicate-only-move?
|
||||
(t/testing "pure move modifiers → only-move? true"
|
||||
(t/is (ctm/only-move? (ctm/move-modifiers (gpt/point 1 0)))))
|
||||
|
||||
(t/testing "resize modifiers → only-move? false"
|
||||
(t/is (not (ctm/only-move? (ctm/resize-modifiers (gpt/point 2 2) (gpt/point 0 0))))))
|
||||
|
||||
(t/testing "structure ops present → only-move? false"
|
||||
(let [mods (-> (ctm/empty)
|
||||
(ctm/move (gpt/point 1 0))
|
||||
(ctm/reflow))]
|
||||
(t/is (not (ctm/only-move? mods)))))
|
||||
|
||||
(t/testing "empty modifiers → only-move? true (vacuously)"
|
||||
(t/is (ctm/only-move? (ctm/empty)))))
|
||||
|
||||
;; ─── Projection functions ──────────────────────────────────────────────────────
|
||||
|
||||
(t/deftest projection-select-child
|
||||
(t/testing "select-child keeps geometry-child and structure-child, clears parent"
|
||||
(let [mods (-> (ctm/empty)
|
||||
(ctm/move (gpt/point 1 0))
|
||||
(ctm/move-parent (gpt/point 2 0))
|
||||
(ctm/reflow)
|
||||
(ctm/scale-content 2.0))
|
||||
result (ctm/select-child mods)]
|
||||
(t/is (= 1 (count (:geometry-child result))))
|
||||
(t/is (= 1 (count (:structure-child result))))
|
||||
(t/is (empty? (:geometry-parent result)))
|
||||
(t/is (empty? (:structure-parent result))))))
|
||||
|
||||
(t/deftest projection-select-parent
|
||||
(t/testing "select-parent keeps geometry-parent and structure-parent, clears child"
|
||||
(let [mods (-> (ctm/empty)
|
||||
(ctm/move (gpt/point 1 0))
|
||||
(ctm/move-parent (gpt/point 2 0))
|
||||
(ctm/reflow)
|
||||
(ctm/scale-content 2.0))
|
||||
result (ctm/select-parent mods)]
|
||||
(t/is (= 1 (count (:geometry-parent result))))
|
||||
(t/is (= 1 (count (:structure-parent result))))
|
||||
(t/is (empty? (:geometry-child result)))
|
||||
(t/is (empty? (:structure-child result))))))
|
||||
|
||||
(t/deftest projection-select-geometry
|
||||
(t/testing "select-geometry keeps both geometry lists, clears structure"
|
||||
(let [mods (-> (ctm/empty)
|
||||
(ctm/move (gpt/point 1 0))
|
||||
(ctm/move-parent (gpt/point 2 0))
|
||||
(ctm/reflow)
|
||||
(ctm/scale-content 2.0))
|
||||
result (ctm/select-geometry mods)]
|
||||
(t/is (= 1 (count (:geometry-child result))))
|
||||
(t/is (= 1 (count (:geometry-parent result))))
|
||||
(t/is (empty? (:structure-child result)))
|
||||
(t/is (empty? (:structure-parent result))))))
|
||||
|
||||
(t/deftest projection-select-child-structre-modifiers
|
||||
(t/testing "select-child-structre-modifiers keeps only structure-child"
|
||||
(let [mods (-> (ctm/empty)
|
||||
(ctm/move (gpt/point 1 0))
|
||||
(ctm/move-parent (gpt/point 2 0))
|
||||
(ctm/reflow)
|
||||
(ctm/scale-content 2.0))
|
||||
result (ctm/select-child-structre-modifiers mods)]
|
||||
(t/is (= 1 (count (:structure-child result))))
|
||||
(t/is (empty? (:geometry-child result)))
|
||||
(t/is (empty? (:geometry-parent result)))
|
||||
(t/is (empty? (:structure-parent result))))))
|
||||
|
||||
;; ─── added-children-frames ────────────────────────────────────────────────────
|
||||
|
||||
(t/deftest added-children-frames-test
|
||||
(t/testing "returns frame/shape pairs for add-children operations"
|
||||
(let [frame-id (uuid/next)
|
||||
shape-id (uuid/next)
|
||||
mods (ctm/add-children (ctm/empty) [shape-id] nil)
|
||||
tree {frame-id {:modifiers mods}}
|
||||
result (ctm/added-children-frames tree)]
|
||||
(t/is (= 1 (count result)))
|
||||
(t/is (= frame-id (:frame (first result))))
|
||||
(t/is (= shape-id (:shape (first result))))))
|
||||
|
||||
(t/testing "returns empty when there are no add-children operations"
|
||||
(let [frame-id (uuid/next)
|
||||
mods (ctm/reflow-modifiers)
|
||||
tree {frame-id {:modifiers mods}}
|
||||
result (ctm/added-children-frames tree)]
|
||||
(t/is (empty? result))))
|
||||
|
||||
(t/testing "returns empty for an empty modif-tree"
|
||||
(t/is (empty? (ctm/added-children-frames {})))))
|
||||
|
||||
;; ─── apply-modifier and apply-structure-modifiers ─────────────────────────────
|
||||
|
||||
(t/deftest apply-modifier-test
|
||||
(t/testing "rotation op increments shape :rotation field"
|
||||
(let [shape (make-shape 100 50)
|
||||
op (-> (ctm/rotation (ctm/empty) (gpt/point 50 25) 90)
|
||||
:structure-child
|
||||
first)
|
||||
result (ctm/apply-modifier shape op)]
|
||||
(t/is (mth/close? 90.0 (:rotation result)))))
|
||||
|
||||
(t/testing "rotation wraps around 360"
|
||||
(let [shape (assoc (make-shape 100 50) :rotation 350)
|
||||
op (-> (ctm/rotation (ctm/empty) (gpt/point 50 25) 20)
|
||||
:structure-child
|
||||
first)
|
||||
result (ctm/apply-modifier shape op)]
|
||||
(t/is (mth/close? 10.0 (:rotation result)))))
|
||||
|
||||
(t/testing "add-children op appends ids to shape :shapes"
|
||||
(let [id1 (uuid/next)
|
||||
id2 (uuid/next)
|
||||
shape (assoc (make-shape 100 50) :shapes [])
|
||||
op (-> (ctm/add-children (ctm/empty) [id1 id2] nil)
|
||||
:structure-parent
|
||||
first)
|
||||
result (ctm/apply-modifier shape op)]
|
||||
(t/is (= [id1 id2] (:shapes result)))))
|
||||
|
||||
(t/testing "add-children op with index inserts at the given position"
|
||||
(let [id-existing (uuid/next)
|
||||
id-new (uuid/next)
|
||||
shape (assoc (make-shape 100 50) :shapes [id-existing])
|
||||
op (-> (ctm/add-children (ctm/empty) [id-new] 0)
|
||||
:structure-parent
|
||||
first)
|
||||
result (ctm/apply-modifier shape op)]
|
||||
(t/is (= id-new (first (:shapes result))))))
|
||||
|
||||
(t/testing "remove-children op removes given ids from shape :shapes"
|
||||
(let [id1 (uuid/next)
|
||||
id2 (uuid/next)
|
||||
shape (assoc (make-shape 100 50) :shapes [id1 id2])
|
||||
op (-> (ctm/remove-children (ctm/empty) [id1])
|
||||
:structure-parent
|
||||
first)
|
||||
result (ctm/apply-modifier shape op)]
|
||||
(t/is (= [id2] (:shapes result)))))
|
||||
|
||||
(t/testing "change-property op sets the property on the shape"
|
||||
(let [shape (make-shape 100 50)
|
||||
op (-> (ctm/change-property (ctm/empty) :opacity 0.5)
|
||||
:structure-parent
|
||||
first)
|
||||
result (ctm/apply-modifier shape op)]
|
||||
(t/is (= 0.5 (:opacity result)))))
|
||||
|
||||
(t/testing "unknown op type returns shape unchanged"
|
||||
(let [shape (make-shape 100 50)
|
||||
result (ctm/apply-modifier shape {:type :unknown})]
|
||||
(t/is (= shape result)))))
|
||||
|
||||
(t/deftest apply-structure-modifiers-test
|
||||
(t/testing "applies structure-parent and structure-child ops in order"
|
||||
(let [id (uuid/next)
|
||||
shape (assoc (make-shape 100 50) :shapes [])
|
||||
mods (-> (ctm/empty)
|
||||
(ctm/add-children [id] nil)
|
||||
(ctm/scale-content 1.0))
|
||||
result (ctm/apply-structure-modifiers shape mods)]
|
||||
(t/is (= [id] (:shapes result)))))
|
||||
|
||||
(t/testing "empty modifiers returns shape unchanged"
|
||||
(let [shape (make-shape 100 50)
|
||||
result (ctm/apply-structure-modifiers shape (ctm/empty))]
|
||||
(t/is (= shape result))))
|
||||
|
||||
(t/testing "change-property in structure-parent is applied"
|
||||
(let [shape (make-shape 100 50)
|
||||
mods (ctm/change-property (ctm/empty) :opacity 0.3)
|
||||
result (ctm/apply-structure-modifiers shape mods)]
|
||||
(t/is (= 0.3 (:opacity result)))))
|
||||
|
||||
(t/testing "rotation in structure-child is applied"
|
||||
(let [shape (make-shape 100 50)
|
||||
mods (ctm/rotation (ctm/empty) (gpt/point 50 25) 45)
|
||||
result (ctm/apply-structure-modifiers shape mods)]
|
||||
(t/is (mth/close? 45.0 (:rotation result))))))
|
||||
|
||||
1475
common/test/common_tests/types/shape_layout_test.cljc
Normal file
1475
common/test/common_tests/types/shape_layout_test.cljc
Normal file
File diff suppressed because it is too large
Load Diff
445
common/test/common_tests/undo_stack_test.cljc
Normal file
445
common/test/common_tests/undo_stack_test.cljc
Normal file
@@ -0,0 +1,445 @@
|
||||
;; 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 common-tests.undo-stack-test
|
||||
(:require
|
||||
[app.common.data.undo-stack :as sut]
|
||||
[clojure.test :as t]))
|
||||
|
||||
;; --- make-stack ---
|
||||
|
||||
(t/deftest make-stack-creates-empty-stack
|
||||
(let [stack (sut/make-stack)]
|
||||
(t/is (= -1 (:index stack)))
|
||||
(t/is (= [] (:items stack)))))
|
||||
|
||||
(t/deftest make-stack-returns-nil-on-peek
|
||||
(t/is (nil? (sut/peek (sut/make-stack)))))
|
||||
|
||||
(t/deftest make-stack-size-is-zero
|
||||
(t/is (= 0 (sut/size (sut/make-stack)))))
|
||||
|
||||
;; --- peek ---
|
||||
|
||||
(t/deftest peek-empty-stack
|
||||
(t/is (nil? (sut/peek (sut/make-stack)))))
|
||||
|
||||
(t/deftest peek-after-append
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a))]
|
||||
(t/is (= :a (sut/peek stack)))))
|
||||
|
||||
(t/deftest peek-multiple-items
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/append :c))]
|
||||
(t/is (= :c (sut/peek stack)))))
|
||||
|
||||
(t/deftest peek-after-undo
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/undo))]
|
||||
(t/is (= :a (sut/peek stack)))))
|
||||
|
||||
;; --- append ---
|
||||
|
||||
(t/deftest append-to-nil-stack
|
||||
(t/is (nil? (sut/append nil :a))))
|
||||
|
||||
(t/deftest append-single-item
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a))]
|
||||
(t/is (= 0 (:index stack)))
|
||||
(t/is (= [:a] (:items stack)))))
|
||||
|
||||
(t/deftest append-multiple-items
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/append :c))]
|
||||
(t/is (= 2 (:index stack)))
|
||||
(t/is (= [:a :b :c] (:items stack)))))
|
||||
|
||||
(t/deftest append-duplicate-at-current-index-ignored
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :a))]
|
||||
(t/is (= 0 (:index stack)))
|
||||
(t/is (= [:a] (:items stack)))))
|
||||
|
||||
(t/deftest append-duplicate-after-other-operations
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/undo)
|
||||
(sut/append :a))]
|
||||
;; appending :a when current is :a should be a no-op
|
||||
(t/is (= 0 (:index stack)))
|
||||
(t/is (= [:a :b] (:items stack)))))
|
||||
|
||||
(t/deftest append-same-value-at-different-positions-allowed
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/append :a))]
|
||||
(t/is (= 2 (:index stack)))
|
||||
(t/is (= [:a :b :a] (:items stack)))))
|
||||
|
||||
(t/deftest append-nil-value-returns-unchanged
|
||||
;; appending nil when peek is nil returns stack unchanged
|
||||
(let [stack (sut/make-stack)
|
||||
result (sut/append stack nil)]
|
||||
(t/is (= stack result))))
|
||||
|
||||
(t/deftest append-complex-values
|
||||
(let [v1 {:id 1 :name "shape"}
|
||||
v2 {:id 2 :name "rect"}
|
||||
stack (-> (sut/make-stack)
|
||||
(sut/append v1)
|
||||
(sut/append v2))]
|
||||
(t/is (= 1 (:index stack)))
|
||||
(t/is (= v2 (sut/peek stack)))))
|
||||
|
||||
;; --- append truncates redo history ---
|
||||
|
||||
(t/deftest append-truncates-redo-history-at-index-greater-than-zero
|
||||
;; Truncation only happens when index > 0
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/append :c)
|
||||
(sut/append :d)
|
||||
(sut/undo) ;; index -> 2, peek :c
|
||||
(sut/append :e))] ;; index > 0, truncates :d
|
||||
(t/is (= 3 (:index stack)))
|
||||
(t/is (= [:a :b :c :e] (:items stack)))
|
||||
(t/is (= :e (sut/peek stack)))))
|
||||
|
||||
(t/deftest append-at-index-zero-does-not-truncate
|
||||
;; When index is 0, append just adds to end without truncation
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/append :c)
|
||||
(sut/undo) ;; index -> 1
|
||||
(sut/undo) ;; index -> 0
|
||||
(sut/append :d))]
|
||||
;; index 0, no truncation: items become [:a :b :c :d]
|
||||
(t/is (= 1 (:index stack)))
|
||||
(t/is (= [:a :b :c :d] (:items stack)))
|
||||
(t/is (= :b (sut/peek stack)))))
|
||||
|
||||
(t/deftest append-truncates-multiple-redo-items
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/append :c)
|
||||
(sut/append :d)
|
||||
(sut/append :e)
|
||||
(sut/undo) ;; index -> 3, peek :d
|
||||
(sut/undo) ;; index -> 2, peek :c
|
||||
(sut/append :x))]
|
||||
(t/is (= 3 (:index stack)))
|
||||
(t/is (= [:a :b :c :x] (:items stack)))))
|
||||
|
||||
;; --- append respects MAX-UNDO-SIZE ---
|
||||
|
||||
(t/deftest append-max-undo-size-boundary
|
||||
(let [;; Fill stack to MAX-UNDO-SIZE items
|
||||
stack (reduce (fn [s i] (sut/append s (str "item-" i)))
|
||||
(sut/make-stack)
|
||||
(range sut/MAX-UNDO-SIZE))]
|
||||
(t/is (= (dec sut/MAX-UNDO-SIZE) (:index stack)))
|
||||
(t/is (= sut/MAX-UNDO-SIZE (count (:items stack))))
|
||||
(t/is (= "item-0" (first (:items stack))))
|
||||
(t/is (= (str "item-" (dec sut/MAX-UNDO-SIZE)) (sut/peek stack)))))
|
||||
|
||||
(t/deftest append-exceeds-max-undo-size-removes-oldest
|
||||
(let [;; Fill stack to MAX-UNDO-SIZE + 1
|
||||
stack (reduce (fn [s i] (sut/append s (str "item-" i)))
|
||||
(sut/make-stack)
|
||||
(range (inc sut/MAX-UNDO-SIZE)))]
|
||||
(t/is (= (dec sut/MAX-UNDO-SIZE) (:index stack)))
|
||||
(t/is (= sut/MAX-UNDO-SIZE (count (:items stack))))
|
||||
;; Oldest item was removed
|
||||
(t/is (= "item-1" (first (:items stack))))
|
||||
(t/is (= (str "item-" sut/MAX-UNDO-SIZE) (sut/peek stack)))))
|
||||
|
||||
(t/deftest append-far-exceeds-max-undo-size
|
||||
(let [;; Fill stack to MAX-UNDO-SIZE + 10
|
||||
stack (reduce (fn [s i] (sut/append s (str "item-" i)))
|
||||
(sut/make-stack)
|
||||
(range (+ sut/MAX-UNDO-SIZE 10)))]
|
||||
(t/is (= (dec sut/MAX-UNDO-SIZE) (:index stack)))
|
||||
(t/is (= sut/MAX-UNDO-SIZE (count (:items stack))))
|
||||
(t/is (= "item-10" (first (:items stack))))))
|
||||
|
||||
;; --- fixup ---
|
||||
|
||||
(t/deftest fixup-updates-current-item
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/fixup :a-updated))]
|
||||
(t/is (= :a-updated (sut/peek stack)))
|
||||
(t/is (= 0 (:index stack)))))
|
||||
|
||||
(t/deftest fixup-at-middle-of-stack
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/append :c)
|
||||
(sut/undo) ;; index -> 1
|
||||
(sut/fixup :b-updated))]
|
||||
(t/is (= :b-updated (sut/peek stack)))
|
||||
(t/is (= [:a :b-updated :c] (:items stack)))))
|
||||
|
||||
(t/deftest fixup-preserves-index
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/undo) ;; index -> 0
|
||||
(sut/fixup :a-new))]
|
||||
(t/is (= 0 (:index stack)))
|
||||
(t/is (= :a-new (sut/peek stack)))))
|
||||
|
||||
;; --- undo ---
|
||||
|
||||
(t/deftest undo-single-item
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/undo))]
|
||||
(t/is (= 0 (:index stack)))
|
||||
(t/is (= :a (sut/peek stack)))))
|
||||
|
||||
(t/deftest undo-clamps-to-zero
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/undo)
|
||||
(sut/undo))]
|
||||
(t/is (= 0 (:index stack)))
|
||||
(t/is (= :a (sut/peek stack)))))
|
||||
|
||||
(t/deftest undo-multiple-steps
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/append :c)
|
||||
(sut/undo)
|
||||
(sut/undo))]
|
||||
(t/is (= 0 (:index stack)))
|
||||
(t/is (= :a (sut/peek stack)))))
|
||||
|
||||
(t/deftest undo-on-empty-stack
|
||||
;; undo on empty stack clamps index to 0 (from -1, dec gives -2, max with 0 gives 0)
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/undo))]
|
||||
(t/is (= 0 (:index stack)))
|
||||
(t/is (nil? (sut/peek stack)))))
|
||||
|
||||
(t/deftest undo-preserves-items
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/undo))]
|
||||
(t/is (= [:a :b] (:items stack)))))
|
||||
|
||||
;; --- redo ---
|
||||
|
||||
(t/deftest redo-after-undo
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/undo)
|
||||
(sut/redo))]
|
||||
(t/is (= 1 (:index stack)))
|
||||
(t/is (= :b (sut/peek stack)))))
|
||||
|
||||
(t/deftest redo-at-end-of-stack-no-op
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/redo))]
|
||||
(t/is (= 1 (:index stack)))
|
||||
(t/is (= :b (sut/peek stack)))))
|
||||
|
||||
(t/deftest redo-multiple-steps
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/append :c)
|
||||
(sut/undo) ;; index -> 1
|
||||
(sut/undo) ;; index -> 0
|
||||
(sut/redo) ;; index -> 1
|
||||
(sut/redo))] ;; index -> 2
|
||||
(t/is (= 2 (:index stack)))
|
||||
(t/is (= :c (sut/peek stack)))))
|
||||
|
||||
(t/deftest redo-on-empty-stack
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/redo))]
|
||||
(t/is (= -1 (:index stack)))))
|
||||
|
||||
(t/deftest redo-not-available-after-truncating-append
|
||||
;; When index > 0, append truncates redo history
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/append :c)
|
||||
(sut/undo) ;; index -> 1
|
||||
(sut/append :x) ;; truncates :c, items [:a :b :x]
|
||||
(sut/redo))]
|
||||
;; Redo should not work since redo history was truncated
|
||||
(t/is (= 2 (:index stack)))
|
||||
(t/is (= :x (sut/peek stack)))))
|
||||
|
||||
(t/deftest redo-after-append-at-index-zero
|
||||
;; When index is 0, append does not truncate, so old items remain
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/append :c)
|
||||
(sut/undo) ;; index -> 1
|
||||
(sut/undo) ;; index -> 0
|
||||
(sut/append :x) ;; index 0, no truncation: [:a :b :c :x]
|
||||
(sut/redo))]
|
||||
;; redo goes from index 1 to 2, which is :c
|
||||
(t/is (= 2 (:index stack)))
|
||||
(t/is (= :c (sut/peek stack)))))
|
||||
|
||||
;; --- size ---
|
||||
|
||||
(t/deftest size-empty-stack
|
||||
(t/is (= 0 (sut/size (sut/make-stack)))))
|
||||
|
||||
(t/deftest size-after-appends
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/append :c))]
|
||||
(t/is (= 3 (sut/size stack)))))
|
||||
|
||||
(t/deftest size-unchanged-after-undo
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/append :c)
|
||||
(sut/undo))]
|
||||
;; size returns (inc index), not count of items
|
||||
(t/is (= 2 (sut/size stack)))))
|
||||
|
||||
(t/deftest size-is-undo-position
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/append :c)
|
||||
(sut/undo)
|
||||
(sut/undo))]
|
||||
(t/is (= 1 (sut/size stack)))))
|
||||
|
||||
;; --- undo/redo round-trip ---
|
||||
|
||||
(t/deftest undo-redo-round-trip
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/append :c))]
|
||||
(t/is (= :c (sut/peek stack)))
|
||||
(let [s (sut/undo stack)]
|
||||
(t/is (= :b (sut/peek s)))
|
||||
(let [s (sut/undo s)]
|
||||
(t/is (= :a (sut/peek s)))
|
||||
(let [s (sut/undo s)]
|
||||
;; At index 0, undo should clamp
|
||||
(t/is (= :a (sut/peek s)))
|
||||
(t/is (= 0 (:index s)))
|
||||
(let [s (sut/redo s)]
|
||||
(t/is (= :b (sut/peek s)))
|
||||
(let [s (sut/redo s)]
|
||||
(t/is (= :c (sut/peek s)))
|
||||
(let [s (sut/redo s)]
|
||||
;; At end, redo should be no-op
|
||||
(t/is (= :c (sut/peek s)))))))))))
|
||||
|
||||
;; --- mixed operations ---
|
||||
|
||||
(t/deftest append-after-undo-then-redo
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/undo)
|
||||
(sut/undo)
|
||||
(sut/redo)
|
||||
(sut/append :c))]
|
||||
(t/is (= 2 (:index stack)))
|
||||
(t/is (= [:a :b :c] (:items stack)))
|
||||
(t/is (= :c (sut/peek stack)))))
|
||||
|
||||
(t/deftest fixup-then-append
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/fixup :a-fixed)
|
||||
(sut/append :b))]
|
||||
(t/is (= [:a-fixed :b] (:items stack)))
|
||||
(t/is (= :b (sut/peek stack)))))
|
||||
|
||||
(t/deftest append-identical-different-objects
|
||||
(let [m1 {:x 1}
|
||||
m2 {:x 1}
|
||||
stack (-> (sut/make-stack)
|
||||
(sut/append m1)
|
||||
(sut/append m2))]
|
||||
;; Maps are equal, so second append should be a no-op
|
||||
(t/is (= 0 (:index stack)))
|
||||
(t/is (= [m1] (:items stack)))))
|
||||
|
||||
(t/deftest append-maps-not-equal
|
||||
(let [m1 {:x 1}
|
||||
m2 {:x 2}
|
||||
stack (-> (sut/make-stack)
|
||||
(sut/append m1)
|
||||
(sut/append m2))]
|
||||
(t/is (= 1 (:index stack)))
|
||||
(t/is (= [m1 m2] (:items stack)))))
|
||||
|
||||
(t/deftest sequential-fixup-and-undo
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append {:id 1 :val "old"})
|
||||
(sut/append {:id 2 :val "old"})
|
||||
(sut/fixup {:id 2 :val "new"})
|
||||
(sut/undo)
|
||||
(sut/fixup {:id 1 :val "updated"}))]
|
||||
(t/is (= {:id 1 :val "updated"} (sut/peek stack)))
|
||||
(t/is (= [{:id 1 :val "updated"} {:id 2 :val "new"}] (:items stack)))))
|
||||
|
||||
(t/deftest append-undo-append-undo-cycle
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/append :c) ;; index 2
|
||||
(sut/undo) ;; index 1, peek :b
|
||||
(sut/append :d) ;; index 1 > 0, truncates :c, items [:a :b :d]
|
||||
(sut/append :e) ;; items [:a :b :d :e]
|
||||
(sut/undo) ;; index 2, peek :d
|
||||
(sut/undo))] ;; index 1, peek :b
|
||||
(t/is (= 1 (:index stack)))
|
||||
(t/is (= :b (sut/peek stack)))
|
||||
(t/is (= [:a :b :d :e] (:items stack)))))
|
||||
|
||||
(t/deftest size-grows-then-shrinks-with-undo
|
||||
(let [stack (-> (sut/make-stack)
|
||||
(sut/append :a)
|
||||
(sut/append :b)
|
||||
(sut/append :c)
|
||||
(sut/append :d))]
|
||||
(t/is (= 4 (sut/size stack)))
|
||||
(let [s (sut/undo stack)]
|
||||
(t/is (= 3 (sut/size s)))
|
||||
(let [s (sut/undo s)]
|
||||
(t/is (= 2 (sut/size s)))
|
||||
(let [s (sut/redo s)]
|
||||
(t/is (= 3 (sut/size s))))))))
|
||||
@@ -9,6 +9,10 @@ localhost:3449 {
|
||||
}
|
||||
|
||||
http://localhost:3450 {
|
||||
# For subpath test
|
||||
# handle_path /penpot/* {
|
||||
# reverse_proxy localhost:4449
|
||||
# }
|
||||
reverse_proxy localhost:4449
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,6 @@ architectural pieces.
|
||||
|
||||
## General Guidelines
|
||||
|
||||
To ensure consistency across the Penpot stack, all contributions must adhere to
|
||||
these criteria:
|
||||
|
||||
|
||||
### 1. Testing & Validation
|
||||
|
||||
#### Unit Tests
|
||||
@@ -71,6 +67,29 @@ Ensure everything is installed before executing tests with the `./scripts/setup`
|
||||
function in the same namespace if it is only used locally, or look for a helper
|
||||
namespace to make it unit-testable.
|
||||
|
||||
### 4. Stack Trace Analysis
|
||||
|
||||
When analyzing production stack traces (minified code), you can generate a
|
||||
production bundle locally to map the minified code back to the source.
|
||||
|
||||
**To build the production bundle:**
|
||||
|
||||
Run: `pnpm run build:app`
|
||||
|
||||
The compiled files and their corresponding source maps will be generated in
|
||||
`resources/public/js`.
|
||||
|
||||
**Analysis Tips:**
|
||||
|
||||
- **Source Maps:** Use the `.map` files generated in `resources/public/js` with
|
||||
tools like `source-map-lookup` or browser dev tools to resolve minified
|
||||
locations.
|
||||
- **Bundle Inspection:** If the issue is related to bundle size or unexpected
|
||||
code inclusion, inspect the generated modules in `resources/public/js`.
|
||||
- **Shadow-CLJS Reports:** For more detailed analysis of what is included in the
|
||||
bundle, you can run shadow-cljs build reports (consult `shadow-cljs.edn` for
|
||||
build IDs like `main` or `worker`).
|
||||
|
||||
|
||||
## Code Conventions
|
||||
|
||||
|
||||
@@ -239,12 +239,11 @@
|
||||
([media]
|
||||
(resolve-file-media media false))
|
||||
([{:keys [id data-uri] :as media} thumbnail?]
|
||||
(if data-uri
|
||||
data-uri
|
||||
(dm/str
|
||||
(cond-> (u/join public-uri "assets/by-file-media-id/")
|
||||
(true? thumbnail?) (u/join (dm/str id "/thumbnail"))
|
||||
(false? thumbnail?) (u/join (dm/str id)))))))
|
||||
(or data-uri
|
||||
(dm/str
|
||||
(cond-> (u/join public-uri "assets/by-file-media-id/")
|
||||
(true? thumbnail?) (u/join (dm/str id "/thumbnail"))
|
||||
(false? thumbnail?) (u/join (dm/str id)))))))
|
||||
|
||||
(defn resolve-href
|
||||
[resource]
|
||||
|
||||
@@ -623,27 +623,30 @@
|
||||
(assoc-in [:workspace-global :default-font] data))))))
|
||||
|
||||
(defn apply-text-modifier
|
||||
[shape {:keys [width height position-data]}]
|
||||
[shape text-modifier]
|
||||
|
||||
(let [new-shape
|
||||
(cond-> shape
|
||||
(some? width)
|
||||
(gsh/transform-shape (ctm/change-dimensions-modifiers shape :width width {:ignore-lock? true}))
|
||||
(if (some? text-modifier)
|
||||
(let [{:keys [width height position-data]} text-modifier
|
||||
new-shape
|
||||
(cond-> shape
|
||||
(some? width)
|
||||
(gsh/transform-shape (ctm/change-dimensions-modifiers shape :width width {:ignore-lock? true}))
|
||||
|
||||
(some? height)
|
||||
(gsh/transform-shape (ctm/change-dimensions-modifiers shape :height height {:ignore-lock? true}))
|
||||
(some? height)
|
||||
(gsh/transform-shape (ctm/change-dimensions-modifiers shape :height height {:ignore-lock? true}))
|
||||
|
||||
(some? position-data)
|
||||
(assoc :position-data position-data))
|
||||
(some? position-data)
|
||||
(assoc :position-data position-data))
|
||||
|
||||
delta-move
|
||||
(gpt/subtract (gpt/point (:selrect new-shape))
|
||||
(gpt/point (:selrect shape)))
|
||||
delta-move
|
||||
(gpt/subtract (gpt/point (ctm/safe-size-rect new-shape))
|
||||
(gpt/point (ctm/safe-size-rect shape)))
|
||||
|
||||
new-shape
|
||||
(update new-shape :position-data gsh/move-position-data delta-move)]
|
||||
new-shape
|
||||
(update new-shape :position-data gsh/move-position-data delta-move)]
|
||||
|
||||
new-shape))
|
||||
new-shape)
|
||||
shape))
|
||||
|
||||
(defn commit-update-text-modifier
|
||||
[]
|
||||
|
||||
@@ -395,10 +395,11 @@
|
||||
(= message "Unexpected end of input")
|
||||
(str/starts-with? message "invalid props on component")
|
||||
(str/starts-with? message "Unexpected token ")
|
||||
;; Abort errors are expected when an in-flight HTTP request is
|
||||
;; cancelled (e.g. via RxJS unsubscription / take-until). They
|
||||
;; are handled gracefully inside app.util.http/fetch and must
|
||||
;; NOT be surfaced as application errors.
|
||||
;; Native AbortError DOMException: raised when an in-flight
|
||||
;; HTTP fetch is cancelled via AbortController (e.g. by an
|
||||
;; RxJS unsubscription / take-until chain). These are
|
||||
;; handled gracefully inside app.util.http/fetch and must NOT
|
||||
;; be surfaced as application errors.
|
||||
(= (.-name ^js cause) "AbortError"))))
|
||||
|
||||
(on-unhandled-error [event]
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
:code :challenge-required}))
|
||||
|
||||
(and (>= status 400) (map? body))
|
||||
(rx/throw (ex-info "http error" body))
|
||||
(rx/throw (ex-info "http error" (assoc body :uri uri :status status)))
|
||||
|
||||
:else
|
||||
(rx/throw
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.main.data.common :as dcm]
|
||||
[app.main.data.dashboard :as dd]
|
||||
@@ -95,8 +96,8 @@
|
||||
(mf/defc card-item
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [item index is-visible collapsed on-import]}]
|
||||
(let [id (dm/str "card-container-" index)
|
||||
thb (assoc cf/public-uri :path (dm/str "/images/thumbnails/template-" (:id item) ".jpg"))
|
||||
(let [id (dm/str "card-container-" index)
|
||||
href (u/join cf/public-uri (dm/str "images/thumbnails/template-" (:id item) ".jpg"))
|
||||
hover? (mf/use-state false)
|
||||
|
||||
on-click
|
||||
@@ -124,7 +125,7 @@
|
||||
:on-mouse-leave #(reset! hover? false)
|
||||
:on-key-down on-key-down}
|
||||
[:div {:class (stl/css :img-container)}
|
||||
[:img {:src (dm/str thb)
|
||||
[:img {:src (dm/str href)
|
||||
:alt (:name item)
|
||||
:loading "lazy"
|
||||
:decoding "async"}]]
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
text-modifier
|
||||
(mf/deref text-modifier-ref)
|
||||
|
||||
shape (if (some? shape)
|
||||
shape (if (and (some? shape) (some? text-modifier))
|
||||
(dwt/apply-text-modifier shape text-modifier)
|
||||
shape)]
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
(defn- google-font-id->uuid
|
||||
"Returns the UUID for a Google Font ID. Uses uuid/zero as fallback when the
|
||||
font is not found in fontsdb. uuid/zero maps to the default font (Source
|
||||
font is not found in fontsdb. uuid/zero maps to the default font (Source
|
||||
Sans Pro) in WASM.
|
||||
A font id may not exist for different reasons:
|
||||
- the gfonts.json catalog was updated and fonts were renamed or removed,
|
||||
@@ -158,7 +158,7 @@
|
||||
[font-id font-variant-id font-weight font-style]
|
||||
(let [variant (font-db-data font-id font-variant-id font-weight font-style)]
|
||||
(if-let [ttf-url (:ttf-url variant)]
|
||||
(str/replace ttf-url "https://fonts.gstatic.com/s/" (u/join cf/public-uri "/internal/gfonts/font/"))
|
||||
(str/replace ttf-url "https://fonts.gstatic.com/s/" (u/join cf/public-uri "internal/gfonts/font/"))
|
||||
nil)))
|
||||
|
||||
(defn- font-id->ttf-url
|
||||
|
||||
@@ -126,11 +126,17 @@
|
||||
(fn []
|
||||
(vreset! unsubscribed? true)
|
||||
(when @abortable?
|
||||
;; Provide an explicit reason so that the resulting AbortError carries
|
||||
;; a meaningful message instead of the browser default
|
||||
;; "signal is aborted without reason".
|
||||
(.abort ^js controller (ex-info (str "fetch to '" uri "' is aborted")
|
||||
{:uri uri}))))))))
|
||||
;; Do NOT pass a custom reason to .abort(): browsers that support
|
||||
;; AbortController reason (Chrome 98+, Firefox 97+) would reject
|
||||
;; the fetch promise with the supplied value directly. When that
|
||||
;; value is a ClojureScript ExceptionInfo its `.name` property is
|
||||
;; "Error", not "AbortError", which defeats every existing guard
|
||||
;; that checks `(= (.-name cause) "AbortError")`. Calling .abort
|
||||
;; without a reason always produces a native DOMException whose
|
||||
;; `.name` is "AbortError", which is correctly recognised and
|
||||
;; suppressed by both the p/catch handler and the global
|
||||
;; unhandled-exception filter.
|
||||
(.abort ^js controller)))))))
|
||||
|
||||
(defn response->map
|
||||
[response]
|
||||
|
||||
274
frontend/test/frontend_tests/data/workspace_texts_test.cljs
Normal file
274
frontend/test/frontend_tests/data/workspace_texts_test.cljs
Normal file
@@ -0,0 +1,274 @@
|
||||
;; 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 frontend-tests.data.workspace-texts-test
|
||||
(:require
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Helpers
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defn- make-text-shape
|
||||
"Build a fully initialised text shape at the given position."
|
||||
[& {:keys [x y width height position-data]
|
||||
:or {x 10 y 20 width 100 height 50}}]
|
||||
(cond-> (cts/setup-shape {:type :text
|
||||
:x x
|
||||
:y y
|
||||
:width width
|
||||
:height height})
|
||||
(some? position-data)
|
||||
(assoc :position-data position-data)))
|
||||
|
||||
(defn- make-degenerate-text-shape
|
||||
"Simulate a text shape decoded from the server via map->Rect (which bypasses
|
||||
make-rect's 0.01 minimum enforcement), giving it a zero-width / zero-height
|
||||
selrect. This is the exact condition that triggered the original crash:
|
||||
change-dimensions-modifiers divided by sr-width (== 0), producing an Infinity
|
||||
scale factor that propagated through the transform pipeline until
|
||||
calculate-selrect / center->rect returned nil, and then gpt/point threw
|
||||
'invalid arguments (on pointer constructor)'."
|
||||
[& {:keys [x y width height]
|
||||
:or {x 10 y 20 width 0 height 0}}]
|
||||
(-> (make-text-shape :x x :y y :width 100 :height 50)
|
||||
;; Bypass make-rect by constructing the Rect record directly, the same
|
||||
;; way decode-rect does during JSON deserialization from the backend.
|
||||
(assoc :selrect (grc/map->Rect {:x x :y y
|
||||
:width width :height height
|
||||
:x1 x :y1 y
|
||||
:x2 (+ x width) :y2 (+ y height)}))))
|
||||
|
||||
(defn- sample-position-data
|
||||
"Return a minimal position-data vector with the supplied coords."
|
||||
[x y]
|
||||
[{:x x :y y :width 80 :height 16 :fills [] :text "hello"}])
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Tests: nil / no-op guard
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest apply-text-modifier-nil-modifier-returns-shape-unchanged
|
||||
(t/testing "nil text-modifier returns the original shape untouched"
|
||||
(let [shape (make-text-shape)
|
||||
result (dwt/apply-text-modifier shape nil)]
|
||||
(t/is (= shape result)))))
|
||||
|
||||
(t/deftest apply-text-modifier-empty-map-no-keys-returns-shape-unchanged
|
||||
(t/testing "modifier with no recognised keys leaves shape unchanged"
|
||||
(let [shape (make-text-shape)
|
||||
modifier {}
|
||||
result (dwt/apply-text-modifier shape modifier)]
|
||||
(t/is (= (:selrect shape) (:selrect result)))
|
||||
(t/is (= (:width result) (:width shape)))
|
||||
(t/is (= (:height result) (:height shape))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Tests: width modifier
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest apply-text-modifier-width-changes-shape-width
|
||||
(t/testing "width modifier resizes the shape width"
|
||||
(let [shape (make-text-shape :x 0 :y 0 :width 100 :height 50)
|
||||
modifier {:width 200}
|
||||
result (dwt/apply-text-modifier shape modifier)]
|
||||
(t/is (= 200.0 (-> result :selrect :width))))))
|
||||
|
||||
(t/deftest apply-text-modifier-width-nil-skips-width-change
|
||||
(t/testing "nil :width in modifier does not alter the width"
|
||||
(let [shape (make-text-shape :x 0 :y 0 :width 100 :height 50)
|
||||
modifier {:width nil}
|
||||
result (dwt/apply-text-modifier shape modifier)]
|
||||
(t/is (= (-> shape :selrect :width) (-> result :selrect :width))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Tests: height modifier
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest apply-text-modifier-height-changes-shape-height
|
||||
(t/testing "height modifier resizes the shape height"
|
||||
(let [shape (make-text-shape :x 0 :y 0 :width 100 :height 50)
|
||||
modifier {:height 120}
|
||||
result (dwt/apply-text-modifier shape modifier)]
|
||||
(t/is (= 120.0 (-> result :selrect :height))))))
|
||||
|
||||
(t/deftest apply-text-modifier-height-nil-skips-height-change
|
||||
(t/testing "nil :height in modifier does not alter the height"
|
||||
(let [shape (make-text-shape :x 0 :y 0 :width 100 :height 50)
|
||||
modifier {:height nil}
|
||||
result (dwt/apply-text-modifier shape modifier)]
|
||||
(t/is (= (-> shape :selrect :height) (-> result :selrect :height))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Tests: width + height together
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest apply-text-modifier-width-and-height-both-applied
|
||||
(t/testing "both width and height are applied simultaneously"
|
||||
(let [shape (make-text-shape :x 0 :y 0 :width 100 :height 50)
|
||||
modifier {:width 300 :height 80}
|
||||
result (dwt/apply-text-modifier shape modifier)]
|
||||
(t/is (= 300.0 (-> result :selrect :width)))
|
||||
(t/is (= 80.0 (-> result :selrect :height))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Tests: position-data modifier
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest apply-text-modifier-position-data-is-set-on-shape
|
||||
(t/testing "position-data modifier replaces the position-data on shape"
|
||||
(let [pd (sample-position-data 5 10)
|
||||
shape (make-text-shape :x 0 :y 0 :width 100 :height 50)
|
||||
modifier {:position-data pd}
|
||||
result (dwt/apply-text-modifier shape modifier)]
|
||||
(t/is (some? (:position-data result))))))
|
||||
|
||||
(t/deftest apply-text-modifier-position-data-nil-leaves-position-data-unchanged
|
||||
(t/testing "nil :position-data in modifier does not alter position-data"
|
||||
(let [pd (sample-position-data 5 10)
|
||||
shape (-> (make-text-shape :x 0 :y 0 :width 100 :height 50)
|
||||
(assoc :position-data pd))
|
||||
modifier {:position-data nil}
|
||||
result (dwt/apply-text-modifier shape modifier)]
|
||||
(t/is (= pd (:position-data result))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Tests: position-data is translated by delta when shape moves
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest apply-text-modifier-position-data-translated-on-resize
|
||||
(t/testing "position-data x/y is adjusted by the delta of the selrect origin"
|
||||
(let [pd (sample-position-data 10 20)
|
||||
shape (-> (make-text-shape :x 0 :y 0 :width 100 :height 50)
|
||||
(assoc :position-data pd))
|
||||
;; Only set position-data; no resize so no origin shift expected
|
||||
modifier {:position-data pd}
|
||||
result (dwt/apply-text-modifier shape modifier)]
|
||||
;; Delta should be zero (no dimension change), so coords stay the same
|
||||
(t/is (= 10.0 (-> result :position-data first :x)))
|
||||
(t/is (= 20.0 (-> result :position-data first :y))))))
|
||||
|
||||
(t/deftest apply-text-modifier-position-data-not-translated-when-nil
|
||||
(t/testing "nil position-data on result after modifier is left as nil"
|
||||
(let [shape (make-text-shape :x 0 :y 0 :width 100 :height 50)
|
||||
modifier {:width 200}
|
||||
result (dwt/apply-text-modifier shape modifier)]
|
||||
;; shape had no position-data; modifier doesn't set one — stays nil
|
||||
(t/is (nil? (:position-data result))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Tests: degenerate selrect (zero width or height decoded from the server)
|
||||
;;
|
||||
;; Root cause of the original crash:
|
||||
;; change-dimensions-modifiers divided by (:width selrect) or (:height selrect)
|
||||
;; which is 0 when the shape was decoded via map->Rect (bypassing make-rect's
|
||||
;; 0.01 minimum), producing Infinity → transform pipeline returned nil selrect
|
||||
;; → gpt/point threw "invalid arguments (on pointer constructor)".
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest apply-text-modifier-zero-width-selrect-does-not-throw
|
||||
(t/testing "width modifier on a shape with zero selrect width does not throw"
|
||||
;; Simulates a shape received from the server whose selrect has width=0
|
||||
;; (map->Rect bypasses the 0.01 floor of make-rect).
|
||||
(let [shape (make-degenerate-text-shape :x 0 :y 0 :width 0 :height 50)
|
||||
modifier {:width 200}
|
||||
result (dwt/apply-text-modifier shape modifier)]
|
||||
(t/is (some? result))
|
||||
(t/is (some? (:selrect result))))))
|
||||
|
||||
(t/deftest apply-text-modifier-zero-height-selrect-does-not-throw
|
||||
(t/testing "height modifier on a shape with zero selrect height does not throw"
|
||||
(let [shape (make-degenerate-text-shape :x 0 :y 0 :width 100 :height 0)
|
||||
modifier {:height 80}
|
||||
result (dwt/apply-text-modifier shape modifier)]
|
||||
(t/is (some? result))
|
||||
(t/is (some? (:selrect result))))))
|
||||
|
||||
(t/deftest apply-text-modifier-zero-width-and-height-selrect-does-not-throw
|
||||
(t/testing "both modifiers on a fully-degenerate selrect do not throw"
|
||||
(let [shape (make-degenerate-text-shape :x 0 :y 0 :width 0 :height 0)
|
||||
modifier {:width 150 :height 60}
|
||||
result (dwt/apply-text-modifier shape modifier)]
|
||||
(t/is (some? result))
|
||||
(t/is (some? (:selrect result))))))
|
||||
|
||||
(t/deftest apply-text-modifier-zero-width-selrect-result-has-correct-width
|
||||
(t/testing "applying width modifier to a zero-width shape yields the requested width"
|
||||
(let [shape (make-degenerate-text-shape :x 0 :y 0 :width 0 :height 50)
|
||||
modifier {:width 200}
|
||||
result (dwt/apply-text-modifier shape modifier)]
|
||||
(t/is (= 200.0 (-> result :selrect :width))))))
|
||||
|
||||
(t/deftest apply-text-modifier-zero-height-selrect-result-has-correct-height
|
||||
(t/testing "applying height modifier to a zero-height shape yields the requested height"
|
||||
(let [shape (make-degenerate-text-shape :x 0 :y 0 :width 100 :height 0)
|
||||
modifier {:height 80}
|
||||
result (dwt/apply-text-modifier shape modifier)]
|
||||
(t/is (= 80.0 (-> result :selrect :height))))))
|
||||
|
||||
(t/deftest apply-text-modifier-nil-modifier-on-degenerate-shape-returns-unchanged
|
||||
(t/testing "nil modifier on a zero-selrect shape returns the same shape"
|
||||
(let [shape (make-degenerate-text-shape :x 0 :y 0 :width 0 :height 0)
|
||||
result (dwt/apply-text-modifier shape nil)]
|
||||
(t/is (identical? shape result)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Tests: shape origin is preserved when there is no dimension change
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest apply-text-modifier-selrect-origin-preserved-without-resize
|
||||
(t/testing "selrect x/y origin does not shift when no dimension changes"
|
||||
(let [shape (make-text-shape :x 30 :y 40 :width 100 :height 50)
|
||||
modifier {:position-data (sample-position-data 30 40)}
|
||||
result (dwt/apply-text-modifier shape modifier)]
|
||||
(t/is (= (-> shape :selrect :x) (-> result :selrect :x)))
|
||||
(t/is (= (-> shape :selrect :y) (-> result :selrect :y))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Tests: returned shape is a proper map-like value
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest apply-text-modifier-returns-shape-with-required-keys
|
||||
(t/testing "result always contains the core shape keys"
|
||||
(let [shape (make-text-shape :x 0 :y 0 :width 100 :height 50)
|
||||
modifier {:width 200 :height 80}
|
||||
result (dwt/apply-text-modifier shape modifier)]
|
||||
(t/is (some? (:id result)))
|
||||
(t/is (some? (:type result)))
|
||||
(t/is (some? (:selrect result))))))
|
||||
|
||||
(t/deftest apply-text-modifier-nil-modifier-returns-same-identity
|
||||
(t/testing "nil modifier returns the exact same shape object (identity)"
|
||||
(let [shape (make-text-shape)]
|
||||
(t/is (identical? shape (dwt/apply-text-modifier shape nil))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Tests: delta-move computation does not throw on degenerate selrect
|
||||
;;
|
||||
;; The delta-move in apply-text-modifier calls gpt/point on both the
|
||||
;; original and new shape selrects. gpt/point throws when given a
|
||||
;; non-point-like value (nil, or a map with non-finite :x/:y). Using
|
||||
;; ctm/safe-size-rect instead of raw (:selrect …) access ensures a valid
|
||||
;; rect is always available for that computation.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest apply-text-modifier-position-data-with-degenerate-selrect-does-not-throw
|
||||
(t/testing "position-data modifier on a zero-selrect shape does not throw"
|
||||
(let [pd (sample-position-data 5 10)
|
||||
shape (make-degenerate-text-shape :x 0 :y 0 :width 0 :height 0)
|
||||
result (dwt/apply-text-modifier shape {:position-data pd})]
|
||||
(t/is (some? result))
|
||||
(t/is (= pd (:position-data result)))))
|
||||
|
||||
(t/testing "width + position-data modifier on a zero-selrect shape does not throw"
|
||||
(let [pd (sample-position-data 5 10)
|
||||
shape (make-degenerate-text-shape :x 0 :y 0 :width 0 :height 0)
|
||||
result (dwt/apply-text-modifier shape {:width 200 :position-data pd})]
|
||||
(t/is (some? result))
|
||||
(t/is (some? (:selrect result))))))
|
||||
@@ -4,6 +4,7 @@
|
||||
[frontend-tests.basic-shapes-test]
|
||||
[frontend-tests.data.repo-test]
|
||||
[frontend-tests.data.workspace-colors-test]
|
||||
[frontend-tests.data.workspace-texts-test]
|
||||
[frontend-tests.helpers-shapes-test]
|
||||
[frontend-tests.logic.comp-remove-swap-slots-test]
|
||||
[frontend-tests.logic.components-and-tokens]
|
||||
@@ -38,6 +39,7 @@
|
||||
'frontend-tests.basic-shapes-test
|
||||
'frontend-tests.data.repo-test
|
||||
'frontend-tests.data.workspace-colors-test
|
||||
'frontend-tests.data.workspace-texts-test
|
||||
'frontend-tests.helpers-shapes-test
|
||||
'frontend-tests.logic.comp-remove-swap-slots-test
|
||||
'frontend-tests.logic.components-and-tokens
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.31.0+sha512.e3927388bfaa8078ceb79b748ffc1e8274e84d75163e67bc22e06c0d3aed43dd153151cbf11d7f8301ff4acb98c68bdc5cadf6989532801ffafe3b3e4a63c268",
|
||||
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
@@ -12,12 +12,13 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"lint": "./scripts/lint",
|
||||
"check-fmt": "./scripts/check-fmt",
|
||||
"fmt": "./scripts/fmt"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@github/copilot": "^1.0.12",
|
||||
"@types/node": "^20.12.7",
|
||||
"esbuild": "^0.25.9",
|
||||
"esbuild": "^0.27.4",
|
||||
"opencode-ai": "^1.3.7"
|
||||
}
|
||||
}
|
||||
|
||||
218
pnpm-lock.yaml
generated
218
pnpm-lock.yaml
generated
@@ -15,166 +15,166 @@ importers:
|
||||
specifier: ^20.12.7
|
||||
version: 20.19.37
|
||||
esbuild:
|
||||
specifier: ^0.25.9
|
||||
version: 0.25.12
|
||||
specifier: ^0.27.4
|
||||
version: 0.27.4
|
||||
opencode-ai:
|
||||
specifier: ^1.3.7
|
||||
version: 1.3.7
|
||||
|
||||
packages:
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.12':
|
||||
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
|
||||
'@esbuild/aix-ppc64@0.27.4':
|
||||
resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
|
||||
'@esbuild/android-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.25.12':
|
||||
resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
|
||||
'@esbuild/android-arm@0.27.4':
|
||||
resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.25.12':
|
||||
resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
|
||||
'@esbuild/android-x64@0.27.4':
|
||||
resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
|
||||
'@esbuild/darwin-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.25.12':
|
||||
resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
|
||||
'@esbuild/darwin-x64@0.27.4':
|
||||
resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
|
||||
'@esbuild/freebsd-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.12':
|
||||
resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
|
||||
'@esbuild/freebsd-x64@0.27.4':
|
||||
resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
|
||||
'@esbuild/linux-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.25.12':
|
||||
resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
|
||||
'@esbuild/linux-arm@0.27.4':
|
||||
resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.25.12':
|
||||
resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
|
||||
'@esbuild/linux-ia32@0.27.4':
|
||||
resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.25.12':
|
||||
resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
|
||||
'@esbuild/linux-loong64@0.27.4':
|
||||
resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.12':
|
||||
resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
|
||||
'@esbuild/linux-mips64el@0.27.4':
|
||||
resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.12':
|
||||
resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
|
||||
'@esbuild/linux-ppc64@0.27.4':
|
||||
resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.12':
|
||||
resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
|
||||
'@esbuild/linux-riscv64@0.27.4':
|
||||
resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.25.12':
|
||||
resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
|
||||
'@esbuild/linux-s390x@0.27.4':
|
||||
resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.25.12':
|
||||
resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
|
||||
'@esbuild/linux-x64@0.27.4':
|
||||
resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
|
||||
'@esbuild/netbsd-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.12':
|
||||
resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
|
||||
'@esbuild/netbsd-x64@0.27.4':
|
||||
resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
|
||||
'@esbuild/openbsd-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.12':
|
||||
resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
|
||||
'@esbuild/openbsd-x64@0.27.4':
|
||||
resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
|
||||
'@esbuild/openharmony-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.25.12':
|
||||
resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
|
||||
'@esbuild/sunos-x64@0.27.4':
|
||||
resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
|
||||
'@esbuild/win32-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.25.12':
|
||||
resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
|
||||
'@esbuild/win32-ia32@0.27.4':
|
||||
resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.25.12':
|
||||
resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
|
||||
'@esbuild/win32-x64@0.27.4':
|
||||
resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -222,8 +222,8 @@ packages:
|
||||
'@types/node@20.19.37':
|
||||
resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==}
|
||||
|
||||
esbuild@0.25.12:
|
||||
resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
|
||||
esbuild@0.27.4:
|
||||
resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
@@ -296,82 +296,82 @@ packages:
|
||||
|
||||
snapshots:
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.12':
|
||||
'@esbuild/aix-ppc64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.25.12':
|
||||
'@esbuild/android-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.25.12':
|
||||
'@esbuild/android-arm@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.25.12':
|
||||
'@esbuild/android-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.12':
|
||||
'@esbuild/darwin-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.25.12':
|
||||
'@esbuild/darwin-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.12':
|
||||
'@esbuild/freebsd-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.12':
|
||||
'@esbuild/freebsd-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.25.12':
|
||||
'@esbuild/linux-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.25.12':
|
||||
'@esbuild/linux-arm@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.25.12':
|
||||
'@esbuild/linux-ia32@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.25.12':
|
||||
'@esbuild/linux-loong64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.12':
|
||||
'@esbuild/linux-mips64el@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.12':
|
||||
'@esbuild/linux-ppc64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.12':
|
||||
'@esbuild/linux-riscv64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.25.12':
|
||||
'@esbuild/linux-s390x@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.25.12':
|
||||
'@esbuild/linux-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.12':
|
||||
'@esbuild/netbsd-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.12':
|
||||
'@esbuild/netbsd-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.12':
|
||||
'@esbuild/openbsd-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.12':
|
||||
'@esbuild/openbsd-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.25.12':
|
||||
'@esbuild/openharmony-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.25.12':
|
||||
'@esbuild/sunos-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.25.12':
|
||||
'@esbuild/win32-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.25.12':
|
||||
'@esbuild/win32-ia32@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.25.12':
|
||||
'@esbuild/win32-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@github/copilot-darwin-arm64@1.0.12':
|
||||
@@ -405,34 +405,34 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
esbuild@0.25.12:
|
||||
esbuild@0.27.4:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.25.12
|
||||
'@esbuild/android-arm': 0.25.12
|
||||
'@esbuild/android-arm64': 0.25.12
|
||||
'@esbuild/android-x64': 0.25.12
|
||||
'@esbuild/darwin-arm64': 0.25.12
|
||||
'@esbuild/darwin-x64': 0.25.12
|
||||
'@esbuild/freebsd-arm64': 0.25.12
|
||||
'@esbuild/freebsd-x64': 0.25.12
|
||||
'@esbuild/linux-arm': 0.25.12
|
||||
'@esbuild/linux-arm64': 0.25.12
|
||||
'@esbuild/linux-ia32': 0.25.12
|
||||
'@esbuild/linux-loong64': 0.25.12
|
||||
'@esbuild/linux-mips64el': 0.25.12
|
||||
'@esbuild/linux-ppc64': 0.25.12
|
||||
'@esbuild/linux-riscv64': 0.25.12
|
||||
'@esbuild/linux-s390x': 0.25.12
|
||||
'@esbuild/linux-x64': 0.25.12
|
||||
'@esbuild/netbsd-arm64': 0.25.12
|
||||
'@esbuild/netbsd-x64': 0.25.12
|
||||
'@esbuild/openbsd-arm64': 0.25.12
|
||||
'@esbuild/openbsd-x64': 0.25.12
|
||||
'@esbuild/openharmony-arm64': 0.25.12
|
||||
'@esbuild/sunos-x64': 0.25.12
|
||||
'@esbuild/win32-arm64': 0.25.12
|
||||
'@esbuild/win32-ia32': 0.25.12
|
||||
'@esbuild/win32-x64': 0.25.12
|
||||
'@esbuild/aix-ppc64': 0.27.4
|
||||
'@esbuild/android-arm': 0.27.4
|
||||
'@esbuild/android-arm64': 0.27.4
|
||||
'@esbuild/android-x64': 0.27.4
|
||||
'@esbuild/darwin-arm64': 0.27.4
|
||||
'@esbuild/darwin-x64': 0.27.4
|
||||
'@esbuild/freebsd-arm64': 0.27.4
|
||||
'@esbuild/freebsd-x64': 0.27.4
|
||||
'@esbuild/linux-arm': 0.27.4
|
||||
'@esbuild/linux-arm64': 0.27.4
|
||||
'@esbuild/linux-ia32': 0.27.4
|
||||
'@esbuild/linux-loong64': 0.27.4
|
||||
'@esbuild/linux-mips64el': 0.27.4
|
||||
'@esbuild/linux-ppc64': 0.27.4
|
||||
'@esbuild/linux-riscv64': 0.27.4
|
||||
'@esbuild/linux-s390x': 0.27.4
|
||||
'@esbuild/linux-x64': 0.27.4
|
||||
'@esbuild/netbsd-arm64': 0.27.4
|
||||
'@esbuild/netbsd-x64': 0.27.4
|
||||
'@esbuild/openbsd-arm64': 0.27.4
|
||||
'@esbuild/openbsd-x64': 0.27.4
|
||||
'@esbuild/openharmony-arm64': 0.27.4
|
||||
'@esbuild/sunos-x64': 0.27.4
|
||||
'@esbuild/win32-arm64': 0.27.4
|
||||
'@esbuild/win32-ia32': 0.27.4
|
||||
'@esbuild/win32-x64': 0.27.4
|
||||
|
||||
opencode-ai@1.3.7:
|
||||
optionalDependencies:
|
||||
|
||||
Reference in New Issue
Block a user