mirror of
https://github.com/penpot/penpot.git
synced 2026-09-09 20:29:02 -04:00
Compare commits
3
Commits
issue-11592
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61caefbcf3 | ||
|
|
6586631293 | ||
|
|
66fb4a69ba |
No files matched your search
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.graph.arrow
|
||||
"Bulk Ladybug ingest through in-memory Arrow.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.graph.debug
|
||||
"In-memory Ladybug sessions for the debug graph console."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.graph.ingest
|
||||
"Penpot file -> Ladybug graph projection."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.graph.ladybug
|
||||
"Ladybug access layer for graph-backed Penpot.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.graph.meta
|
||||
"`GraphMeta`: the graph's own account of who built it and from what.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.graph.projection.document
|
||||
"Project a Penpot file-data map into Ladybug nodes and structural edges.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.graph.projection.transforms
|
||||
"Derived graph links: edges a reader could compute from the projected
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.graph.report
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.graph.schema
|
||||
"Ladybug DDL facade for the graph-backed Penpot vertical slice.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.graph.schema.contract
|
||||
"Deliberate choices in Penpot's graph schema, recorded as data.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.graph.schema.nodes
|
||||
"Single source of truth for graph node tables.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.graph.schema.projection
|
||||
"Derive Ladybug node column schemas from Penpot Malli sources.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.graph.schema.types
|
||||
"Map Malli schemas to Ladybug column types.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.graph.schema.values
|
||||
"Shape a Penpot value into the plain data its Ladybug column type wants.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.graph.stats
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.graph.sync
|
||||
"Incremental Ladybug graph updates from Penpot file-change events."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.rpc.commands.plugins
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.storage.pending-gc
|
||||
"A maintenance task that reclaims storage objects created in 'pending'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.tasks.demo-purge
|
||||
"Task handler for delayed demo profile deletion. Submitted at demo
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns backend-tests.demo-test
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns backend-tests.graph-binder-gate-test
|
||||
"Binder gate for the incremental-sync statement templates.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns backend-tests.graph-sync-parity-test
|
||||
"Cold projection and incremental sync are two implementations of one mapping,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns backend-tests.passwords-test
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns backend-tests.rpc-demo-test
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns backend-tests.rpc-plugins-test
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.common.types.path.fit
|
||||
"Curve fitting helpers."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.common.types.path.selection
|
||||
"Transforms selected path nodes and handlers."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.common.types.tokens-status
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns common-tests.files-migrations-0026-test
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns common-tests.types.tokens-status-test
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.auth
|
||||
"Resolves the caller's session cookie to a real profile id.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.handlers.export
|
||||
"Handle export jobs"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.handlers.jobs
|
||||
"REST surface for export jobs, under `/api/export/jobs`.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.jobs
|
||||
"Export job model and lifecycle.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.jobs.scheduler
|
||||
"Admission control for export jobs.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.jobs.store
|
||||
"Redis persistence for export jobs.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.jobs.utils
|
||||
"Temp file ownership for export jobs.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.router
|
||||
"Method + path dispatch.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.wasm.pool
|
||||
"Pool of headless render workers.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.wasm.render
|
||||
"Headless render pipeline: renders exports with the render-wasm Skia pipeline,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.wasm.worker
|
||||
"Render worker entry point.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns exporter-tests.export-shapes-test
|
||||
"Chunking of the browser backend."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns exporter-tests.jobs-test
|
||||
"Job state machine. Runs without redis: a store write with no connection is
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns exporter-tests.scheduler-test
|
||||
"Admission control. A headless job leases one render worker for its whole run,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns exporter-tests.wasm-pool-test
|
||||
"Worker leasing, against a stub pool: `with-worker` must give the worker back
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.main.data.workspace.path.clipboard
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.main.ui.workspace.viewport.path-state
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns frontend-tests.logic.path-actions-test
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
|
||||
(ns frontend-tests.logic.path-clipboard-test
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
|
||||
(ns frontend-tests.logic.path-helpers-test
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
|
||||
(ns frontend-tests.logic.path-lifecycle-test
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
|
||||
(ns frontend-tests.logic.path-test-helpers
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
|
||||
(ns frontend-tests.logic.path-tools-test
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns frontend-tests.logic.wasm-modifiers-nil-id-test
|
||||
"Reproduces the production crash \"Cannot read properties of null
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns frontend-tests.plugins.flex-test
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 Sucursal en España SL
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns frontend-tests.plugins.user-test
|
||||
(:require
|
||||
|
||||
+316
-50
@@ -268,7 +268,6 @@ fn difference(
|
||||
.iter()
|
||||
.filter(|s| path_a.contains(to_point(s.evaluate(TValue::Parametric(0.5)))))
|
||||
.copied()
|
||||
.map(|s| s.reverse())
|
||||
.map(|b| (BezierSource::B, b)),
|
||||
);
|
||||
|
||||
@@ -278,16 +277,53 @@ fn difference(
|
||||
fn exclusion(segments_a: Vec<Bezier>, segments_b: Vec<Bezier>) -> Vec<(BezierSource, Bezier)> {
|
||||
let mut result = Vec::new();
|
||||
result.extend(segments_a.iter().copied().map(|b| (BezierSource::A, b)));
|
||||
result.extend(
|
||||
segments_b
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|s| s.reverse())
|
||||
.map(|b| (BezierSource::B, b)),
|
||||
);
|
||||
result.extend(segments_b.iter().copied().map(|b| (BezierSource::B, b)));
|
||||
result
|
||||
}
|
||||
|
||||
// Mirrors `app.common.types.path.subpath/clockwise?`.
|
||||
fn is_clockwise(path: &Path) -> bool {
|
||||
let mut points: Vec<(f32, f32)> = Vec::new();
|
||||
|
||||
for segment in path.segments().iter() {
|
||||
match *segment {
|
||||
Segment::MoveTo(p) => {
|
||||
if !points.is_empty() {
|
||||
break;
|
||||
}
|
||||
points.push(p);
|
||||
}
|
||||
Segment::LineTo(p) => points.push(p),
|
||||
Segment::CurveTo((_, _, p)) => points.push(p),
|
||||
Segment::Close => break,
|
||||
}
|
||||
}
|
||||
|
||||
if points.len() < 3 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut signed_area = 0.0f64;
|
||||
for i in 0..points.len() {
|
||||
let (x1, y1) = points[i];
|
||||
let (x2, y2) = points[(i + 1) % points.len()];
|
||||
signed_area += f64::from(x1) * f64::from(y2) - f64::from(x2) * f64::from(y1);
|
||||
}
|
||||
|
||||
signed_area > 0.0
|
||||
}
|
||||
|
||||
// The kept pieces of B must point the same way round as the kept pieces of A. Not the
|
||||
// `path.bool/content-bool-pair` rule, which reverses intersection on same winding and
|
||||
// relies on `subpath/merge-paths` flipping subpaths when it joins them.
|
||||
fn should_reverse_b(bool_type: BoolType, a_is_clockwise: bool, path_b: &Path) -> bool {
|
||||
let same_winding = a_is_clockwise == is_clockwise(path_b);
|
||||
match bool_type {
|
||||
BoolType::Union | BoolType::Intersection => !same_winding,
|
||||
BoolType::Difference | BoolType::Exclusion => same_winding,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Copy)]
|
||||
enum BezierSource {
|
||||
A,
|
||||
@@ -305,17 +341,14 @@ fn pop_first_from_pool(pool: &mut BezierPool) -> Option<(BezierSource, Bezier)>
|
||||
pool.iter_mut().find_map(|e| e.take())
|
||||
}
|
||||
|
||||
// Find and remove the segment whose start point is closest to `end` within the
|
||||
// appropriate threshold. Same-source segments use a tight threshold
|
||||
// (INTERSECT_THRESHOLD_SAMEd) so we prefer staying on the same original path;
|
||||
// cross-source segments use a wider threshold (INTERSECT_THRESHOLD_DIFFERENT)
|
||||
// to allow switching paths at intersection points.
|
||||
// Same-source candidates get a tighter threshold so we stay on the original path. A
|
||||
// candidate that joins by its `end` points the wrong way, so we reverse it.
|
||||
fn find_next_in_pool(
|
||||
pool: &mut BezierPool,
|
||||
end: DVec2,
|
||||
source: BezierSource,
|
||||
) -> Option<(BezierSource, Bezier)> {
|
||||
let mut best_idx: Option<usize> = None;
|
||||
let mut best: Option<(usize, bool)> = None;
|
||||
let mut best_dist_sq = f64::MAX;
|
||||
|
||||
for (i, entry) in pool.iter().enumerate() {
|
||||
@@ -327,16 +360,21 @@ fn find_next_in_pool(
|
||||
} else {
|
||||
INTERSECT_THRESHOLD_DIFFERENT as f64
|
||||
};
|
||||
let dx = bezier.start.x - end.x;
|
||||
let dy = bezier.start.y - end.y;
|
||||
let dist_sq = dx * dx + dy * dy;
|
||||
if dist_sq <= threshold * threshold && dist_sq < best_dist_sq {
|
||||
best_dist_sq = dist_sq;
|
||||
best_idx = Some(i);
|
||||
for (reversed, point) in [(false, bezier.start), (true, bezier.end)] {
|
||||
let dx = point.x - end.x;
|
||||
let dy = point.y - end.y;
|
||||
let dist_sq = dx * dx + dy * dy;
|
||||
if dist_sq <= threshold * threshold && dist_sq < best_dist_sq {
|
||||
best_dist_sq = dist_sq;
|
||||
best = Some((i, reversed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
best_idx.and_then(|i| pool[i].take())
|
||||
let (idx, reversed) = best?;
|
||||
pool[idx]
|
||||
.take()
|
||||
.map(|(src, bezier)| (src, if reversed { bezier.reverse() } else { bezier }))
|
||||
}
|
||||
|
||||
fn push_bezier(result: &mut Vec<Segment>, bezier: &Bezier) {
|
||||
@@ -410,32 +448,45 @@ fn beziers_to_segments(beziers: &[(BezierSource, Bezier)]) -> Vec<Segment> {
|
||||
result
|
||||
}
|
||||
|
||||
pub fn bool_from_shapes(bool_type: BoolType, children_ids: &[Uuid], shapes: ShapesPoolRef) -> Path {
|
||||
if children_ids.is_empty() {
|
||||
return Path::default();
|
||||
fn bool_beziers(
|
||||
bool_type: BoolType,
|
||||
path_a: &Path,
|
||||
a_is_clockwise: bool,
|
||||
path_b: &Path,
|
||||
) -> (Vec<(BezierSource, Bezier)>, bool) {
|
||||
let (segs_a, mut segs_b) = split_segments(path_a, path_b);
|
||||
|
||||
if should_reverse_b(bool_type, a_is_clockwise, path_b) {
|
||||
for segment in segs_b.iter_mut() {
|
||||
*segment = segment.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
let Some(child) = shapes.get(&children_ids[children_ids.len() - 1]) else {
|
||||
let beziers = match bool_type {
|
||||
BoolType::Union => union(path_a, segs_a, path_b, segs_b),
|
||||
BoolType::Difference => difference(path_a, segs_a, path_b, segs_b),
|
||||
BoolType::Intersection => intersection(path_a, segs_a, path_b, segs_b),
|
||||
BoolType::Exclusion => exclusion(segs_a, segs_b),
|
||||
};
|
||||
|
||||
(beziers, path_a.is_even_odd() || path_b.is_even_odd())
|
||||
}
|
||||
|
||||
// Fold `paths` left to right; the first entry is the base operand.
|
||||
fn bool_fold(bool_type: BoolType, paths: &[Path]) -> Path {
|
||||
let Some((first, rest)) = paths.split_first() else {
|
||||
return Path::default();
|
||||
};
|
||||
|
||||
let mut current_path = child.to_path(shapes);
|
||||
let mut current_path = first.clone();
|
||||
// Every fold step chains A's fragments, which keep their direction, so the
|
||||
// accumulated path keeps this winding. Carry it instead of re-reading it from the
|
||||
// emitted segment list, whose subpath order and direction fall out of pool ordering.
|
||||
let is_clockwise_a = is_clockwise(¤t_path);
|
||||
|
||||
for idx in (0..children_ids.len() - 1).rev() {
|
||||
let Some(other) = shapes.get(&children_ids[idx]) else {
|
||||
continue;
|
||||
};
|
||||
let other_path = other.to_path(shapes);
|
||||
|
||||
let (segs_a, segs_b) = split_segments(¤t_path, &other_path);
|
||||
|
||||
let is_even_odd = current_path.is_even_odd() || other_path.is_even_odd();
|
||||
let beziers = match bool_type {
|
||||
BoolType::Union => union(¤t_path, segs_a, &other_path, segs_b),
|
||||
BoolType::Difference => difference(¤t_path, segs_a, &other_path, segs_b),
|
||||
BoolType::Intersection => intersection(¤t_path, segs_a, &other_path, segs_b),
|
||||
BoolType::Exclusion => exclusion(segs_a, segs_b),
|
||||
};
|
||||
for other_path in rest {
|
||||
let (beziers, is_even_odd) =
|
||||
bool_beziers(bool_type, ¤t_path, is_clockwise_a, other_path);
|
||||
|
||||
current_path = Path::new(beziers_to_segments(&beziers)).with_even_odd(is_even_odd);
|
||||
}
|
||||
@@ -443,6 +494,16 @@ pub fn bool_from_shapes(bool_type: BoolType, children_ids: &[Uuid], shapes: Shap
|
||||
current_path
|
||||
}
|
||||
|
||||
pub fn bool_from_shapes(bool_type: BoolType, children_ids: &[Uuid], shapes: ShapesPoolRef) -> Path {
|
||||
let paths: Vec<Path> = children_ids
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|id| shapes.get(id).map(|child| child.to_path(shapes)))
|
||||
.collect();
|
||||
|
||||
bool_fold(bool_type, &paths)
|
||||
}
|
||||
|
||||
pub fn update_bool_to_path(shape: &mut Shape, shapes: ShapesPoolRef) {
|
||||
let children_ids = shape.children_ids(true);
|
||||
|
||||
@@ -481,6 +542,7 @@ pub fn debug_render_bool_paths(
|
||||
};
|
||||
|
||||
let mut current_path = child.to_path(shapes);
|
||||
let is_clockwise_a = is_clockwise(¤t_path);
|
||||
|
||||
for idx in (0..children_ids.len() - 1).rev() {
|
||||
let Some(other) = shapes.get(&children_ids[idx]) else {
|
||||
@@ -488,15 +550,12 @@ pub fn debug_render_bool_paths(
|
||||
};
|
||||
let other_path = other.to_path(shapes);
|
||||
|
||||
let (segs_a, segs_b) = split_segments(¤t_path, &other_path);
|
||||
|
||||
let is_even_odd = current_path.is_even_odd() || other_path.is_even_odd();
|
||||
let beziers = match bool_data.bool_type {
|
||||
BoolType::Union => union(¤t_path, segs_a, &other_path, segs_b),
|
||||
BoolType::Difference => difference(¤t_path, segs_a, &other_path, segs_b),
|
||||
BoolType::Intersection => intersection(¤t_path, segs_a, &other_path, segs_b),
|
||||
BoolType::Exclusion => exclusion(segs_a, segs_b),
|
||||
};
|
||||
let (beziers, is_even_odd) = bool_beziers(
|
||||
bool_data.bool_type,
|
||||
¤t_path,
|
||||
is_clockwise_a,
|
||||
&other_path,
|
||||
);
|
||||
current_path = Path::new(beziers_to_segments(&beziers)).with_even_odd(is_even_odd);
|
||||
|
||||
if idx == 0 {
|
||||
@@ -572,3 +631,210 @@ pub fn debug_render_bool_paths(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn linear(from: (f64, f64), to: (f64, f64)) -> Bezier {
|
||||
Bezier::from_linear_coordinates(from.0, from.1, to.0, to.1)
|
||||
}
|
||||
|
||||
fn polygon(points: &[(f32, f32)]) -> Path {
|
||||
let mut segments = vec![Segment::MoveTo(points[0])];
|
||||
segments.extend(points[1..].iter().map(|p| Segment::LineTo(*p)));
|
||||
segments.push(Segment::Close);
|
||||
Path::new(segments)
|
||||
}
|
||||
|
||||
fn count(segments: &[Segment], f: fn(&Segment) -> bool) -> usize {
|
||||
segments.iter().filter(|s| f(s)).count()
|
||||
}
|
||||
|
||||
fn is_move_to(s: &Segment) -> bool {
|
||||
matches!(s, Segment::MoveTo(_))
|
||||
}
|
||||
|
||||
fn is_close(s: &Segment) -> bool {
|
||||
matches!(s, Segment::Close)
|
||||
}
|
||||
|
||||
fn ring_area(ring: &[(f32, f32)]) -> f64 {
|
||||
let mut area = 0.0f64;
|
||||
for i in 0..ring.len() {
|
||||
let (x1, y1) = ring[i];
|
||||
let (x2, y2) = ring[(i + 1) % ring.len()];
|
||||
area += f64::from(x1) * f64::from(y2) - f64::from(x2) * f64::from(y1);
|
||||
}
|
||||
area / 2.0
|
||||
}
|
||||
|
||||
fn signed_area(segments: &[Segment]) -> f64 {
|
||||
let mut total = 0.0f64;
|
||||
let mut ring: Vec<(f32, f32)> = Vec::new();
|
||||
|
||||
for segment in segments {
|
||||
match *segment {
|
||||
Segment::MoveTo(p) => {
|
||||
total += ring_area(&ring);
|
||||
ring.clear();
|
||||
ring.push(p);
|
||||
}
|
||||
Segment::LineTo(p) | Segment::CurveTo((_, _, p)) => ring.push(p),
|
||||
Segment::Close => {
|
||||
total += ring_area(&ring);
|
||||
ring.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
total + ring_area(&ring)
|
||||
}
|
||||
|
||||
// Operands and expected results taken from the CLJS bool (`app.common.types.path.bool`)
|
||||
// run on the same shapes: A clockwise, B and C counter-clockwise, all overlapping.
|
||||
const A_CW: [(f32, f32); 4] = [
|
||||
(100.0, 100.0),
|
||||
(300.0, 100.0),
|
||||
(300.0, 300.0),
|
||||
(100.0, 300.0),
|
||||
];
|
||||
const B_CCW: [(f32, f32); 4] = [
|
||||
(200.0, 200.0),
|
||||
(200.0, 400.0),
|
||||
(400.0, 400.0),
|
||||
(400.0, 200.0),
|
||||
];
|
||||
const C_CCW: [(f32, f32); 4] = [(60.0, 240.0), (60.0, 360.0), (260.0, 360.0), (260.0, 240.0)];
|
||||
|
||||
#[test]
|
||||
fn test_is_clockwise() {
|
||||
let cw = Path::new(vec![
|
||||
Segment::MoveTo((0.0, 0.0)),
|
||||
Segment::LineTo((10.0, 0.0)),
|
||||
Segment::LineTo((10.0, 10.0)),
|
||||
Segment::LineTo((0.0, 10.0)),
|
||||
Segment::Close,
|
||||
]);
|
||||
assert!(is_clockwise(&cw));
|
||||
|
||||
let ccw = Path::new(vec![
|
||||
Segment::MoveTo((0.0, 0.0)),
|
||||
Segment::LineTo((0.0, 10.0)),
|
||||
Segment::LineTo((10.0, 10.0)),
|
||||
Segment::LineTo((10.0, 0.0)),
|
||||
Segment::Close,
|
||||
]);
|
||||
assert!(!is_clockwise(&ccw));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_reverse_b_only_depends_on_relative_winding() {
|
||||
let cw = Path::new(vec![
|
||||
Segment::MoveTo((0.0, 0.0)),
|
||||
Segment::LineTo((10.0, 0.0)),
|
||||
Segment::LineTo((10.0, 10.0)),
|
||||
Segment::LineTo((0.0, 10.0)),
|
||||
Segment::Close,
|
||||
]);
|
||||
let ccw = Path::new(vec![
|
||||
Segment::MoveTo((0.0, 0.0)),
|
||||
Segment::LineTo((0.0, 10.0)),
|
||||
Segment::LineTo((10.0, 10.0)),
|
||||
Segment::LineTo((10.0, 0.0)),
|
||||
Segment::Close,
|
||||
]);
|
||||
|
||||
assert!(should_reverse_b(BoolType::Difference, true, &cw));
|
||||
assert!(!should_reverse_b(BoolType::Difference, true, &ccw));
|
||||
assert!(!should_reverse_b(BoolType::Union, true, &cw));
|
||||
assert!(should_reverse_b(BoolType::Union, true, &ccw));
|
||||
}
|
||||
|
||||
// Fragments from #11482: two point the wrong way, so joining them start-to-start only
|
||||
// left five open subpaths.
|
||||
#[test]
|
||||
fn test_beziers_to_segments_closes_reversed_fragments() {
|
||||
let beziers = vec![
|
||||
(
|
||||
BezierSource::A,
|
||||
linear((2764.00, -240.00), (2834.74, -110.71)),
|
||||
),
|
||||
(
|
||||
BezierSource::A,
|
||||
linear((2809.29, -85.26), (2693.26, -201.29)),
|
||||
),
|
||||
(
|
||||
BezierSource::A,
|
||||
linear((2718.71, -226.74), (2764.00, -240.00)),
|
||||
),
|
||||
(
|
||||
BezierSource::B,
|
||||
linear((2718.71, -226.74), (2834.74, -110.71)),
|
||||
),
|
||||
(
|
||||
BezierSource::B,
|
||||
linear((2809.29, -85.26), (2693.26, -201.29)),
|
||||
),
|
||||
];
|
||||
|
||||
let segments = beziers_to_segments(&beziers);
|
||||
|
||||
let moves = segments
|
||||
.iter()
|
||||
.filter(|s| matches!(s, Segment::MoveTo(_)))
|
||||
.count();
|
||||
let closes = segments
|
||||
.iter()
|
||||
.filter(|s| matches!(s, Segment::Close))
|
||||
.count();
|
||||
|
||||
assert_eq!(moves, 2);
|
||||
assert_eq!(closes, 2);
|
||||
// 3 fragments in the first subpath, 2 in the second, each dropping its closing LineTo.
|
||||
assert_eq!(segments.len(), 7);
|
||||
}
|
||||
|
||||
// #11482: A clockwise, B counter-clockwise. Reference (CLJS):
|
||||
// M100,100 L300,100 L300,200 L200,200 L200,300 L100,300 Z
|
||||
#[test]
|
||||
fn test_difference_with_opposite_winding_operand() {
|
||||
let result = bool_fold(BoolType::Difference, &[polygon(&A_CW), polygon(&B_CCW)]);
|
||||
let segments = result.segments();
|
||||
|
||||
assert_eq!(count(segments, is_move_to), 1);
|
||||
assert_eq!(count(segments, is_close), 1);
|
||||
assert!((signed_area(segments) - 30000.0).abs() < 1.0);
|
||||
assert!(is_clockwise(&result));
|
||||
}
|
||||
|
||||
// Reference (CLJS):
|
||||
// M100,100 L300,100 L300,200 L400,200 L400,400 L200,400 L200,300 L100,300 Z
|
||||
#[test]
|
||||
fn test_union_with_opposite_winding_operand() {
|
||||
let result = bool_fold(BoolType::Union, &[polygon(&A_CW), polygon(&B_CCW)]);
|
||||
let segments = result.segments();
|
||||
|
||||
assert_eq!(count(segments, is_move_to), 1);
|
||||
assert_eq!(count(segments, is_close), 1);
|
||||
assert!((signed_area(segments) - 70000.0).abs() < 1.0);
|
||||
assert!(is_clockwise(&result));
|
||||
}
|
||||
|
||||
// The second fold step must compare against A's winding, not against the winding of
|
||||
// the intermediate path, whose subpath order and direction fall out of pool ordering.
|
||||
// Reference (CLJS): M100,100 L300,100 L300,200 L200,200 L200,240 L100,240 Z
|
||||
#[test]
|
||||
fn test_difference_folds_three_opposite_winding_operands() {
|
||||
let result = bool_fold(
|
||||
BoolType::Difference,
|
||||
&[polygon(&A_CW), polygon(&B_CCW), polygon(&C_CCW)],
|
||||
);
|
||||
let segments = result.segments();
|
||||
|
||||
assert_eq!(count(segments, is_move_to), 1);
|
||||
assert_eq!(count(segments, is_close), 1);
|
||||
assert!((signed_area(segments) - 24000.0).abs() < 1.0);
|
||||
assert!(is_clockwise(&result));
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,7 @@ use skia_safe::{self as skia, Paint, RRect};
|
||||
use super::{filters, RenderState, SurfaceId};
|
||||
use crate::error::Result;
|
||||
use crate::get_resources;
|
||||
use crate::math::Rect as MathRect;
|
||||
use crate::render::get_source_rect;
|
||||
use crate::render::{get_image_dest_rect, get_source_rect};
|
||||
use crate::shapes::{merge_fills, Fill, Frame, ImageFill, Rect, Shape, Type};
|
||||
|
||||
// Set the clipping area to the shape outline within the container bounds
|
||||
@@ -94,15 +93,7 @@ fn draw_image_fill(
|
||||
let container = &shape.selrect;
|
||||
let sampling = get_resources().sampling_options;
|
||||
|
||||
let dest_rect = match image_fill.transform() {
|
||||
Some(tf) => MathRect::from_xywh(
|
||||
container.left + tf.x * container.width(),
|
||||
container.top + tf.y * container.height(),
|
||||
tf.width * container.width(),
|
||||
tf.height * container.height(),
|
||||
),
|
||||
None => *container,
|
||||
};
|
||||
let dest_rect = get_image_dest_rect(container, image_fill);
|
||||
let src_rect = get_source_rect(size, &dest_rect, image_fill);
|
||||
let needs_clip = image_fill.transform().is_some() || !is_axis_aligned_image_rect(shape);
|
||||
|
||||
@@ -189,16 +180,7 @@ fn draw_svg_image_fill(
|
||||
let fill_layer = skia::canvas::SaveLayerRec::default().paint(paint);
|
||||
canvas.save_layer(&fill_layer);
|
||||
|
||||
let dest_rect = match image_fill.transform() {
|
||||
Some(tf) => MathRect::from_xywh(
|
||||
container.left + tf.x * container.width(),
|
||||
container.top + tf.y * container.height(),
|
||||
tf.width * container.width(),
|
||||
tf.height * container.height(),
|
||||
),
|
||||
None => *container,
|
||||
};
|
||||
|
||||
let dest_rect = get_image_dest_rect(container, image_fill);
|
||||
let src_rect = get_source_rect(size, &dest_rect, image_fill);
|
||||
if src_rect.width() <= 0.0 || src_rect.height() <= 0.0 {
|
||||
canvas.restore();
|
||||
|
||||
@@ -20,6 +20,18 @@ pub fn get_dest_rect(container: &MathRect, delta: f32) -> MathRect {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_image_dest_rect(container: &MathRect, image_fill: &ImageFill) -> MathRect {
|
||||
match image_fill.transform() {
|
||||
Some(tf) => MathRect::from_xywh(
|
||||
container.left + tf.x * container.width(),
|
||||
container.top + tf.y * container.height(),
|
||||
tf.width * container.width(),
|
||||
tf.height * container.height(),
|
||||
),
|
||||
None => *container,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_source_rect(size: ISize, container: &MathRect, image_fill: &ImageFill) -> MathRect {
|
||||
let image_width = size.width as f32;
|
||||
let image_height = size.height as f32;
|
||||
|
||||
@@ -5,7 +5,8 @@ use crate::shapes::{Fill, ImageFill, Shape};
|
||||
use crate::state::ShapesPoolRef;
|
||||
|
||||
use super::document::SvgLayerCanvas;
|
||||
use crate::render::RenderResources;
|
||||
use crate::math::Rect as MathRect;
|
||||
use crate::render::{get_image_dest_rect, RenderResources};
|
||||
|
||||
/// Emits fills bottom -> top for SVG export.
|
||||
///
|
||||
@@ -63,19 +64,20 @@ fn emit_image_fill(
|
||||
let clip_id = builder.unique("imgclip");
|
||||
builder.push_clip_path(&clip_id, shape, tree);
|
||||
let href = xml_escape_attr(url);
|
||||
emit_linked_image_element(builder, shape, image_fill, &href, &clip_id);
|
||||
let dest_rect = get_image_dest_rect(&shape.selrect(), image_fill);
|
||||
emit_linked_image_element(builder, shape, image_fill, dest_rect, &href, &clip_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Emits `<g clip-path>` + `<image href>` using the shape selrect and page CTM.
|
||||
/// Emits `<g clip-path>` + `<image href>` at `dest_rect`, under the page CTM.
|
||||
pub(super) fn emit_linked_image_element(
|
||||
builder: &mut SvgLayerCanvas,
|
||||
shape: &Shape,
|
||||
image_fill: &ImageFill,
|
||||
dest_rect: MathRect,
|
||||
href: &str,
|
||||
clip_id: &str,
|
||||
) {
|
||||
let selrect = shape.selrect();
|
||||
let opacity = image_fill.opacity() as f32 / 255.0;
|
||||
let preserve = if image_fill.keep_aspect_ratio() {
|
||||
"xMidYMid slice"
|
||||
@@ -93,10 +95,10 @@ pub(super) fn emit_linked_image_element(
|
||||
builder.open_group(&format!("clip-path=\"url(#{clip_id})\""));
|
||||
builder.push_raw(&format!(
|
||||
r#"<image href="{href}" x="{}" y="{}" width="{}" height="{}" preserveAspectRatio="{preserve}"{opacity_attr} transform="{transform}"/>"#,
|
||||
selrect.left(),
|
||||
selrect.top(),
|
||||
selrect.width(),
|
||||
selrect.height(),
|
||||
dest_rect.left(),
|
||||
dest_rect.top(),
|
||||
dest_rect.width(),
|
||||
dest_rect.height(),
|
||||
));
|
||||
builder.close_group();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use super::fixtures::*;
|
||||
|
||||
use crate::shapes::{BlendMode, Fill, ImageFill, SolidColor, StrokeCap, StrokeKind};
|
||||
use crate::shapes::{
|
||||
BlendMode, Fill, ImageFill, ImageFillTransform, SolidColor, StrokeCap, StrokeKind,
|
||||
};
|
||||
use crate::state::ShapesPool;
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
@@ -1076,6 +1078,43 @@ fn exports_image_fill_as_linked_image() {
|
||||
insta::assert_snapshot!(svg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exports_image_fill_bounds_transform() {
|
||||
let mut pool = ShapesPool::new();
|
||||
let id = uid(1);
|
||||
let image_id = uid(42);
|
||||
add_rect_with_fills(
|
||||
&mut pool,
|
||||
id,
|
||||
Uuid::nil(),
|
||||
(0.0, 0.0, 100.0, 80.0),
|
||||
vec![Fill::Image(ImageFill::new_with_transform(
|
||||
image_id,
|
||||
255,
|
||||
200,
|
||||
100,
|
||||
false,
|
||||
Some(ImageFillTransform {
|
||||
x: 0.25,
|
||||
y: 0.5,
|
||||
width: 0.5,
|
||||
height: 0.25,
|
||||
}),
|
||||
))],
|
||||
);
|
||||
|
||||
let svg = render_with(&pool, id, |resources| {
|
||||
resources
|
||||
.images
|
||||
.set_source_url(image_id, TEST_IMAGE_URL.to_string());
|
||||
});
|
||||
|
||||
assert!(
|
||||
svg.contains(r#"x="25" y="40" width="50" height="20""#),
|
||||
"linked image must keep the independent image bounds: {svg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exports_mixed_solid_and_image_fills_in_order() {
|
||||
// Image under a translucent solid; stretch (keep-aspect off); partial image
|
||||
|
||||
@@ -117,6 +117,6 @@ fn emit_text_image_fill(
|
||||
builder.finish_clip_path_fragment(&clip_id, canvas);
|
||||
|
||||
let href = xml_escape_attr(url);
|
||||
emit_linked_image_element(builder, shape, image_fill, &href, &clip_id);
|
||||
emit_linked_image_element(builder, shape, image_fill, shape.selrect(), &href, &clip_id);
|
||||
Ok(())
|
||||
}
|
||||
@@ -12,7 +12,7 @@ use super::shape_renderer::ShapeRenderer;
|
||||
use super::text;
|
||||
use super::RenderResources;
|
||||
use super::RenderState;
|
||||
use super::{get_dest_rect, get_source_rect};
|
||||
use super::{get_dest_rect, get_image_dest_rect, get_source_rect};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VectorRenderer — implements ShapeRenderer for canvas-based vector export
|
||||
@@ -1043,8 +1043,8 @@ fn draw_image_fill(
|
||||
let size = image.dimensions();
|
||||
let container = &shape.selrect;
|
||||
|
||||
let src_rect = get_source_rect(size, container, image_fill);
|
||||
let dest_rect = container;
|
||||
let dest_rect = get_image_dest_rect(container, image_fill);
|
||||
let src_rect = get_source_rect(size, &dest_rect, image_fill);
|
||||
|
||||
canvas.save();
|
||||
|
||||
|
||||
@@ -173,9 +173,11 @@ main() {
|
||||
fi
|
||||
done
|
||||
|
||||
# Get tracked files matching our extensions
|
||||
local files
|
||||
files=$(git ls-files | grep -E "\.(${EXTENSIONS})$" || true)
|
||||
# Get tracked files matching our extensions, excluding this script itself
|
||||
# (it mentions the search string in its own usage docs).
|
||||
local files script_name
|
||||
script_name=$(basename "$0")
|
||||
files=$(git ls-files | grep -E "\.(${EXTENSIONS})$" | grep -v -F "$script_name" || true)
|
||||
|
||||
if [[ -z "$files" ]]; then
|
||||
log_warn "No tracked files found matching extensions: ${EXTENSIONS}"
|
||||
|
||||
Reference in new issue
Block a user