mirror of
https://github.com/penpot/penpot.git
synced 2026-01-23 05:40:17 -05:00
Compare commits
29 Commits
eva-replac
...
elenatorro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11c122af9c | ||
|
|
f94c9cdb02 | ||
|
|
8637c46ba1 | ||
|
|
5d7d23a2c7 | ||
|
|
aab1d97c4c | ||
|
|
499aac31a4 | ||
|
|
962d7839a2 | ||
|
|
83387701a0 | ||
|
|
5775fa61ba | ||
|
|
5b1766835f | ||
|
|
ff25df0457 | ||
|
|
8c7fd0af4b | ||
|
|
cf46051f56 | ||
|
|
6393330ee1 | ||
|
|
8252bc485e | ||
|
|
47775a9e2c | ||
|
|
8191d04114 | ||
|
|
b7c2d9a079 | ||
|
|
aeb34a6f64 | ||
|
|
6fa0c3af0c | ||
|
|
260b9fb040 | ||
|
|
884954f4ff | ||
|
|
88f0f75174 | ||
|
|
1ffa956251 | ||
|
|
31054099ff | ||
|
|
983487d73c | ||
|
|
5c71c57dd9 | ||
|
|
5abc1aafb4 | ||
|
|
935728aa39 |
40
.travis.yml
40
.travis.yml
@@ -1,40 +0,0 @@
|
||||
dist: xenial
|
||||
|
||||
language: generic
|
||||
sudo: required
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.m2
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
|
||||
install:
|
||||
- curl -O https://download.clojure.org/install/linux-install-1.10.1.447.sh
|
||||
- chmod +x linux-install-1.10.1.447.sh
|
||||
- sudo ./linux-install-1.10.1.447.sh
|
||||
|
||||
before_script:
|
||||
- env | sort
|
||||
|
||||
script:
|
||||
- ./manage.sh build-devenv
|
||||
- ./manage.sh run-frontend-tests
|
||||
- ./manage.sh run-backend-tests
|
||||
- ./manage.sh build-images
|
||||
- ./manage.sh run
|
||||
|
||||
after_script:
|
||||
- docker images
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
|
||||
env:
|
||||
- NODE_VERSION=10.16.0
|
||||
@@ -152,9 +152,9 @@ services:
|
||||
|
||||
# AWS_ACCESS_KEY_ID: <KEY_ID>
|
||||
# AWS_SECRET_ACCESS_KEY: <ACCESS_KEY>
|
||||
# PENPOT_ASSETS_STORAGE_BACKEND: assets-s3
|
||||
# PENPOT_STORAGE_ASSETS_S3_ENDPOINT: <ENDPOINT>
|
||||
# PENPOT_STORAGE_ASSETS_S3_BUCKET: <BUKET_NAME>
|
||||
# PENPOT_OBJECTS_STORAGE_BACKEND: s3
|
||||
# PENPOT_OBJECTS_STORAGE_S3_ENDPOINT: <ENDPOINT>
|
||||
# PENPOT_OBJECTS_STORAGE_S3_BUCKET: <BUKET_NAME>
|
||||
|
||||
## Telemetry. When enabled, a periodical process will send anonymous data about this
|
||||
## instance. Telemetry data will enable us to learn how the application is used,
|
||||
|
||||
@@ -114,14 +114,7 @@ configuration.
|
||||
The callback has the following format:
|
||||
|
||||
```html
|
||||
https://<your_domain>/api/auth/oauth/<oauth_provider>/callback
|
||||
```
|
||||
|
||||
You will need to change <your_domain> and <oauth_provider> according to your setup.
|
||||
This is how it looks with Gitlab provider:
|
||||
|
||||
```html
|
||||
https://<your_domain>/api/auth/oauth/gitlab/callback
|
||||
https://<your_domain>/api/auth/oidc/callback
|
||||
```
|
||||
|
||||
#### Google
|
||||
|
||||
@@ -17,17 +17,18 @@
|
||||
<meta name="twitter:site" content="@penpotapp">
|
||||
<meta name="twitter:creator" content="@penpotapp">
|
||||
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
|
||||
<link id="theme" href="css/main.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||
<link id="theme" href="css/main.css?version={{& version_tag}}" rel="stylesheet" type="text/css" />
|
||||
{{#isDebug}}
|
||||
<link href="css/debug.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||
<link href="css/debug.css?version={{& version_tag}}" rel="stylesheet" type="text/css" />
|
||||
{{/isDebug}}
|
||||
|
||||
<link rel="icon" href="images/favicon.png" />
|
||||
<link rel="icon" href="images/favicon.png?version={{& version_tag }}" />
|
||||
|
||||
<script type="importmap">{{& manifest.importmap }}</script>
|
||||
|
||||
<script type="module">
|
||||
globalThis.penpotVersion = "{{& version}}";
|
||||
globalThis.penpotVersionTag = "{{& version_tag}}";
|
||||
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||
globalThis.penpotWorkerURI = "{{& manifest.worker_main}}";
|
||||
</script>
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Penpot - Rasterizer</title>
|
||||
<link rel="icon" href="images/favicon.png" />
|
||||
<link rel="icon" href="images/favicon.png?version={{& version_tag }}" />
|
||||
|
||||
<script>
|
||||
globalThis.penpotVersion = "{{& version}}";
|
||||
globalThis.penpotVersionTag = "{{& version_tag}}";
|
||||
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||
globalThis.penpotWorkerURI = "{{& manifest.worker_main}}";
|
||||
</script>
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Penpot - Render</title>
|
||||
<link rel="icon" href="images/favicon.png" />
|
||||
|
||||
<link rel="icon" href="images/favicon.png?version={{& version_tag }}" />
|
||||
|
||||
<script>
|
||||
globalThis.penpotVersion = "{{& version}}";
|
||||
globalThis.penpotVersionTag = "{{& version_tag}}";
|
||||
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||
</script>
|
||||
|
||||
|
||||
@@ -27,9 +27,11 @@ export function startWorker() {
|
||||
});
|
||||
}
|
||||
|
||||
export const isDebug = process.env.NODE_ENV !== "production";
|
||||
export const CURRENT_VERSION = process.env.CURRENT_VERSION || "develop";
|
||||
export const BUILD_DATE = process.env.BUILD_DATE || "" + new Date();
|
||||
export const IS_DEBUG = process.env.NODE_ENV !== "production";
|
||||
export const BUILD_DATE = process.env.BUILD_DATE || (new Date().toString()) ;
|
||||
export const BUILD_TS = process.env.BUILD_TS || Date.now();
|
||||
export const VERSION = process.env.VERSION || "develop";
|
||||
export const VERSION_TAG = process.env.VERSION_TAG || VERSION;
|
||||
|
||||
async function findFiles(basePath, predicate, options = {}) {
|
||||
predicate =
|
||||
@@ -193,25 +195,25 @@ async function generateManifest() {
|
||||
render_main: "./js/render.js",
|
||||
rasterizer_main: "./js/rasterizer.js",
|
||||
|
||||
config: "./js/config.js?version=" + CURRENT_VERSION,
|
||||
polyfills: "./js/polyfills.js?version=" + CURRENT_VERSION,
|
||||
libs: "./js/libs.js?version=" + CURRENT_VERSION,
|
||||
worker_main: "./js/worker/main.js?version=" + CURRENT_VERSION,
|
||||
default_translations: "./js/translation.en.js?version=" + CURRENT_VERSION,
|
||||
config: "./js/config.js?version=" + VERSION_TAG,
|
||||
polyfills: "./js/polyfills.js?version=" + VERSION_TAG,
|
||||
libs: "./js/libs.js?version=" + VERSION_TAG,
|
||||
worker_main: "./js/worker/main.js?version=" + VERSION_TAG,
|
||||
default_translations: "./js/translation.en.js?version=" + VERSION_TAG,
|
||||
|
||||
importmap: JSON.stringify({
|
||||
"imports": {
|
||||
"./js/shared.js": "./js/shared.js?version=" + CURRENT_VERSION,
|
||||
"./js/main.js": "./js/main.js?version=" + CURRENT_VERSION,
|
||||
"./js/render.js": "./js/render.js?version=" + CURRENT_VERSION,
|
||||
"./js/render-wasm.js": "./js/render-wasm.js?version=" + CURRENT_VERSION,
|
||||
"./js/rasterizer.js": "./js/rasterizer.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-dashboard.js": "./js/main-dashboard.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-auth.js": "./js/main-auth.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-viewer.js": "./js/main-viewer.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-settings.js": "./js/main-settings.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-workspace.js": "./js/main-workspace.js?version=" + CURRENT_VERSION,
|
||||
"./js/util-highlight.js": "./js/util-highlight.js?version=" + CURRENT_VERSION
|
||||
"./js/shared.js": "./js/shared.js?version=" + VERSION_TAG,
|
||||
"./js/main.js": "./js/main.js?version=" + VERSION_TAG,
|
||||
"./js/render.js": "./js/render.js?version=" + VERSION_TAG,
|
||||
"./js/render-wasm.js": "./js/render-wasm.js?version=" + VERSION_TAG,
|
||||
"./js/rasterizer.js": "./js/rasterizer.js?version=" + VERSION_TAG,
|
||||
"./js/main-dashboard.js": "./js/main-dashboard.js?version=" + VERSION_TAG,
|
||||
"./js/main-auth.js": "./js/main-auth.js?version=" + VERSION_TAG,
|
||||
"./js/main-viewer.js": "./js/main-viewer.js?version=" + VERSION_TAG,
|
||||
"./js/main-settings.js": "./js/main-settings.js?version=" + VERSION_TAG,
|
||||
"./js/main-workspace.js": "./js/main-workspace.js?version=" + VERSION_TAG,
|
||||
"./js/util-highlight.js": "./js/util-highlight.js?version=" + VERSION_TAG
|
||||
}
|
||||
})
|
||||
};
|
||||
@@ -222,11 +224,12 @@ async function generateManifest() {
|
||||
async function renderTemplate(path, context = {}, partials = {}) {
|
||||
const content = await fs.readFile(path, { encoding: "utf-8" });
|
||||
|
||||
const ts = Math.floor(new Date());
|
||||
|
||||
context = Object.assign({}, context, {
|
||||
ts: ts,
|
||||
isDebug,
|
||||
isDebug: IS_DEBUG,
|
||||
version: VERSION,
|
||||
version_tag: VERSION_TAG,
|
||||
build_date: BUILD_DATE,
|
||||
build_ts: BUILD_TS,
|
||||
});
|
||||
|
||||
return mustache.render(content, context, partials);
|
||||
@@ -390,7 +393,6 @@ async function generateSvgSprites() {
|
||||
}
|
||||
|
||||
async function generateTemplates() {
|
||||
const isDebug = process.env.NODE_ENV !== "production";
|
||||
await fs.mkdir("./resources/public/", { recursive: true });
|
||||
|
||||
const manifest = await generateManifest();
|
||||
@@ -415,10 +417,7 @@ async function generateTemplates() {
|
||||
};
|
||||
|
||||
const context = {
|
||||
manifest: manifest,
|
||||
version: CURRENT_VERSION,
|
||||
build_date: BUILD_DATE,
|
||||
isDebug,
|
||||
manifest: manifest
|
||||
};
|
||||
|
||||
content = await renderTemplate(
|
||||
@@ -487,7 +486,7 @@ export async function compileStyles() {
|
||||
await fs.mkdir("./resources/public/css", { recursive: true });
|
||||
await fs.writeFile("./resources/public/css/main.css", result);
|
||||
|
||||
if (isDebug) {
|
||||
if (IS_DEBUG) {
|
||||
let debugCSS = await compileSassDebug(worker);
|
||||
await fs.writeFile("./resources/public/css/debug.css", debugCSS);
|
||||
}
|
||||
|
||||
@@ -2,26 +2,26 @@
|
||||
# NOTE: this script should be called from the parent directory to
|
||||
# properly work.
|
||||
|
||||
set -ex
|
||||
|
||||
export INCLUDE_STORYBOOK=${BUILD_STORYBOOK:-no};
|
||||
export INCLUDE_WASM=${BUILD_WASM:-yes};
|
||||
export CURRENT_VERSION=$1;
|
||||
export BUILD_DATE=$(date -R);
|
||||
export CURRENT_HASH=${CURRENT_HASH:-$(git rev-parse --short HEAD)};
|
||||
export EXTRA_PARAMS=$SHADOWCLJS_EXTRA_PARAMS;
|
||||
export TS=$(date +%s);
|
||||
|
||||
export BUILD_DATE=$(date -R);
|
||||
export BUILD_TS=$(date +%s);
|
||||
|
||||
export VERSION=${1:-develop};
|
||||
export VERSION_TAG="${VERSION}-${BUILD_TS}";
|
||||
|
||||
# Some cljs reacts on this environment variable for define more
|
||||
# performant code on macros (example: rumext)
|
||||
export NODE_ENV=production;
|
||||
|
||||
echo "Current path:"
|
||||
echo $PATH
|
||||
|
||||
set -ex
|
||||
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install || exit 1;
|
||||
yarn install;
|
||||
|
||||
rm -rf target/dist;
|
||||
rm -rf resources/public;
|
||||
@@ -37,7 +37,7 @@ yarn run build:app:main $EXTRA_PARAMS;
|
||||
yarn run build:app:libs;
|
||||
yarn run build:app:assets;
|
||||
|
||||
sed -i "s/\.\/render.js/.\/render.js?version=$CURRENT_VERSION/g" resources/public/js/worker/main*.js
|
||||
sed -i "s/\.\/render.js/.\/render.js?version=$VERSION_TAG/g" resources/public/js/worker/main*.js
|
||||
|
||||
rsync -avr resources/public/ target/dist/
|
||||
|
||||
|
||||
@@ -2,18 +2,16 @@
|
||||
# NOTE: this script should be called from the parent directory to
|
||||
# properly work.
|
||||
|
||||
export CURRENT_VERSION=$1;
|
||||
set -ex
|
||||
|
||||
export BUILD_TS=$(date +%s);
|
||||
export BUILD_DATE=$(date -R);
|
||||
export CURRENT_HASH=${CURRENT_HASH:-$(git rev-parse --short HEAD)};
|
||||
export TS=$(date +%s);
|
||||
|
||||
export VERSION=${1:-develop};
|
||||
export VERSION_TAG="${VERSION}-${BUILD_TS}";
|
||||
|
||||
export NODE_ENV=production;
|
||||
|
||||
echo "Current path:"
|
||||
echo $PATH
|
||||
|
||||
set -ex
|
||||
|
||||
corepack enable;
|
||||
corepack install || exit 1;
|
||||
yarn install || exit 1;
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
{:fn-invoke-direct true
|
||||
:optimizations #shadow/env ["PENPOT_BUILD_OPTIMIZATIONS" :as :keyword :default :advanced]
|
||||
:source-map true
|
||||
:pseudo-names true
|
||||
:elide-asserts true
|
||||
:anon-fn-naming-policy :off
|
||||
:cross-chunk-method-motion false
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
(def browser (parse-browser))
|
||||
(def platform (parse-platform))
|
||||
|
||||
(def version-tag (obj/get global "penpotVersionTag"))
|
||||
(def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI"))
|
||||
(def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI"))
|
||||
(def flex-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/"))
|
||||
@@ -190,9 +191,8 @@
|
||||
|
||||
(defn resolve-href
|
||||
[resource]
|
||||
(let [version (get version :full)
|
||||
href (-> public-uri
|
||||
(u/ensure-path-slash)
|
||||
(u/join resource)
|
||||
(get :path))]
|
||||
(str href "?version=" version)))
|
||||
(let [href (-> public-uri
|
||||
(u/ensure-path-slash)
|
||||
(u/join resource)
|
||||
(get :path))]
|
||||
(str href "?version=" version-tag)))
|
||||
|
||||
@@ -27,8 +27,10 @@
|
||||
[app.main.data.workspace.colors :as wdc]
|
||||
[app.main.data.workspace.shape-layout :as dwsl]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.data.workspace.transforms :as dwtr]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.features :as features]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.store :as st]
|
||||
[app.util.i18n :refer [tr]]
|
||||
@@ -300,11 +302,20 @@
|
||||
update-fn (fn [node _]
|
||||
(-> node
|
||||
(d/txt-merge txt-attrs)
|
||||
(cty/remove-typography-from-node)))]
|
||||
(dwsh/update-shapes shape-ids
|
||||
#(txt/update-text-content % update-node? update-fn nil)
|
||||
{:ignore-touched true
|
||||
:page-id page-id})))
|
||||
(cty/remove-typography-from-node)))
|
||||
;; Check if any attribute affects text layout (requires resize)
|
||||
affects-layout? (some #(contains? txt-attrs %) [:font-size :font-family :font-weight :letter-spacing :line-height])]
|
||||
(ptk/reify ::generate-text-shape-update
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(cond-> (rx/of (dwsh/update-shapes shape-ids
|
||||
#(txt/update-text-content % update-node? update-fn nil)
|
||||
{:ignore-touched true
|
||||
:page-id page-id}))
|
||||
(and affects-layout?
|
||||
(features/active-feature? state "render-wasm/v1"))
|
||||
(rx/merge
|
||||
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
|
||||
|
||||
(defn update-line-height
|
||||
([value shape-ids attributes] (update-line-height value shape-ids attributes nil))
|
||||
@@ -353,11 +364,17 @@
|
||||
(-> node
|
||||
(d/txt-merge txt-attrs)
|
||||
(cty/remove-typography-from-node))))]
|
||||
(dwsh/update-shapes shape-ids
|
||||
(fn [shape]
|
||||
(txt/update-text-content shape update-node? #(update-fn %1 (ctst/font-weight-applied? shape)) nil))
|
||||
{:ignore-touched true
|
||||
:page-id page-id})))
|
||||
(ptk/reify ::generate-font-family-text-shape-update
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(cond-> (rx/of (dwsh/update-shapes shape-ids
|
||||
(fn [shape]
|
||||
(txt/update-text-content shape update-node? #(update-fn %1 (ctst/font-weight-applied? shape)) nil))
|
||||
{:ignore-touched true
|
||||
:page-id page-id}))
|
||||
(features/active-feature? state "render-wasm/v1")
|
||||
(rx/merge
|
||||
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
|
||||
|
||||
(defn- create-font-family-text-attrs
|
||||
[value]
|
||||
@@ -425,10 +442,16 @@
|
||||
(-> node
|
||||
(d/txt-merge txt-attrs)
|
||||
(cty/remove-typography-from-node))))]
|
||||
(dwsh/update-shapes shape-ids
|
||||
#(txt/update-text-content % update-node? update-fn nil)
|
||||
{:ignore-touched true
|
||||
:page-id page-id})))
|
||||
(ptk/reify ::generate-font-weight-text-shape-update
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(cond-> (rx/of (dwsh/update-shapes shape-ids
|
||||
#(txt/update-text-content % update-node? update-fn nil)
|
||||
{:ignore-touched true
|
||||
:page-id page-id}))
|
||||
(features/active-feature? state "render-wasm/v1")
|
||||
(rx/merge
|
||||
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
|
||||
|
||||
(defn update-font-weight
|
||||
([value shape-ids attributes] (update-font-weight value shape-ids attributes nil))
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
touched? (and (contains? (:data @form) input-name)
|
||||
(get-in @form [:touched input-name]))
|
||||
|
||||
error (get-in @form [:errors input-name])
|
||||
|
||||
value (get-in @form [:data input-name] "")
|
||||
@@ -52,7 +53,8 @@
|
||||
(let [form (mf/use-ctx context)
|
||||
disabled? (or (and (some? form)
|
||||
(or (not (:valid @form))
|
||||
(seq (:external-errors @form))))
|
||||
(seq (:async-errors @form))
|
||||
(seq (:extra-errors @form))))
|
||||
(true? disabled))
|
||||
handle-key-down-save
|
||||
(mf/use-fn
|
||||
|
||||
@@ -236,12 +236,14 @@
|
||||
(on-composite-input-change form field value false))
|
||||
([form field value trim?]
|
||||
(letfn [(clean-errors [errors]
|
||||
(-> errors
|
||||
(dissoc field)
|
||||
(not-empty)))]
|
||||
(some-> errors
|
||||
(update :value #(when (map? %) (dissoc % field)))
|
||||
(update :value #(when (seq %) %))
|
||||
(not-empty)))]
|
||||
(swap! form (fn [state]
|
||||
(-> state
|
||||
(assoc-in [:data :value field] (if trim? (str/trim value) value))
|
||||
(assoc-in [:touched :value field] true)
|
||||
(update :errors clean-errors)
|
||||
(update :extra-errors clean-errors)))))))
|
||||
|
||||
@@ -257,6 +259,9 @@
|
||||
value
|
||||
(get-in @form [:data :value input-name] "")
|
||||
|
||||
touched?
|
||||
(get-in @form [:touched :value input-name])
|
||||
|
||||
resolve-stream
|
||||
(mf/with-memo [token]
|
||||
(if-let [value (get-in token [:value input-name])]
|
||||
@@ -284,7 +289,7 @@
|
||||
:hint-message (:message hint)
|
||||
:hint-type (:type hint)})
|
||||
props
|
||||
(if error
|
||||
(if (and touched? error)
|
||||
(mf/spread-props props {:hint-type "error"
|
||||
:hint-message (:message error)})
|
||||
props)
|
||||
@@ -332,6 +337,7 @@
|
||||
message (tr "workspace.tokens.resolved-value" (or resolved-value value))]
|
||||
(swap! form update :errors dissoc :value)
|
||||
(swap! form update :extra-errors dissoc :value)
|
||||
(swap! form update :async-errors dissoc :reference)
|
||||
(if (= input-value (str resolved-value))
|
||||
(reset! hint* {})
|
||||
(reset! hint* {:message message :type "hint"})))))))]
|
||||
|
||||
@@ -101,13 +101,6 @@
|
||||
active-tab* (mf/use-state #(if (cft/is-reference? token) :reference :composite))
|
||||
active-tab (deref active-tab*)
|
||||
|
||||
on-toggle-tab
|
||||
(mf/use-fn
|
||||
(mf/deps)
|
||||
(fn [new-tab]
|
||||
(let [new-tab (keyword new-tab)]
|
||||
(reset! active-tab* new-tab))))
|
||||
|
||||
token
|
||||
(mf/with-memo [token]
|
||||
(or token {:type token-type}))
|
||||
@@ -144,6 +137,17 @@
|
||||
(fm/use-form :schema schema
|
||||
:initial initial)
|
||||
|
||||
on-toggle-tab
|
||||
(mf/use-fn
|
||||
(mf/deps form)
|
||||
(fn [new-tab]
|
||||
(let [new-tab (keyword new-tab)]
|
||||
(if (= new-tab :reference)
|
||||
(swap! form assoc-in [:async-errors :reference]
|
||||
{:message "Need valid reference"})
|
||||
(swap! form update :async-errors dissoc :reference))
|
||||
(reset! active-tab* new-tab))))
|
||||
|
||||
warning-name-change?
|
||||
(not= (get-in @form [:data :name])
|
||||
(:name initial))
|
||||
|
||||
@@ -291,6 +291,7 @@
|
||||
[:color {:optional true} [:maybe :string]]
|
||||
[:color-result {:optional true} ::sm/any]
|
||||
[:inset {:optional true} [:maybe :boolean]]]]]
|
||||
|
||||
(if (= active-tab :reference)
|
||||
[:reference {:optional false} ::sm/text]
|
||||
[:reference {:optional true} [:maybe :string]])]]
|
||||
|
||||
@@ -1185,7 +1185,6 @@
|
||||
{:cmd :export-shapes
|
||||
:profile-id (:profile-id @st/state)
|
||||
:wait true
|
||||
:skip-children (:skip-children value false)
|
||||
:exports [{:file-id file-id
|
||||
:page-id page-id
|
||||
:object-id id
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
[app.main.ui.shapes.text]
|
||||
[app.main.worker :as mw]
|
||||
[app.render-wasm.api.fonts :as f]
|
||||
[app.render-wasm.api.shapes :as shapes]
|
||||
[app.render-wasm.api.texts :as t]
|
||||
[app.render-wasm.api.webgl :as webgl]
|
||||
[app.render-wasm.deserializers :as dr]
|
||||
@@ -68,12 +69,25 @@
|
||||
(def ^:const DEBOUNCE_DELAY_MS 100)
|
||||
(def ^:const THROTTLE_DELAY_MS 10)
|
||||
|
||||
;; Number of shapes to process before yielding to browser
|
||||
(def ^:const SHAPES_CHUNK_SIZE 100)
|
||||
;; Threshold below which we use synchronous processing (no chunking overhead)
|
||||
(def ^:const ASYNC_THRESHOLD 100)
|
||||
|
||||
(def dpr
|
||||
(if use-dpr? (if (exists? js/window) js/window.devicePixelRatio 1.0) 1.0))
|
||||
|
||||
(def noop-fn
|
||||
(constantly nil))
|
||||
|
||||
(defn- yield-to-browser
|
||||
"Returns a promise that resolves after yielding to the browser's event loop.
|
||||
Uses requestAnimationFrame for smooth visual updates during loading."
|
||||
[]
|
||||
(p/create
|
||||
(fn [resolve _reject]
|
||||
(js/requestAnimationFrame (fn [_] (resolve nil))))))
|
||||
|
||||
;; Based on app.main.render/object-svg
|
||||
(mf/defc object-svg
|
||||
{::mf/props :obj}
|
||||
@@ -120,17 +134,56 @@
|
||||
(aget buffer 3))
|
||||
(set! wasm/internal-frame-id nil))))
|
||||
|
||||
(defn render-preview!
|
||||
"Render a lightweight preview without tile caching.
|
||||
Used during progressive loading for fast feedback."
|
||||
[]
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(h/call wasm/internal-module "_render_preview")))
|
||||
|
||||
|
||||
(defonce pending-render (atom false))
|
||||
(defonce shapes-loading? (atom false))
|
||||
(defonce deferred-render? (atom false))
|
||||
|
||||
(defn- register-deferred-render!
|
||||
[]
|
||||
(reset! deferred-render? true))
|
||||
|
||||
(defn request-render
|
||||
[_requester]
|
||||
(when (and wasm/context-initialized? (not @pending-render) (not @wasm/context-lost?))
|
||||
(reset! pending-render true)
|
||||
(js/requestAnimationFrame
|
||||
(fn [ts]
|
||||
(reset! pending-render false)
|
||||
(render ts)))))
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(if @shapes-loading?
|
||||
(register-deferred-render!)
|
||||
(when-not @pending-render
|
||||
(reset! pending-render true)
|
||||
(let [frame-id
|
||||
(js/requestAnimationFrame
|
||||
(fn [ts]
|
||||
(reset! pending-render false)
|
||||
(set! wasm/internal-frame-id nil)
|
||||
(render ts)))]
|
||||
(set! wasm/internal-frame-id frame-id))))))
|
||||
|
||||
(defn- begin-shapes-loading!
|
||||
[]
|
||||
(reset! shapes-loading? true)
|
||||
(let [frame-id wasm/internal-frame-id
|
||||
was-pending @pending-render]
|
||||
(when frame-id
|
||||
(js/cancelAnimationFrame frame-id)
|
||||
(set! wasm/internal-frame-id nil))
|
||||
(reset! pending-render false)
|
||||
(reset! deferred-render? was-pending)))
|
||||
|
||||
(defn- end-shapes-loading!
|
||||
[]
|
||||
(let [was-loading (compare-and-set! shapes-loading? true false)]
|
||||
(reset! deferred-render? false)
|
||||
;; Always trigger a render after loading completes
|
||||
;; This ensures shapes are displayed even if no deferred render was requested
|
||||
(when was-loading
|
||||
(request-render "set-objects:flush"))))
|
||||
|
||||
(declare get-text-dimensions)
|
||||
|
||||
@@ -895,24 +948,12 @@
|
||||
id (dm/get-prop shape :id)
|
||||
type (dm/get-prop shape :type)
|
||||
|
||||
parent-id (get shape :parent-id)
|
||||
masked (get shape :masked-group)
|
||||
selrect (get shape :selrect)
|
||||
constraint-h (get shape :constraints-h)
|
||||
constraint-v (get shape :constraints-v)
|
||||
clip-content (if (= type :frame)
|
||||
(not (get shape :show-content))
|
||||
false)
|
||||
rotation (get shape :rotation)
|
||||
transform (get shape :transform)
|
||||
|
||||
fills (get shape :fills)
|
||||
strokes (if (= type :group)
|
||||
[] (get shape :strokes))
|
||||
children (get shape :shapes)
|
||||
blend-mode (get shape :blend-mode)
|
||||
opacity (get shape :opacity)
|
||||
hidden (get shape :hidden)
|
||||
content (let [content (get shape :content)]
|
||||
(if (= type :text)
|
||||
(ensure-text-content content)
|
||||
@@ -921,22 +962,12 @@
|
||||
grow-type (get shape :grow-type)
|
||||
blur (get shape :blur)
|
||||
svg-attrs (get shape :svg-attrs)
|
||||
shadows (get shape :shadow)
|
||||
corners (map #(get shape %) [:r1 :r2 :r3 :r4])]
|
||||
shadows (get shape :shadow)]
|
||||
|
||||
(use-shape id)
|
||||
(set-parent-id parent-id)
|
||||
(set-shape-type type)
|
||||
(set-shape-clip-content clip-content)
|
||||
(set-shape-constraints constraint-h constraint-v)
|
||||
(shapes/set-shape-base-props shape)
|
||||
|
||||
(set-shape-rotation rotation)
|
||||
(set-shape-transform transform)
|
||||
(set-shape-blend-mode blend-mode)
|
||||
(set-shape-opacity opacity)
|
||||
(set-shape-hidden hidden)
|
||||
;; Remaining properties that need separate calls (variable-length or conditional)
|
||||
(set-shape-children children)
|
||||
(set-shape-corners corners)
|
||||
(set-shape-blur blur)
|
||||
(when (= type :group)
|
||||
(set-masked (boolean masked)))
|
||||
@@ -956,7 +987,6 @@
|
||||
|
||||
(set-shape-layout shape)
|
||||
(set-layout-data shape)
|
||||
(set-shape-selrect selrect)
|
||||
|
||||
(let [pending_thumbnails (into [] (concat
|
||||
(set-shape-text-content id content)
|
||||
@@ -1012,30 +1042,143 @@
|
||||
(let [{:keys [thumbnails full]} (set-object shape)]
|
||||
(process-pending [shape] thumbnails full noop-fn)))
|
||||
|
||||
(defn- process-shapes-chunk
|
||||
"Process a chunk of shapes synchronously, returning accumulated pending operations.
|
||||
Returns {:thumbnails [...] :full [...] :next-index n}"
|
||||
[shapes start-index chunk-size thumbnails-acc full-acc]
|
||||
(let [total (count shapes)
|
||||
end-index (min total (+ start-index chunk-size))]
|
||||
(loop [index start-index
|
||||
t-acc thumbnails-acc
|
||||
f-acc full-acc]
|
||||
(if (< index end-index)
|
||||
(let [shape (nth shapes index)
|
||||
{:keys [thumbnails full]} (set-object shape)]
|
||||
(recur (inc index)
|
||||
(into t-acc thumbnails)
|
||||
(into f-acc full)))
|
||||
{:thumbnails t-acc
|
||||
:full f-acc
|
||||
:next-index end-index}))))
|
||||
|
||||
(defn- set-objects-async
|
||||
"Asynchronously process shapes in chunks, yielding to the browser between chunks.
|
||||
Returns a promise that resolves when all shapes are processed.
|
||||
|
||||
Renders a preview only periodically during loading to show progress,
|
||||
then does a full tile-based render at the end."
|
||||
[shapes render-callback]
|
||||
(let [total-shapes (count shapes)
|
||||
total-chunks (mth/ceil (/ total-shapes SHAPES_CHUNK_SIZE))
|
||||
;; Render at 25%, 50%, 75% of loading
|
||||
render-at-chunks (set [(mth/floor (* total-chunks 0.25))
|
||||
(mth/floor (* total-chunks 0.5))
|
||||
(mth/floor (* total-chunks 0.75))])]
|
||||
(p/create
|
||||
(fn [resolve _reject]
|
||||
(letfn [(process-next-chunk [index thumbnails-acc full-acc chunk-count]
|
||||
(if (< index total-shapes)
|
||||
;; Process one chunk
|
||||
(let [{:keys [thumbnails full next-index]}
|
||||
(process-shapes-chunk shapes index SHAPES_CHUNK_SIZE
|
||||
thumbnails-acc full-acc)
|
||||
new-chunk-count (inc chunk-count)]
|
||||
;; Only render at specific progress milestones
|
||||
(when (contains? render-at-chunks new-chunk-count)
|
||||
(render-preview!))
|
||||
|
||||
;; Yield to browser, then continue with next chunk
|
||||
(-> (yield-to-browser)
|
||||
(p/then (fn [_]
|
||||
(process-next-chunk next-index thumbnails full new-chunk-count)))))
|
||||
;; All chunks done - finalize
|
||||
(do
|
||||
(perf/end-measure "set-objects")
|
||||
(process-pending shapes thumbnails-acc full-acc noop-fn
|
||||
(fn []
|
||||
(end-shapes-loading!)
|
||||
(if render-callback
|
||||
(render-callback)
|
||||
(render-finish))
|
||||
(ug/dispatch! (ug/event "penpot:wasm:set-objects"))
|
||||
(resolve nil))))))]
|
||||
(process-next-chunk 0 [] [] 0))))))
|
||||
|
||||
(defn- set-objects-sync
|
||||
"Synchronously process all shapes (for small shape counts)."
|
||||
[shapes render-callback]
|
||||
(let [total-shapes (count shapes)
|
||||
{:keys [thumbnails full]}
|
||||
(loop [index 0 thumbnails-acc [] full-acc []]
|
||||
(if (< index total-shapes)
|
||||
(let [shape (nth shapes index)
|
||||
{:keys [thumbnails full]} (set-object shape)]
|
||||
(recur (inc index)
|
||||
(into thumbnails-acc thumbnails)
|
||||
(into full-acc full)))
|
||||
{:thumbnails thumbnails-acc :full full-acc}))]
|
||||
(perf/end-measure "set-objects")
|
||||
(process-pending shapes thumbnails full noop-fn
|
||||
(fn []
|
||||
(if render-callback
|
||||
(render-callback)
|
||||
(render-finish))
|
||||
(ug/dispatch! (ug/event "penpot:wasm:set-objects"))))))
|
||||
|
||||
(defn- shapes-in-tree-order
|
||||
"Returns shapes sorted in tree order (parents before children).
|
||||
This ensures parent shapes are processed before their children,
|
||||
maintaining proper shape reference consistency in WASM."
|
||||
[objects]
|
||||
;; Get IDs in tree order starting from root (uuid/zero)
|
||||
;; If root doesn't exist (e.g., filtered thumbnail data), fall back to
|
||||
;; finding top-level shapes (those without a parent in objects) and
|
||||
;; traversing from there.
|
||||
(if (contains? objects uuid/zero)
|
||||
;; Normal case: traverse from root
|
||||
(let [ordered-ids (cfh/get-children-ids-with-self objects uuid/zero)]
|
||||
(into []
|
||||
(keep #(get objects %))
|
||||
ordered-ids))
|
||||
;; Fallback for filtered data (thumbnails): find top-level shapes and traverse
|
||||
(let [;; Find shapes whose parent is not in the objects map (top-level in this subset)
|
||||
top-level-ids (->> (vals objects)
|
||||
(filter (fn [shape]
|
||||
(not (contains? objects (:parent-id shape)))))
|
||||
(map :id))
|
||||
;; Get all children in order for each top-level shape
|
||||
all-ordered-ids (into []
|
||||
(mapcat #(cfh/get-children-ids-with-self objects %))
|
||||
top-level-ids)]
|
||||
(into []
|
||||
(keep #(get objects %))
|
||||
all-ordered-ids))))
|
||||
|
||||
(defn set-objects
|
||||
"Set all shape objects for rendering.
|
||||
|
||||
Shapes are processed in tree order (parents before children)
|
||||
to maintain proper shape reference consistency in WASM."
|
||||
([objects]
|
||||
(set-objects objects nil))
|
||||
([objects render-callback]
|
||||
(perf/begin-measure "set-objects")
|
||||
(let [shapes (into [] (vals objects))
|
||||
total-shapes (count shapes)
|
||||
;; Collect pending operations - set-object returns {:thumbnails [...] :full [...]}
|
||||
{:keys [thumbnails full]}
|
||||
(loop [index 0 thumbnails-acc [] full-acc []]
|
||||
(if (< index total-shapes)
|
||||
(let [shape (nth shapes index)
|
||||
{:keys [thumbnails full]} (set-object shape)]
|
||||
(recur (inc index)
|
||||
(into thumbnails-acc thumbnails)
|
||||
(into full-acc full)))
|
||||
{:thumbnails thumbnails-acc :full full-acc}))]
|
||||
(perf/end-measure "set-objects")
|
||||
(process-pending shapes thumbnails full noop-fn
|
||||
(fn []
|
||||
(if render-callback
|
||||
(render-callback)
|
||||
(render-finish))
|
||||
(ug/dispatch! (ug/event "penpot:wasm:set-objects")))))))
|
||||
(let [shapes (shapes-in-tree-order objects)
|
||||
total-shapes (count shapes)]
|
||||
(if (< total-shapes ASYNC_THRESHOLD)
|
||||
(set-objects-sync shapes render-callback)
|
||||
(do
|
||||
(begin-shapes-loading!)
|
||||
(try
|
||||
(-> (set-objects-async shapes render-callback)
|
||||
(p/catch (fn [error]
|
||||
(end-shapes-loading!)
|
||||
(js/console.error "Async WASM shape loading failed" error))))
|
||||
(catch :default error
|
||||
(end-shapes-loading!)
|
||||
(js/console.error "Async WASM shape loading failed" error)
|
||||
(throw error)))
|
||||
nil)))))
|
||||
|
||||
(defn clear-focus-mode
|
||||
[]
|
||||
|
||||
193
frontend/src/app/render_wasm/api/shapes.cljs
Normal file
193
frontend/src/app/render_wasm/api/shapes.cljs
Normal file
@@ -0,0 +1,193 @@
|
||||
;; 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 app.render-wasm.api.shapes
|
||||
"Batched shape property serialization for improved WASM performance.
|
||||
|
||||
This module provides a single WASM call to set all base shape properties,
|
||||
replacing multiple individual calls (use_shape, set_parent, set_shape_type,
|
||||
etc.) with one batched operation."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.render-wasm.helpers :as h]
|
||||
[app.render-wasm.mem :as mem]
|
||||
[app.render-wasm.serializers :as sr]
|
||||
[app.render-wasm.wasm :as wasm]))
|
||||
|
||||
;; Binary layout constants matching Rust implementation:
|
||||
;;
|
||||
;; | Offset | Size | Field | Type |
|
||||
;; |--------|------|--------------|-----------------------------------|
|
||||
;; | 0 | 16 | id | UUID (4 × u32 LE) |
|
||||
;; | 16 | 16 | parent_id | UUID (4 × u32 LE) |
|
||||
;; | 32 | 1 | shape_type | u8 |
|
||||
;; | 33 | 1 | flags | u8 (bit0: clip, bit1: hidden) |
|
||||
;; | 34 | 1 | blend_mode | u8 |
|
||||
;; | 35 | 1 | constraint_h | u8 (0xFF = None) |
|
||||
;; | 36 | 1 | constraint_v | u8 (0xFF = None) |
|
||||
;; | 37 | 3 | padding | - |
|
||||
;; | 40 | 4 | opacity | f32 LE |
|
||||
;; | 44 | 4 | rotation | f32 LE |
|
||||
;; | 48 | 24 | transform | 6 × f32 LE (a,b,c,d,e,f) |
|
||||
;; | 72 | 16 | selrect | 4 × f32 LE (x1,y1,x2,y2) |
|
||||
;; | 88 | 16 | corners | 4 × f32 LE (r1,r2,r3,r4) |
|
||||
;; |--------|------|--------------|-----------------------------------|
|
||||
;; | Total | 104 | | |
|
||||
|
||||
(def ^:const BASE-PROPS-SIZE 104)
|
||||
(def ^:const FLAG-CLIP-CONTENT 0x01)
|
||||
(def ^:const FLAG-HIDDEN 0x02)
|
||||
(def ^:const CONSTRAINT-NONE 0xFF)
|
||||
|
||||
(defn- write-uuid-to-heap
|
||||
"Write a UUID to the heap at the given byte offset using DataView."
|
||||
[dview offset id]
|
||||
(let [buffer (uuid/get-u32 id)]
|
||||
(.setUint32 dview offset (aget buffer 0) true)
|
||||
(.setUint32 dview (+ offset 4) (aget buffer 1) true)
|
||||
(.setUint32 dview (+ offset 8) (aget buffer 2) true)
|
||||
(.setUint32 dview (+ offset 12) (aget buffer 3) true)))
|
||||
|
||||
(defn- serialize-transform
|
||||
"Extract transform matrix values, defaulting to identity matrix."
|
||||
[transform]
|
||||
(if (some? transform)
|
||||
[(dm/get-prop transform :a)
|
||||
(dm/get-prop transform :b)
|
||||
(dm/get-prop transform :c)
|
||||
(dm/get-prop transform :d)
|
||||
(dm/get-prop transform :e)
|
||||
(dm/get-prop transform :f)]
|
||||
[1.0 0.0 0.0 1.0 0.0 0.0])) ; identity matrix
|
||||
|
||||
(defn- serialize-selrect
|
||||
"Extract selrect values."
|
||||
[selrect]
|
||||
(if (some? selrect)
|
||||
[(dm/get-prop selrect :x1)
|
||||
(dm/get-prop selrect :y1)
|
||||
(dm/get-prop selrect :x2)
|
||||
(dm/get-prop selrect :y2)]
|
||||
[0.0 0.0 0.0 0.0]))
|
||||
|
||||
(defn set-shape-base-props
|
||||
"Set all base shape properties in a single WASM call.
|
||||
|
||||
This replaces the following individual calls:
|
||||
- use-shape
|
||||
- set-parent-id
|
||||
- set-shape-type
|
||||
- set-shape-clip-content
|
||||
- set-shape-rotation
|
||||
- set-shape-transform
|
||||
- set-shape-blend-mode
|
||||
- set-shape-opacity
|
||||
- set-shape-hidden
|
||||
- set-shape-selrect
|
||||
- set-shape-corners
|
||||
- set-shape-constraints (clear + h + v)
|
||||
|
||||
Returns nil."
|
||||
[shape]
|
||||
(when wasm/context-initialized?
|
||||
(let [id (dm/get-prop shape :id)
|
||||
parent-id (get shape :parent-id)
|
||||
shape-type (dm/get-prop shape :type)
|
||||
|
||||
clip-content (if (= shape-type :frame)
|
||||
(not (get shape :show-content))
|
||||
false)
|
||||
hidden (get shape :hidden false)
|
||||
|
||||
flags (cond-> 0
|
||||
clip-content (bit-or FLAG-CLIP-CONTENT)
|
||||
hidden (bit-or FLAG-HIDDEN))
|
||||
|
||||
blend-mode (sr/translate-blend-mode (get shape :blend-mode))
|
||||
constraint-h (let [c (get shape :constraints-h)]
|
||||
(if (some? c)
|
||||
(sr/translate-constraint-h c)
|
||||
CONSTRAINT-NONE))
|
||||
constraint-v (let [c (get shape :constraints-v)]
|
||||
(if (some? c)
|
||||
(sr/translate-constraint-v c)
|
||||
CONSTRAINT-NONE))
|
||||
|
||||
opacity (d/nilv (get shape :opacity) 1.0)
|
||||
rotation (d/nilv (get shape :rotation) 0.0)
|
||||
|
||||
;; Transform matrix
|
||||
[ta tb tc td te tf] (serialize-transform (get shape :transform))
|
||||
|
||||
;; Selrect
|
||||
selrect (get shape :selrect)
|
||||
[sx1 sy1 sx2 sy2] (serialize-selrect selrect)
|
||||
|
||||
;; Corners
|
||||
r1 (d/nilv (get shape :r1) 0.0)
|
||||
r2 (d/nilv (get shape :r2) 0.0)
|
||||
r3 (d/nilv (get shape :r3) 0.0)
|
||||
r4 (d/nilv (get shape :r4) 0.0)
|
||||
|
||||
;; Allocate buffer and get DataView
|
||||
offset (mem/alloc BASE-PROPS-SIZE)
|
||||
heap (mem/get-heap-u8)
|
||||
dview (js/DataView. (.-buffer heap))]
|
||||
|
||||
;; Write id (offset 0, 16 bytes)
|
||||
(write-uuid-to-heap dview offset id)
|
||||
|
||||
;; Write parent_id (offset 16, 16 bytes)
|
||||
(write-uuid-to-heap dview (+ offset 16) (d/nilv parent-id uuid/zero))
|
||||
|
||||
;; Write shape_type (offset 32, 1 byte)
|
||||
(.setUint8 dview (+ offset 32) (sr/translate-shape-type shape-type))
|
||||
|
||||
;; Write flags (offset 33, 1 byte)
|
||||
(.setUint8 dview (+ offset 33) flags)
|
||||
|
||||
;; Write blend_mode (offset 34, 1 byte)
|
||||
(.setUint8 dview (+ offset 34) blend-mode)
|
||||
|
||||
;; Write constraint_h (offset 35, 1 byte)
|
||||
(.setUint8 dview (+ offset 35) constraint-h)
|
||||
|
||||
;; Write constraint_v (offset 36, 1 byte)
|
||||
(.setUint8 dview (+ offset 36) constraint-v)
|
||||
|
||||
;; Padding at offset 37-39 (already zero from alloc)
|
||||
|
||||
;; Write opacity (offset 40, f32)
|
||||
(.setFloat32 dview (+ offset 40) opacity true)
|
||||
|
||||
;; Write rotation (offset 44, f32)
|
||||
(.setFloat32 dview (+ offset 44) rotation true)
|
||||
|
||||
;; Write transform matrix (offset 48, 6 × f32)
|
||||
(.setFloat32 dview (+ offset 48) ta true)
|
||||
(.setFloat32 dview (+ offset 52) tb true)
|
||||
(.setFloat32 dview (+ offset 56) tc true)
|
||||
(.setFloat32 dview (+ offset 60) td true)
|
||||
(.setFloat32 dview (+ offset 64) te true)
|
||||
(.setFloat32 dview (+ offset 68) tf true)
|
||||
|
||||
;; Write selrect (offset 72, 4 × f32)
|
||||
(.setFloat32 dview (+ offset 72) sx1 true)
|
||||
(.setFloat32 dview (+ offset 76) sy1 true)
|
||||
(.setFloat32 dview (+ offset 80) sx2 true)
|
||||
(.setFloat32 dview (+ offset 84) sy2 true)
|
||||
|
||||
;; Write corners (offset 88, 4 × f32)
|
||||
(.setFloat32 dview (+ offset 88) r1 true)
|
||||
(.setFloat32 dview (+ offset 92) r2 true)
|
||||
(.setFloat32 dview (+ offset 96) r3 true)
|
||||
(.setFloat32 dview (+ offset 100) r4 true)
|
||||
|
||||
(h/call wasm/internal-module "_set_shape_base_props")
|
||||
|
||||
nil)))
|
||||
@@ -227,7 +227,7 @@
|
||||
:svg-attrs
|
||||
(do
|
||||
(api/set-shape-svg-attrs v)
|
||||
;; Always update fills/blur/shadow to clear previous state if filters disappear
|
||||
;; Always update fills/blur/shadow to clear previous state if filters disappear
|
||||
(api/set-shape-fills id (:fills shape) false)
|
||||
(api/set-shape-blur (:blur shape))
|
||||
(api/set-shape-shadows (:shadow shape)))
|
||||
@@ -397,12 +397,18 @@
|
||||
(next es))
|
||||
(throw (js/Error. "conj on a map takes map entries or seqables of map entries"))))))))
|
||||
|
||||
(def ^:private xf:without-id-and-type
|
||||
(remove (fn [kvpair]
|
||||
(let [k (key kvpair)]
|
||||
(or (= k :id)
|
||||
(= k :type))))))
|
||||
|
||||
(defn create-shape
|
||||
"Instanciate a shape from a map"
|
||||
[attrs]
|
||||
(ShapeProxy. (:id attrs)
|
||||
(:type attrs)
|
||||
(dissoc attrs :id :type)))
|
||||
(into {} xf:without-id-and-type attrs)))
|
||||
|
||||
(t/add-handlers!
|
||||
;; We only add a write handler, read handler uses the dynamic dispatch
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
|
||||
(defn- load
|
||||
[locale]
|
||||
(let [path (str "./translation." locale ".js?version=" (:full cf/version))]
|
||||
(let [path (str "./translation." locale ".js?version=" cf/version-tag)]
|
||||
(->> (mod/import path)
|
||||
(p/fmap (fn [result] (unchecked-get result "default")))
|
||||
(p/fnly (fn [data cause]
|
||||
|
||||
@@ -58,6 +58,8 @@
|
||||
|
||||
(swap! state update ::snap snap/update-page old-page new-page)
|
||||
(swap! state update ::selection selection/update-page old-page new-page))
|
||||
(catch :default cause
|
||||
(log/error :hint "error updating page index" :id page-id :cause cause))
|
||||
(finally
|
||||
(let [elapsed (tpoint)]
|
||||
(log/dbg :hint "page index updated" :id page-id :elapsed elapsed ::log/sync? true))))
|
||||
|
||||
@@ -7766,7 +7766,7 @@ msgstr "Line height (multiplicador, px o %) o {alias}"
|
||||
|
||||
#: src/app/main/data/workspace/tokens/errors.cljs:57
|
||||
msgid "workspace.tokens.missing-references"
|
||||
msgstr "Referéncias de tokens no encontradas:"
|
||||
msgstr "Referencias de tokens no encontradas: "
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:123
|
||||
msgid "workspace.tokens.more-options"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export CURRENT_VERSION=${CURRENT_VERSION:-develop};
|
||||
export VERSION_TAG=${VERSION:-develop};
|
||||
|
||||
if [ "$NODE_ENV" = "production" ]; then
|
||||
export BUILD_MODE="release";
|
||||
@@ -81,7 +81,7 @@ function copy_artifacts {
|
||||
cp target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.js $DEST/$BUILD_NAME.js;
|
||||
cp target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.wasm $DEST/$BUILD_NAME.wasm;
|
||||
|
||||
sed -i "s/render_wasm.wasm/$BUILD_NAME.wasm?version=$CURRENT_VERSION/g" $DEST/$BUILD_NAME.js;
|
||||
sed -i "s/render_wasm.wasm/$BUILD_NAME.wasm?version=$VERSION_TAG/g" $DEST/$BUILD_NAME.js;
|
||||
|
||||
yarn esbuild target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.js \
|
||||
--log-level=error \
|
||||
|
||||
@@ -23,7 +23,7 @@ use std::collections::HashMap;
|
||||
use utils::uuid_from_u32_quartet;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) static mut STATE: Option<Box<State<'static>>> = None;
|
||||
pub(crate) static mut STATE: Option<Box<State>> = None;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! with_state_mut {
|
||||
@@ -191,6 +191,20 @@ pub extern "C" fn render_from_cache(_: i32) {
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_preview_mode(enabled: bool) {
|
||||
with_state_mut!(state, {
|
||||
state.render_state.set_preview_mode(enabled);
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn render_preview() {
|
||||
with_state_mut!(state, {
|
||||
state.render_preview(performance::get_time());
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn process_animation_frame(timestamp: i32) {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
@@ -284,6 +298,7 @@ pub extern "C" fn set_view_end() {
|
||||
performance::end_measure!("set_view_end::clear_tile_index");
|
||||
performance::end_timed_log!("clear_tile_index", _clear_start);
|
||||
}
|
||||
state.render_state.sync_cached_viewbox();
|
||||
performance::end_measure!("set_view_end");
|
||||
performance::end_timed_log!("set_view_end", _end_start);
|
||||
#[cfg(feature = "profile-macros")]
|
||||
|
||||
@@ -264,7 +264,6 @@ pub(crate) struct RenderState {
|
||||
pub fonts: FontStore,
|
||||
pub viewbox: Viewbox,
|
||||
pub cached_viewbox: Viewbox,
|
||||
pub cached_target_snapshot: Option<skia::Image>,
|
||||
pub images: ImageStore,
|
||||
pub background_color: skia::Color,
|
||||
// Identifier of the current requestAnimationFrame call, if any.
|
||||
@@ -294,6 +293,8 @@ pub(crate) struct RenderState {
|
||||
/// where we must render shapes without inheriting ancestor layer blurs. Toggle it through
|
||||
/// `with_nested_blurs_suppressed` to ensure it's always restored.
|
||||
pub ignore_nested_blurs: bool,
|
||||
/// Preview render mode - when true, uses simplified rendering for progressive loading
|
||||
pub preview_mode: bool,
|
||||
}
|
||||
|
||||
pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
|
||||
@@ -343,7 +344,6 @@ impl RenderState {
|
||||
fonts,
|
||||
viewbox,
|
||||
cached_viewbox: Viewbox::new(0., 0.),
|
||||
cached_target_snapshot: None,
|
||||
images: ImageStore::new(gpu_state.context.clone()),
|
||||
background_color: skia::Color::TRANSPARENT,
|
||||
render_request_id: None,
|
||||
@@ -366,6 +366,7 @@ impl RenderState {
|
||||
focus_mode: FocusMode::new(),
|
||||
touched_ids: HashSet::default(),
|
||||
ignore_nested_blurs: false,
|
||||
preview_mode: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,6 +487,10 @@ impl RenderState {
|
||||
self.background_color = color;
|
||||
}
|
||||
|
||||
pub fn set_preview_mode(&mut self, enabled: bool) {
|
||||
self.preview_mode = enabled;
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: i32, height: i32) {
|
||||
let dpr_width = (width as f32 * self.options.dpr()).floor() as i32;
|
||||
let dpr_height = (height as f32 * self.options.dpr()).floor() as i32;
|
||||
@@ -1087,15 +1092,12 @@ impl RenderState {
|
||||
let _start = performance::begin_timed_log!("render_from_cache");
|
||||
performance::begin_measure!("render_from_cache");
|
||||
let scale = self.get_cached_scale();
|
||||
if let Some(snapshot) = &self.cached_target_snapshot {
|
||||
let canvas = self.surfaces.canvas(SurfaceId::Target);
|
||||
canvas.save();
|
||||
|
||||
// Check if we have a valid cached viewbox (non-zero dimensions indicate valid cache)
|
||||
if self.cached_viewbox.area.width() > 0.0 {
|
||||
// Scale and translate the target according to the cached data
|
||||
let navigate_zoom = self.viewbox.zoom / self.cached_viewbox.zoom;
|
||||
|
||||
canvas.scale((navigate_zoom, navigate_zoom));
|
||||
|
||||
let TileRect(start_tile_x, start_tile_y, _, _) =
|
||||
tiles::get_tiles_for_viewbox_with_interest(
|
||||
self.cached_viewbox,
|
||||
@@ -1104,15 +1106,24 @@ impl RenderState {
|
||||
);
|
||||
let offset_x = self.viewbox.area.left * self.cached_viewbox.zoom * self.options.dpr();
|
||||
let offset_y = self.viewbox.area.top * self.cached_viewbox.zoom * self.options.dpr();
|
||||
let translate_x = (start_tile_x as f32 * tiles::TILE_SIZE) - offset_x;
|
||||
let translate_y = (start_tile_y as f32 * tiles::TILE_SIZE) - offset_y;
|
||||
let bg_color = self.background_color;
|
||||
|
||||
canvas.translate((
|
||||
(start_tile_x as f32 * tiles::TILE_SIZE) - offset_x,
|
||||
(start_tile_y as f32 * tiles::TILE_SIZE) - offset_y,
|
||||
));
|
||||
// Setup canvas transform
|
||||
{
|
||||
let canvas = self.surfaces.canvas(SurfaceId::Target);
|
||||
canvas.save();
|
||||
canvas.scale((navigate_zoom, navigate_zoom));
|
||||
canvas.translate((translate_x, translate_y));
|
||||
canvas.clear(bg_color);
|
||||
}
|
||||
|
||||
canvas.clear(self.background_color);
|
||||
canvas.draw_image(snapshot, (0, 0), Some(&skia::Paint::default()));
|
||||
canvas.restore();
|
||||
// Draw directly from cache surface, avoiding snapshot overhead
|
||||
self.surfaces.draw_cache_to_target();
|
||||
|
||||
// Restore canvas state
|
||||
self.surfaces.canvas(SurfaceId::Target).restore();
|
||||
|
||||
if self.options.is_debug_visible() {
|
||||
debug::render(self);
|
||||
@@ -1127,6 +1138,25 @@ impl RenderState {
|
||||
performance::end_timed_log!("render_from_cache", _start);
|
||||
}
|
||||
|
||||
/// Render a preview of the shapes during loading.
|
||||
/// This rebuilds tiles for touched shapes and renders synchronously.
|
||||
pub fn render_preview(&mut self, tree: ShapesPoolRef, timestamp: i32) -> Result<(), String> {
|
||||
let _start = performance::begin_timed_log!("render_preview");
|
||||
performance::begin_measure!("render_preview");
|
||||
|
||||
// Skip tile rebuilding during preview - we'll do it at the end
|
||||
// Just rebuild tiles for touched shapes and render synchronously
|
||||
self.rebuild_touched_tiles(tree);
|
||||
|
||||
// Use the sync render path
|
||||
self.start_render_loop(None, tree, timestamp, true)?;
|
||||
|
||||
performance::end_measure!("render_preview");
|
||||
performance::end_timed_log!("render_preview", _start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn start_render_loop(
|
||||
&mut self,
|
||||
base_object: Option<&Uuid>,
|
||||
@@ -1136,6 +1166,7 @@ impl RenderState {
|
||||
) -> Result<(), String> {
|
||||
let _start = performance::begin_timed_log!("start_render_loop");
|
||||
let scale = self.get_scale();
|
||||
|
||||
self.tile_viewbox.update(self.viewbox, scale);
|
||||
|
||||
self.focus_mode.reset();
|
||||
@@ -1560,7 +1591,7 @@ impl RenderState {
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((image, filter_scale)) = filter_result {
|
||||
if let Some((mut surface, filter_scale)) = filter_result {
|
||||
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
||||
drop_canvas.save();
|
||||
drop_canvas.scale((scale, scale));
|
||||
@@ -1570,34 +1601,26 @@ impl RenderState {
|
||||
|
||||
// If we scaled down in the filter surface, we need to scale back up
|
||||
if filter_scale < 1.0 {
|
||||
let scaled_width = bounds.width() * filter_scale;
|
||||
let scaled_height = bounds.height() * filter_scale;
|
||||
let src_rect = skia::Rect::from_xywh(0.0, 0.0, scaled_width, scaled_height);
|
||||
|
||||
drop_canvas.save();
|
||||
drop_canvas.scale((1.0 / filter_scale, 1.0 / filter_scale));
|
||||
drop_canvas.draw_image_rect_with_sampling_options(
|
||||
image,
|
||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
||||
skia::Rect::from_xywh(
|
||||
bounds.left * filter_scale,
|
||||
bounds.top * filter_scale,
|
||||
scaled_width,
|
||||
scaled_height,
|
||||
),
|
||||
drop_canvas.translate((bounds.left * filter_scale, bounds.top * filter_scale));
|
||||
surface.draw(
|
||||
drop_canvas,
|
||||
(0.0, 0.0),
|
||||
self.sampling_options,
|
||||
&drop_paint,
|
||||
Some(&drop_paint),
|
||||
);
|
||||
drop_canvas.restore();
|
||||
} else {
|
||||
let src_rect = skia::Rect::from_xywh(0.0, 0.0, bounds.width(), bounds.height());
|
||||
drop_canvas.draw_image_rect_with_sampling_options(
|
||||
image,
|
||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
||||
bounds,
|
||||
drop_canvas.save();
|
||||
drop_canvas.translate((bounds.left, bounds.top));
|
||||
surface.draw(
|
||||
drop_canvas,
|
||||
(0.0, 0.0),
|
||||
self.sampling_options,
|
||||
&drop_paint,
|
||||
Some(&drop_paint),
|
||||
);
|
||||
drop_canvas.restore();
|
||||
}
|
||||
drop_canvas.restore();
|
||||
}
|
||||
@@ -1621,10 +1644,11 @@ impl RenderState {
|
||||
|
||||
is_empty = false;
|
||||
|
||||
let element = tree.get(&node_id).ok_or(format!(
|
||||
"Error: Element with root_id {} not found in the tree.",
|
||||
node_render_state.id
|
||||
))?;
|
||||
let Some(element) = tree.get(&node_id) else {
|
||||
// The shape isn't available yet (likely still streaming in from WASM).
|
||||
// Skip it for this pass; a subsequent render will pick it up once present.
|
||||
continue;
|
||||
};
|
||||
let scale = self.get_scale();
|
||||
let mut extrect: Option<Rect> = None;
|
||||
|
||||
@@ -1742,7 +1766,9 @@ impl RenderState {
|
||||
if !matches!(element.shape_type, Type::Bool(_)) {
|
||||
// Nested shapes shadowing - apply black shadow to child shapes too
|
||||
for shadow_shape_id in element.children.iter() {
|
||||
let shadow_shape = tree.get(shadow_shape_id).unwrap();
|
||||
let Some(shadow_shape) = tree.get(shadow_shape_id) else {
|
||||
continue;
|
||||
};
|
||||
if shadow_shape.hidden {
|
||||
continue;
|
||||
}
|
||||
@@ -2067,11 +2093,9 @@ impl RenderState {
|
||||
|
||||
self.surfaces.gc();
|
||||
|
||||
// Cache target surface in a texture
|
||||
// Mark cache as valid for render_from_cache
|
||||
self.cached_viewbox = self.viewbox;
|
||||
|
||||
self.cached_target_snapshot = Some(self.surfaces.snapshot(SurfaceId::Cache));
|
||||
|
||||
if self.options.is_debug_visible() {
|
||||
debug::render(self);
|
||||
}
|
||||
@@ -2140,9 +2164,7 @@ impl RenderState {
|
||||
}
|
||||
|
||||
pub fn remove_cached_tile(&mut self, tile: tiles::Tile) {
|
||||
let rect = self.get_aligned_tile_bounds(tile);
|
||||
self.surfaces
|
||||
.remove_cached_tile_surface(tile, rect, self.background_color);
|
||||
self.surfaces.remove_cached_tile_surface(tile);
|
||||
}
|
||||
|
||||
pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) {
|
||||
@@ -2163,7 +2185,7 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
|
||||
// Update the changed tiles
|
||||
// Invalidate changed tiles - old content stays visible until new tiles render
|
||||
self.surfaces.remove_cached_tiles(self.background_color);
|
||||
for tile in all_tiles {
|
||||
self.remove_cached_tile(tile);
|
||||
@@ -2210,7 +2232,7 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
|
||||
// Update the changed tiles
|
||||
// Invalidate changed tiles - old content stays visible until new tiles render
|
||||
self.surfaces.remove_cached_tiles(self.background_color);
|
||||
for tile in all_tiles {
|
||||
self.remove_cached_tile(tile);
|
||||
@@ -2292,6 +2314,10 @@ impl RenderState {
|
||||
(self.viewbox.zoom - self.cached_viewbox.zoom).abs() > f32::EPSILON
|
||||
}
|
||||
|
||||
pub fn sync_cached_viewbox(&mut self) {
|
||||
self.cached_viewbox = self.viewbox;
|
||||
}
|
||||
|
||||
pub fn mark_touched(&mut self, uuid: Uuid) {
|
||||
self.touched_ids.insert(uuid);
|
||||
}
|
||||
|
||||
@@ -40,41 +40,21 @@ pub fn render_with_filter_surface<F>(
|
||||
where
|
||||
F: FnOnce(&mut RenderState, SurfaceId),
|
||||
{
|
||||
if let Some((image, scale)) = render_into_filter_surface(render_state, bounds, draw_fn) {
|
||||
if let Some((mut surface, scale)) = render_into_filter_surface(render_state, bounds, draw_fn) {
|
||||
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
|
||||
|
||||
// If we scaled down, we need to scale the source rect and adjust the destination
|
||||
if scale < 1.0 {
|
||||
// The image was rendered at a smaller scale, so we need to scale it back up
|
||||
let scaled_width = bounds.width() * scale;
|
||||
let scaled_height = bounds.height() * scale;
|
||||
let src_rect = skia::Rect::from_xywh(0.0, 0.0, scaled_width, scaled_height);
|
||||
|
||||
canvas.save();
|
||||
canvas.scale((1.0 / scale, 1.0 / scale));
|
||||
canvas.draw_image_rect_with_sampling_options(
|
||||
image,
|
||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
||||
skia::Rect::from_xywh(
|
||||
bounds.left * scale,
|
||||
bounds.top * scale,
|
||||
scaled_width,
|
||||
scaled_height,
|
||||
),
|
||||
render_state.sampling_options,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
canvas.translate((bounds.left * scale, bounds.top * scale));
|
||||
surface.draw(canvas, (0.0, 0.0), render_state.sampling_options, None);
|
||||
canvas.restore();
|
||||
} else {
|
||||
// No scaling needed, draw normally
|
||||
let src_rect = skia::Rect::from_xywh(0.0, 0.0, bounds.width(), bounds.height());
|
||||
canvas.draw_image_rect_with_sampling_options(
|
||||
image,
|
||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
||||
bounds,
|
||||
render_state.sampling_options,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
canvas.save();
|
||||
canvas.translate((bounds.left, bounds.top));
|
||||
surface.draw(canvas, (0.0, 0.0), render_state.sampling_options, None);
|
||||
canvas.restore();
|
||||
}
|
||||
true
|
||||
} else {
|
||||
@@ -93,7 +73,7 @@ pub fn render_into_filter_surface<F>(
|
||||
render_state: &mut RenderState,
|
||||
bounds: Rect,
|
||||
draw_fn: F,
|
||||
) -> Option<(skia::Image, f32)>
|
||||
) -> Option<(skia::Surface, f32)>
|
||||
where
|
||||
F: FnOnce(&mut RenderState, SurfaceId),
|
||||
{
|
||||
@@ -129,5 +109,6 @@ where
|
||||
|
||||
render_state.surfaces.canvas(filter_id).restore();
|
||||
|
||||
Some((render_state.surfaces.snapshot(filter_id), scale))
|
||||
let filter_surface = render_state.surfaces.surface_clone(filter_id);
|
||||
Some((filter_surface, scale))
|
||||
}
|
||||
|
||||
@@ -175,6 +175,10 @@ impl Surfaces {
|
||||
self.get_mut(id).canvas()
|
||||
}
|
||||
|
||||
pub fn surface_clone(&self, id: SurfaceId) -> skia::Surface {
|
||||
self.get(id).clone()
|
||||
}
|
||||
|
||||
/// Marks a surface as having content (dirty)
|
||||
pub fn mark_dirty(&mut self, id: SurfaceId) {
|
||||
self.dirty_surfaces |= id as u32;
|
||||
@@ -211,6 +215,18 @@ impl Surfaces {
|
||||
);
|
||||
}
|
||||
|
||||
/// Draws the cache surface directly to the target canvas.
|
||||
/// This avoids creating an intermediate snapshot, reducing GPU stalls.
|
||||
pub fn draw_cache_to_target(&mut self) {
|
||||
let sampling_options = self.sampling_options;
|
||||
self.cache.clone().draw(
|
||||
self.target.canvas(),
|
||||
(0.0, 0.0),
|
||||
sampling_options,
|
||||
Some(&skia::Paint::default()),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn apply_mut(&mut self, ids: u32, mut f: impl FnMut(&mut skia::Surface)) {
|
||||
performance::begin_measure!("apply_mut::flags");
|
||||
if ids & SurfaceId::Target as u32 != 0 {
|
||||
@@ -305,6 +321,22 @@ impl Surfaces {
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, id: SurfaceId) -> &skia::Surface {
|
||||
match id {
|
||||
SurfaceId::Target => &self.target,
|
||||
SurfaceId::Filter => &self.filter,
|
||||
SurfaceId::Cache => &self.cache,
|
||||
SurfaceId::Current => &self.current,
|
||||
SurfaceId::DropShadows => &self.drop_shadows,
|
||||
SurfaceId::InnerShadows => &self.inner_shadows,
|
||||
SurfaceId::TextDropShadows => &self.text_drop_shadows,
|
||||
SurfaceId::Fills => &self.shape_fills,
|
||||
SurfaceId::Strokes => &self.shape_strokes,
|
||||
SurfaceId::Debug => &self.debug,
|
||||
SurfaceId::UI => &self.ui,
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_from_target(&mut self, target: skia::Surface) {
|
||||
let dim = (target.width(), target.height());
|
||||
self.target = target;
|
||||
@@ -386,14 +418,22 @@ impl Surfaces {
|
||||
self.current.height() - TILE_SIZE_MULTIPLIER * self.margins.height,
|
||||
);
|
||||
|
||||
if let Some(snapshot) = self.current.image_snapshot_with_bounds(rect) {
|
||||
self.tiles.add(tile_viewbox, tile, snapshot.clone());
|
||||
let snapshot = self.current.image_snapshot();
|
||||
let mut direct_context = self.current.direct_context();
|
||||
let tile_image_opt = snapshot
|
||||
.make_subset(direct_context.as_mut(), rect)
|
||||
.or_else(|| self.current.image_snapshot_with_bounds(rect));
|
||||
|
||||
if let Some(tile_image) = tile_image_opt {
|
||||
// Draw to cache first (takes reference), then move to tile cache
|
||||
self.cache.canvas().draw_image_rect(
|
||||
snapshot.clone(),
|
||||
&tile_image,
|
||||
None,
|
||||
tile_rect,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
|
||||
self.tiles.add(tile_viewbox, tile, tile_image);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,25 +441,65 @@ impl Surfaces {
|
||||
self.tiles.has(tile)
|
||||
}
|
||||
|
||||
pub fn remove_cached_tile_surface(&mut self, tile: Tile, rect: skia::Rect, color: skia::Color) {
|
||||
// Clear the specific tile area in the cache surface with color
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_color(color);
|
||||
self.cache.canvas().draw_rect(rect, &paint);
|
||||
pub fn remove_cached_tile_surface(&mut self, tile: Tile) {
|
||||
// Mark tile as invalid
|
||||
// Old content stays visible until new tile overwrites it atomically,
|
||||
// preventing flickering during tile re-renders.
|
||||
self.tiles.remove(tile);
|
||||
}
|
||||
|
||||
pub fn draw_cached_tile_surface(&mut self, tile: Tile, rect: skia::Rect, color: skia::Color) {
|
||||
let image = self.tiles.get(tile).unwrap();
|
||||
if let Some(image) = self.tiles.get(tile) {
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_color(color);
|
||||
|
||||
self.target.canvas().draw_rect(rect, &paint);
|
||||
|
||||
self.target
|
||||
.canvas()
|
||||
.draw_image_rect(&image, None, rect, &skia::Paint::default());
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws the current tile directly to the target and cache surfaces without
|
||||
/// creating a snapshot. This avoids GPU stalls from ReadPixels but doesn't
|
||||
/// populate the tile texture cache (suitable for one-shot renders like tests).
|
||||
pub fn draw_current_tile_direct(&mut self, tile_rect: &skia::Rect, color: skia::Color) {
|
||||
let sampling_options = self.sampling_options;
|
||||
let src_rect = IRect::from_xywh(
|
||||
self.margins.width,
|
||||
self.margins.height,
|
||||
self.current.width() - TILE_SIZE_MULTIPLIER * self.margins.width,
|
||||
self.current.height() - TILE_SIZE_MULTIPLIER * self.margins.height,
|
||||
);
|
||||
let src_rect_f = skia::Rect::from(src_rect);
|
||||
|
||||
// Draw background
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_color(color);
|
||||
self.target.canvas().draw_rect(tile_rect, &paint);
|
||||
|
||||
self.target.canvas().draw_rect(rect, &paint);
|
||||
// Draw current surface directly to target (no snapshot)
|
||||
self.current.clone().draw(
|
||||
self.target.canvas(),
|
||||
(
|
||||
tile_rect.left - src_rect_f.left,
|
||||
tile_rect.top - src_rect_f.top,
|
||||
),
|
||||
sampling_options,
|
||||
None,
|
||||
);
|
||||
|
||||
self.target
|
||||
.canvas()
|
||||
.draw_image_rect(&image, None, rect, &skia::Paint::default());
|
||||
// Also draw to cache for render_from_cache
|
||||
self.current.clone().draw(
|
||||
self.cache.canvas(),
|
||||
(
|
||||
tile_rect.left - src_rect_f.left,
|
||||
tile_rect.top - src_rect_f.top,
|
||||
),
|
||||
sampling_options,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn remove_cached_tiles(&mut self, color: skia::Color) {
|
||||
@@ -492,9 +572,11 @@ impl TileTextureCache {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&mut self, tile: Tile) -> Result<&mut skia::Image, String> {
|
||||
let image = self.grid.get_mut(&tile).unwrap();
|
||||
Ok(image)
|
||||
pub fn get(&mut self, tile: Tile) -> Option<&mut skia::Image> {
|
||||
if self.removed.contains(&tile) {
|
||||
return None;
|
||||
}
|
||||
self.grid.get_mut(&tile)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, tile: Tile) {
|
||||
|
||||
@@ -1529,6 +1529,7 @@ impl Shape {
|
||||
|| !self.transform.is_identity()
|
||||
|| !math::is_close_to(self.rotation, 0.0)
|
||||
|| matches!(self.shape_type, Type::Group(_) | Type::Frame(_))
|
||||
|| matches!(self.shape_type, Type::Text(_))
|
||||
}
|
||||
|
||||
pub fn count_visible_inner_strokes(&self) -> usize {
|
||||
|
||||
@@ -18,16 +18,16 @@ use crate::shapes::modifiers::grid_layout::grid_cell_data;
|
||||
/// It is created by [init] and passed to the other exported functions.
|
||||
/// Note that rust-skia data structures are not thread safe, so a state
|
||||
/// must not be shared between different Web Workers.
|
||||
pub(crate) struct State<'a> {
|
||||
pub(crate) struct State {
|
||||
pub render_state: RenderState,
|
||||
pub text_editor_state: TextEditorState,
|
||||
pub current_id: Option<Uuid>,
|
||||
pub current_browser: u8,
|
||||
pub shapes: ShapesPool<'a>,
|
||||
pub saved_shapes: Option<ShapesPool<'a>>,
|
||||
pub shapes: ShapesPool,
|
||||
pub saved_shapes: Option<ShapesPool>,
|
||||
}
|
||||
|
||||
impl<'a> State<'a> {
|
||||
impl State {
|
||||
pub fn new(width: i32, height: i32) -> Self {
|
||||
State {
|
||||
render_state: RenderState::new(width, height),
|
||||
@@ -100,6 +100,16 @@ impl<'a> State<'a> {
|
||||
}
|
||||
|
||||
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<(), String> {
|
||||
// If zoom changed, we MUST rebuild the tile index before using it.
|
||||
// Otherwise, the index will have tiles from the old zoom level, causing visible
|
||||
// tiles to appear empty. This can happen if start_render_loop() is called before
|
||||
// set_view_end() finishes rebuilding the index, or if set_view_end() hasn't been
|
||||
// called yet.
|
||||
let zoom_changed = self.render_state.zoom_changed();
|
||||
if zoom_changed {
|
||||
self.rebuild_tiles_shallow();
|
||||
}
|
||||
|
||||
self.render_state
|
||||
.start_render_loop(None, &self.shapes, timestamp, false)?;
|
||||
Ok(())
|
||||
@@ -213,17 +223,14 @@ impl<'a> State<'a> {
|
||||
self.render_state.rebuild_touched_tiles(&self.shapes);
|
||||
}
|
||||
|
||||
pub fn render_preview(&mut self, timestamp: i32) {
|
||||
let _ = self.render_state.render_preview(&self.shapes, timestamp);
|
||||
}
|
||||
|
||||
pub fn rebuild_modifier_tiles(&mut self, ids: Vec<Uuid>) {
|
||||
// SAFETY: We're extending the lifetime of the mutable borrow to 'a.
|
||||
// This is safe because:
|
||||
// 1. shapes has lifetime 'a in the struct
|
||||
// 2. The reference won't outlive the struct
|
||||
// 3. No other references to shapes exist during this call
|
||||
unsafe {
|
||||
let shapes_ptr = &mut self.shapes as *mut ShapesPool<'a>;
|
||||
self.render_state
|
||||
.rebuild_modifier_tiles(&mut *shapes_ptr, ids);
|
||||
}
|
||||
// Index-based storage is safe
|
||||
self.render_state
|
||||
.rebuild_modifier_tiles(&mut self.shapes, ids);
|
||||
}
|
||||
|
||||
pub fn font_collection(&self) -> &FontCollection {
|
||||
|
||||
@@ -28,29 +28,44 @@ const SHAPES_POOL_ALLOC_MULTIPLIER: f32 = 1.3;
|
||||
/// Shapes are stored in a `Vec<Shape>`, which keeps the `Shape` instances
|
||||
/// in a contiguous memory block.
|
||||
///
|
||||
pub struct ShapesPoolImpl<'a> {
|
||||
/// # Index-based Design
|
||||
///
|
||||
/// All auxiliary HashMaps (modifiers, structure, scale_content, modified_shape_cache)
|
||||
/// use `usize` indices instead of `&'a Uuid` references. This eliminates:
|
||||
/// - Unsafe lifetime extensions
|
||||
/// - The need for `rebuild_references()` after Vec reallocation
|
||||
/// - Complex lifetime annotations
|
||||
///
|
||||
/// The `uuid_to_idx` HashMap maps `Uuid` (owned) to indices, avoiding lifetime issues.
|
||||
///
|
||||
pub struct ShapesPoolImpl {
|
||||
shapes: Vec<Shape>,
|
||||
counter: usize,
|
||||
|
||||
shapes_uuid_to_idx: HashMap<&'a Uuid, usize>,
|
||||
/// Maps UUID to index in the shapes Vec. Uses owned Uuid, no lifetime needed.
|
||||
uuid_to_idx: HashMap<Uuid, usize>,
|
||||
|
||||
modified_shape_cache: HashMap<&'a Uuid, OnceCell<Shape>>,
|
||||
modifiers: HashMap<&'a Uuid, skia::Matrix>,
|
||||
structure: HashMap<&'a Uuid, Vec<StructureEntry>>,
|
||||
scale_content: HashMap<&'a Uuid, f32>,
|
||||
/// Cache for modified shapes, keyed by index
|
||||
modified_shape_cache: HashMap<usize, OnceCell<Shape>>,
|
||||
/// Transform modifiers, keyed by index
|
||||
modifiers: HashMap<usize, skia::Matrix>,
|
||||
/// Structure entries, keyed by index
|
||||
structure: HashMap<usize, Vec<StructureEntry>>,
|
||||
/// Scale content values, keyed by index
|
||||
scale_content: HashMap<usize, f32>,
|
||||
}
|
||||
|
||||
// Type aliases to avoid writing lifetimes everywhere
|
||||
pub type ShapesPool<'a> = ShapesPoolImpl<'a>;
|
||||
pub type ShapesPoolRef<'a> = &'a ShapesPoolImpl<'a>;
|
||||
pub type ShapesPoolMutRef<'a> = &'a mut ShapesPoolImpl<'a>;
|
||||
// Type aliases - no longer need lifetimes!
|
||||
pub type ShapesPool = ShapesPoolImpl;
|
||||
pub type ShapesPoolRef<'a> = &'a ShapesPoolImpl;
|
||||
pub type ShapesPoolMutRef<'a> = &'a mut ShapesPoolImpl;
|
||||
|
||||
impl<'a> ShapesPoolImpl<'a> {
|
||||
impl ShapesPoolImpl {
|
||||
pub fn new() -> Self {
|
||||
ShapesPoolImpl {
|
||||
shapes: vec![],
|
||||
counter: 0,
|
||||
shapes_uuid_to_idx: HashMap::default(),
|
||||
uuid_to_idx: HashMap::default(),
|
||||
|
||||
modified_shape_cache: HashMap::default(),
|
||||
modifiers: HashMap::default(),
|
||||
@@ -62,15 +77,14 @@ impl<'a> ShapesPoolImpl<'a> {
|
||||
pub fn initialize(&mut self, capacity: usize) {
|
||||
performance::begin_measure!("shapes_pool_initialize");
|
||||
self.counter = 0;
|
||||
self.shapes_uuid_to_idx = HashMap::with_capacity(capacity);
|
||||
self.uuid_to_idx = HashMap::with_capacity(capacity);
|
||||
|
||||
let additional = capacity as i32 - self.shapes.len() as i32;
|
||||
if additional <= 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reserve exact capacity to avoid any future reallocations
|
||||
// This is critical because we store &'a Uuid references that would be invalidated
|
||||
// Reserve extra capacity to avoid future reallocations
|
||||
let target_capacity = (capacity as f32 * SHAPES_POOL_ALLOC_MULTIPLIER) as usize;
|
||||
self.shapes
|
||||
.reserve_exact(target_capacity.saturating_sub(self.shapes.len()));
|
||||
@@ -81,15 +95,15 @@ impl<'a> ShapesPoolImpl<'a> {
|
||||
}
|
||||
|
||||
pub fn add_shape(&mut self, id: Uuid) -> &mut Shape {
|
||||
let did_reallocate = if self.counter >= self.shapes.len() {
|
||||
// We need more space. Check if we'll need to reallocate the Vec.
|
||||
if self.counter >= self.shapes.len() {
|
||||
// We need more space
|
||||
let current_capacity = self.shapes.capacity();
|
||||
let additional = (self.shapes.len() as f32 * SHAPES_POOL_ALLOC_MULTIPLIER) as usize;
|
||||
// Ensure we add at least 1 shape when the pool is empty
|
||||
let additional =
|
||||
((self.shapes.len() as f32 * SHAPES_POOL_ALLOC_MULTIPLIER) as usize).max(1);
|
||||
let needed_capacity = self.shapes.len() + additional;
|
||||
|
||||
let will_reallocate = needed_capacity > current_capacity;
|
||||
|
||||
if will_reallocate {
|
||||
if needed_capacity > current_capacity {
|
||||
// Reserve extra space to minimize future reallocations
|
||||
let extra_reserve = (needed_capacity as f32 * 0.5) as usize;
|
||||
self.shapes
|
||||
@@ -98,165 +112,68 @@ impl<'a> ShapesPoolImpl<'a> {
|
||||
|
||||
self.shapes
|
||||
.extend(iter::repeat_with(|| Shape::new(Uuid::nil())).take(additional));
|
||||
|
||||
will_reallocate
|
||||
} else {
|
||||
false
|
||||
};
|
||||
}
|
||||
|
||||
let idx = self.counter;
|
||||
let new_shape = &mut self.shapes[idx];
|
||||
new_shape.id = id;
|
||||
|
||||
// Get a reference to the id field in the shape with lifetime 'a
|
||||
// SAFETY: This is safe because:
|
||||
// 1. We pre-allocate enough capacity to avoid Vec reallocation
|
||||
// 2. The shape and its id field won't move within the Vec
|
||||
// 3. The reference won't outlive the ShapesPoolImpl
|
||||
let id_ref: &'a Uuid = unsafe { &*(&self.shapes[idx].id as *const Uuid) };
|
||||
|
||||
self.shapes_uuid_to_idx.insert(id_ref, idx);
|
||||
// Simply store the UUID -> index mapping. No unsafe lifetime tricks needed!
|
||||
self.uuid_to_idx.insert(id, idx);
|
||||
self.counter += 1;
|
||||
|
||||
// If the Vec reallocated, we need to rebuild all references in the HashMaps
|
||||
// because the old references point to deallocated memory
|
||||
if did_reallocate {
|
||||
self.rebuild_references();
|
||||
}
|
||||
|
||||
&mut self.shapes[idx]
|
||||
}
|
||||
|
||||
/// Rebuilds all &'a Uuid references in the HashMaps after a Vec reallocation.
|
||||
/// This is necessary because Vec reallocation invalidates all existing references.
|
||||
fn rebuild_references(&mut self) {
|
||||
// Rebuild shapes_uuid_to_idx with fresh references
|
||||
let mut new_map = HashMap::with_capacity(self.shapes_uuid_to_idx.len());
|
||||
for (_, idx) in self.shapes_uuid_to_idx.drain() {
|
||||
let id_ref: &'a Uuid = unsafe { &*(&self.shapes[idx].id as *const Uuid) };
|
||||
new_map.insert(id_ref, idx);
|
||||
}
|
||||
self.shapes_uuid_to_idx = new_map;
|
||||
|
||||
// Rebuild modifiers with fresh references
|
||||
if !self.modifiers.is_empty() {
|
||||
let old_modifiers: Vec<(Uuid, skia::Matrix)> = self
|
||||
.modifiers
|
||||
.drain()
|
||||
.map(|(uuid_ref, matrix)| (*uuid_ref, matrix))
|
||||
.collect();
|
||||
|
||||
for (uuid, matrix) in old_modifiers {
|
||||
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
|
||||
self.modifiers.insert(uuid_ref, matrix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild structure with fresh references
|
||||
if !self.structure.is_empty() {
|
||||
let old_structure: Vec<(Uuid, Vec<StructureEntry>)> = self
|
||||
.structure
|
||||
.drain()
|
||||
.map(|(uuid_ref, entries)| (*uuid_ref, entries))
|
||||
.collect();
|
||||
|
||||
for (uuid, entries) in old_structure {
|
||||
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
|
||||
self.structure.insert(uuid_ref, entries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild scale_content with fresh references
|
||||
if !self.scale_content.is_empty() {
|
||||
let old_scale_content: Vec<(Uuid, f32)> = self
|
||||
.scale_content
|
||||
.drain()
|
||||
.map(|(uuid_ref, scale)| (*uuid_ref, scale))
|
||||
.collect();
|
||||
|
||||
for (uuid, scale) in old_scale_content {
|
||||
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
|
||||
self.scale_content.insert(uuid_ref, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Rebuild modified_shape_cache with fresh references
|
||||
if !self.modified_shape_cache.is_empty() {
|
||||
let old_cache: Vec<(Uuid, OnceCell<Shape>)> = self
|
||||
.modified_shape_cache
|
||||
.drain()
|
||||
.map(|(uuid_ref, cell)| (*uuid_ref, cell))
|
||||
.collect();
|
||||
|
||||
for (uuid, cell) in old_cache {
|
||||
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
|
||||
self.modified_shape_cache.insert(uuid_ref, cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// No longer needed! Index-based storage means no references to rebuild.
|
||||
// The old rebuild_references() function has been removed entirely.
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.shapes_uuid_to_idx.len()
|
||||
self.uuid_to_idx.len()
|
||||
}
|
||||
|
||||
pub fn has(&self, id: &Uuid) -> bool {
|
||||
self.shapes_uuid_to_idx.contains_key(&id)
|
||||
self.uuid_to_idx.contains_key(id)
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, id: &Uuid) -> Option<&mut Shape> {
|
||||
let idx = *self.shapes_uuid_to_idx.get(&id)?;
|
||||
let idx = *self.uuid_to_idx.get(id)?;
|
||||
Some(&mut self.shapes[idx])
|
||||
}
|
||||
|
||||
pub fn get(&self, id: &Uuid) -> Option<&'a Shape> {
|
||||
let idx = *self.shapes_uuid_to_idx.get(&id)?;
|
||||
/// Get a shape by UUID. Returns the modified shape if modifiers/structure
|
||||
/// are applied, otherwise returns the base shape.
|
||||
pub fn get(&self, id: &Uuid) -> Option<&Shape> {
|
||||
let idx = *self.uuid_to_idx.get(id)?;
|
||||
|
||||
// SAFETY: We're extending the lifetimes to 'a.
|
||||
// This is safe because:
|
||||
// 1. All internal HashMaps and the shapes Vec have fields with lifetime 'a
|
||||
// 2. The shape at idx won't be moved or reallocated (pre-allocated Vec)
|
||||
// 3. The id is stored in shapes[idx].id which has lifetime 'a
|
||||
// 4. The references won't outlive the ShapesPoolImpl
|
||||
unsafe {
|
||||
let shape_ptr = &self.shapes[idx] as *const Shape;
|
||||
let modifiers_ptr = &self.modifiers as *const HashMap<&'a Uuid, skia::Matrix>;
|
||||
let structure_ptr = &self.structure as *const HashMap<&'a Uuid, Vec<StructureEntry>>;
|
||||
let scale_content_ptr = &self.scale_content as *const HashMap<&'a Uuid, f32>;
|
||||
let cache_ptr = &self.modified_shape_cache as *const HashMap<&'a Uuid, OnceCell<Shape>>;
|
||||
let shape = &self.shapes[idx];
|
||||
|
||||
// Extend the lifetime of id to 'a - safe because it's the same Uuid stored in shapes[idx].id
|
||||
let id_ref: &'a Uuid = &*(id as *const Uuid);
|
||||
// Check if this shape needs modification (has modifiers, structure changes, or is a bool)
|
||||
let needs_modification = shape.is_bool()
|
||||
|| self.modifiers.contains_key(&idx)
|
||||
|| self.structure.contains_key(&idx)
|
||||
|| self.scale_content.contains_key(&idx);
|
||||
|
||||
if (*shape_ptr).is_bool()
|
||||
|| (*modifiers_ptr).contains_key(&id_ref)
|
||||
|| (*structure_ptr).contains_key(&id_ref)
|
||||
|| (*scale_content_ptr).contains_key(&id_ref)
|
||||
{
|
||||
if let Some(cell) = (*cache_ptr).get(&id_ref) {
|
||||
Some(cell.get_or_init(|| {
|
||||
let mut shape = (*shape_ptr).transformed(
|
||||
(*modifiers_ptr).get(&id_ref),
|
||||
(*structure_ptr).get(&id_ref),
|
||||
);
|
||||
if needs_modification {
|
||||
// Check if we have a cached modified version
|
||||
if let Some(cell) = self.modified_shape_cache.get(&idx) {
|
||||
Some(cell.get_or_init(|| {
|
||||
let mut modified_shape =
|
||||
shape.transformed(self.modifiers.get(&idx), self.structure.get(&idx));
|
||||
|
||||
if self.to_update_bool(&shape) {
|
||||
math_bools::update_bool_to_path(&mut shape, self);
|
||||
}
|
||||
if self.to_update_bool(&modified_shape) {
|
||||
math_bools::update_bool_to_path(&mut modified_shape, self);
|
||||
}
|
||||
|
||||
if let Some(scale) = (*scale_content_ptr).get(&id_ref) {
|
||||
shape.scale_content(*scale);
|
||||
}
|
||||
shape
|
||||
}))
|
||||
} else {
|
||||
Some(&*shape_ptr)
|
||||
}
|
||||
if let Some(scale) = self.scale_content.get(&idx) {
|
||||
modified_shape.scale_content(*scale);
|
||||
}
|
||||
modified_shape
|
||||
}))
|
||||
} else {
|
||||
Some(&*shape_ptr)
|
||||
Some(shape)
|
||||
}
|
||||
} else {
|
||||
Some(shape)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,69 +192,68 @@ impl<'a> ShapesPoolImpl<'a> {
|
||||
}
|
||||
|
||||
pub fn set_modifiers(&mut self, modifiers: HashMap<Uuid, skia::Matrix>) {
|
||||
// Convert HashMap<Uuid, V> to HashMap<&'a Uuid, V> using references from shapes and
|
||||
// Initialize the cache cells because later we don't want to have the mutable pointer
|
||||
// Convert HashMap<Uuid, V> to HashMap<usize, V> using indices
|
||||
// Initialize the cache cells for affected shapes
|
||||
|
||||
let mut ids = Vec::<Uuid>::new();
|
||||
let mut modifiers_with_idx = HashMap::with_capacity(modifiers.len());
|
||||
|
||||
let mut modifiers_with_refs = HashMap::with_capacity(modifiers.len());
|
||||
for (uuid, matrix) in modifiers {
|
||||
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
|
||||
// self.modified_shape_cache.insert(uuid_ref, OnceCell::new());
|
||||
modifiers_with_refs.insert(uuid_ref, matrix);
|
||||
ids.push(*uuid_ref);
|
||||
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
||||
modifiers_with_idx.insert(idx, matrix);
|
||||
ids.push(uuid);
|
||||
}
|
||||
}
|
||||
self.modifiers = modifiers_with_refs;
|
||||
self.modifiers = modifiers_with_idx;
|
||||
|
||||
let all_ids = shapes::all_with_ancestors(&ids, self, true);
|
||||
for uuid in all_ids {
|
||||
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
|
||||
self.modified_shape_cache.insert(uuid_ref, OnceCell::new());
|
||||
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
||||
self.modified_shape_cache.insert(idx, OnceCell::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_structure(&mut self, structure: HashMap<Uuid, Vec<StructureEntry>>) {
|
||||
// Convert HashMap<Uuid, V> to HashMap<&'a Uuid, V> using references from shapes and
|
||||
// Initialize the cache cells because later we don't want to have the mutable pointer
|
||||
let mut structure_with_refs = HashMap::with_capacity(structure.len());
|
||||
// Convert HashMap<Uuid, V> to HashMap<usize, V> using indices
|
||||
// Initialize the cache cells for affected shapes
|
||||
let mut structure_with_idx = HashMap::with_capacity(structure.len());
|
||||
let mut ids = Vec::<Uuid>::new();
|
||||
|
||||
for (uuid, entries) in structure {
|
||||
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
|
||||
structure_with_refs.insert(uuid_ref, entries);
|
||||
ids.push(*uuid_ref);
|
||||
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
||||
structure_with_idx.insert(idx, entries);
|
||||
ids.push(uuid);
|
||||
}
|
||||
}
|
||||
self.structure = structure_with_refs;
|
||||
self.structure = structure_with_idx;
|
||||
|
||||
let all_ids = shapes::all_with_ancestors(&ids, self, true);
|
||||
for uuid in all_ids {
|
||||
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
|
||||
self.modified_shape_cache.insert(uuid_ref, OnceCell::new());
|
||||
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
||||
self.modified_shape_cache.insert(idx, OnceCell::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_scale_content(&mut self, scale_content: HashMap<Uuid, f32>) {
|
||||
// Convert HashMap<Uuid, V> to HashMap<&'a Uuid, V> using references from shapes and
|
||||
// Initialize the cache cells because later we don't want to have the mutable pointer
|
||||
let mut scale_content_with_refs = HashMap::with_capacity(scale_content.len());
|
||||
// Convert HashMap<Uuid, V> to HashMap<usize, V> using indices
|
||||
// Initialize the cache cells for affected shapes
|
||||
let mut scale_content_with_idx = HashMap::with_capacity(scale_content.len());
|
||||
let mut ids = Vec::<Uuid>::new();
|
||||
|
||||
for (uuid, value) in scale_content {
|
||||
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
|
||||
scale_content_with_refs.insert(uuid_ref, value);
|
||||
ids.push(*uuid_ref);
|
||||
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
||||
scale_content_with_idx.insert(idx, value);
|
||||
ids.push(uuid);
|
||||
}
|
||||
}
|
||||
self.scale_content = scale_content_with_refs;
|
||||
self.scale_content = scale_content_with_idx;
|
||||
|
||||
let all_ids = shapes::all_with_ancestors(&ids, self, true);
|
||||
for uuid in all_ids {
|
||||
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
|
||||
self.modified_shape_cache.insert(uuid_ref, OnceCell::new());
|
||||
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
||||
self.modified_shape_cache.insert(idx, OnceCell::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -349,47 +265,33 @@ impl<'a> ShapesPoolImpl<'a> {
|
||||
self.scale_content = HashMap::default();
|
||||
}
|
||||
|
||||
/// Get a reference to the Uuid stored in a shape, if it exists
|
||||
pub fn get_uuid_ref(&self, id: &Uuid) -> Option<&'a Uuid> {
|
||||
let idx = *self.shapes_uuid_to_idx.get(&id)?;
|
||||
// SAFETY: We're returning a reference with lifetime 'a to a Uuid stored
|
||||
// in the shapes Vec. This is safe because the Vec is stable (pre-allocated)
|
||||
// and won't be reallocated.
|
||||
unsafe { Some(&*(&self.shapes[idx].id as *const Uuid)) }
|
||||
}
|
||||
|
||||
pub fn subtree(&self, id: &Uuid) -> ShapesPoolImpl<'a> {
|
||||
pub fn subtree(&self, id: &Uuid) -> ShapesPoolImpl {
|
||||
let Some(shape) = self.get(id) else {
|
||||
panic!("Subtree not found");
|
||||
};
|
||||
|
||||
let mut shapes = vec![];
|
||||
let mut idx = 0;
|
||||
let mut shapes_uuid_to_idx = HashMap::default();
|
||||
let mut new_idx = 0;
|
||||
let mut uuid_to_idx = HashMap::default();
|
||||
|
||||
for id in shape.all_children_iter(self, true, true) {
|
||||
let Some(shape) = self.get(&id) else {
|
||||
for child_id in shape.all_children_iter(self, true, true) {
|
||||
let Some(child_shape) = self.get(&child_id) else {
|
||||
panic!("Not found");
|
||||
};
|
||||
shapes.push(shape.clone());
|
||||
|
||||
let id_ref: &'a Uuid = unsafe { &*(&self.shapes[idx].id as *const Uuid) };
|
||||
shapes_uuid_to_idx.insert(id_ref, idx);
|
||||
idx += 1;
|
||||
shapes.push(child_shape.clone());
|
||||
uuid_to_idx.insert(child_id, new_idx);
|
||||
new_idx += 1;
|
||||
}
|
||||
|
||||
let mut result = ShapesPoolImpl {
|
||||
ShapesPoolImpl {
|
||||
shapes,
|
||||
counter: idx,
|
||||
shapes_uuid_to_idx,
|
||||
counter: new_idx,
|
||||
uuid_to_idx,
|
||||
modified_shape_cache: HashMap::default(),
|
||||
modifiers: HashMap::default(),
|
||||
structure: HashMap::default(),
|
||||
scale_content: HashMap::default(),
|
||||
};
|
||||
result.rebuild_references();
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fn to_update_bool(&self, shape: &Shape) -> bool {
|
||||
@@ -398,11 +300,21 @@ impl<'a> ShapesPoolImpl<'a> {
|
||||
}
|
||||
|
||||
let default = &Matrix::default();
|
||||
let parent_modifier = self.modifiers.get(&shape.id).unwrap_or(default);
|
||||
|
||||
// Get parent modifier by index
|
||||
let parent_idx = self.uuid_to_idx.get(&shape.id);
|
||||
let parent_modifier = parent_idx
|
||||
.and_then(|idx| self.modifiers.get(idx))
|
||||
.unwrap_or(default);
|
||||
|
||||
// Returns true if the transform of any child is different to the parent's
|
||||
shape.all_children_iter(self, true, false).any(|id| {
|
||||
!math::is_close_matrix(parent_modifier, self.modifiers.get(&id).unwrap_or(default))
|
||||
shape.all_children_iter(self, true, false).any(|child_id| {
|
||||
let child_modifier = self
|
||||
.uuid_to_idx
|
||||
.get(&child_id)
|
||||
.and_then(|idx| self.modifiers.get(idx))
|
||||
.unwrap_or(default);
|
||||
!math::is_close_matrix(parent_modifier, child_modifier)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{with_current_shape_mut, STATE};
|
||||
use macros::ToJs;
|
||||
|
||||
mod align;
|
||||
mod constraints;
|
||||
pub mod constraints;
|
||||
mod flex;
|
||||
mod grid;
|
||||
|
||||
|
||||
173
render-wasm/src/wasm/shapes/base_props.rs
Normal file
173
render-wasm/src/wasm/shapes/base_props.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
use crate::mem;
|
||||
use crate::shapes::{BlendMode, ConstraintH, ConstraintV};
|
||||
use crate::utils::uuid_from_u32_quartet;
|
||||
use crate::uuid::Uuid;
|
||||
use crate::wasm::blend::RawBlendMode;
|
||||
use crate::wasm::layouts::constraints::{RawConstraintH, RawConstraintV};
|
||||
use crate::{with_state_mut, STATE};
|
||||
|
||||
use super::RawShapeType;
|
||||
|
||||
/// Binary layout for batched shape base properties:
|
||||
///
|
||||
/// | Offset | Size | Field | Type |
|
||||
/// |--------|------|--------------|-----------------------------------|
|
||||
/// | 0 | 16 | id | UUID (4 × u32 LE) |
|
||||
/// | 16 | 16 | parent_id | UUID (4 × u32 LE) |
|
||||
/// | 32 | 1 | shape_type | u8 |
|
||||
/// | 33 | 1 | flags | u8 (bit0: clip, bit1: hidden) |
|
||||
/// | 34 | 1 | blend_mode | u8 |
|
||||
/// | 35 | 1 | constraint_h | u8 (0xFF = None) |
|
||||
/// | 36 | 1 | constraint_v | u8 (0xFF = None) |
|
||||
/// | 37 | 3 | padding | - |
|
||||
/// | 40 | 4 | opacity | f32 LE |
|
||||
/// | 44 | 4 | rotation | f32 LE |
|
||||
/// | 48 | 24 | transform | 6 × f32 LE (a,b,c,d,e,f) |
|
||||
/// | 72 | 16 | selrect | 4 × f32 LE (x1,y1,x2,y2) |
|
||||
/// | 88 | 16 | corners | 4 × f32 LE (r1,r2,r3,r4) |
|
||||
/// |--------|------|--------------|-----------------------------------|
|
||||
/// | Total | 104 | | |
|
||||
pub const BASE_PROPS_SIZE: usize = 104;
|
||||
|
||||
const FLAG_CLIP_CONTENT: u8 = 0b0000_0001;
|
||||
const FLAG_HIDDEN: u8 = 0b0000_0010;
|
||||
const CONSTRAINT_NONE: u8 = 0xFF;
|
||||
|
||||
/// Reads a f32 from a byte slice at the given offset (little-endian)
|
||||
#[inline]
|
||||
fn read_f32_le(bytes: &[u8], offset: usize) -> f32 {
|
||||
f32::from_le_bytes([
|
||||
bytes[offset],
|
||||
bytes[offset + 1],
|
||||
bytes[offset + 2],
|
||||
bytes[offset + 3],
|
||||
])
|
||||
}
|
||||
|
||||
/// Reads a u32 from a byte slice at the given offset (little-endian)
|
||||
#[inline]
|
||||
fn read_u32_le(bytes: &[u8], offset: usize) -> u32 {
|
||||
u32::from_le_bytes([
|
||||
bytes[offset],
|
||||
bytes[offset + 1],
|
||||
bytes[offset + 2],
|
||||
bytes[offset + 3],
|
||||
])
|
||||
}
|
||||
|
||||
/// Parses UUID from bytes at given offset
|
||||
#[inline]
|
||||
fn read_uuid(bytes: &[u8], offset: usize) -> Uuid {
|
||||
uuid_from_u32_quartet(
|
||||
read_u32_le(bytes, offset),
|
||||
read_u32_le(bytes, offset + 4),
|
||||
read_u32_le(bytes, offset + 8),
|
||||
read_u32_le(bytes, offset + 12),
|
||||
)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_shape_base_props() {
|
||||
let bytes = mem::bytes();
|
||||
|
||||
if bytes.len() < BASE_PROPS_SIZE {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse all fields from the buffer
|
||||
let id = read_uuid(&bytes, 0);
|
||||
let parent_id = read_uuid(&bytes, 16);
|
||||
let shape_type = bytes[32];
|
||||
let flags = bytes[33];
|
||||
let blend_mode = bytes[34];
|
||||
let constraint_h = bytes[35];
|
||||
let constraint_v = bytes[36];
|
||||
// bytes[37..40] are padding
|
||||
|
||||
let opacity = read_f32_le(&bytes, 40);
|
||||
let rotation = read_f32_le(&bytes, 44);
|
||||
|
||||
// Transform matrix (a, b, c, d, e, f)
|
||||
let transform_a = read_f32_le(&bytes, 48);
|
||||
let transform_b = read_f32_le(&bytes, 52);
|
||||
let transform_c = read_f32_le(&bytes, 56);
|
||||
let transform_d = read_f32_le(&bytes, 60);
|
||||
let transform_e = read_f32_le(&bytes, 64);
|
||||
let transform_f = read_f32_le(&bytes, 68);
|
||||
|
||||
// Selrect (x1, y1, x2, y2)
|
||||
let selrect_x1 = read_f32_le(&bytes, 72);
|
||||
let selrect_y1 = read_f32_le(&bytes, 76);
|
||||
let selrect_x2 = read_f32_le(&bytes, 80);
|
||||
let selrect_y2 = read_f32_le(&bytes, 84);
|
||||
|
||||
// Corners (r1, r2, r3, r4)
|
||||
let corner_r1 = read_f32_le(&bytes, 88);
|
||||
let corner_r2 = read_f32_le(&bytes, 92);
|
||||
let corner_r3 = read_f32_le(&bytes, 96);
|
||||
let corner_r4 = read_f32_le(&bytes, 100);
|
||||
|
||||
// Decode flags
|
||||
let clip_content = (flags & FLAG_CLIP_CONTENT) != 0;
|
||||
let hidden = (flags & FLAG_HIDDEN) != 0;
|
||||
|
||||
// Convert raw enum values
|
||||
let shape_type_enum = RawShapeType::from(shape_type);
|
||||
let blend_mode_enum: BlendMode = RawBlendMode::from(blend_mode).into();
|
||||
|
||||
let constraint_h_opt: Option<ConstraintH> = if constraint_h == CONSTRAINT_NONE {
|
||||
None
|
||||
} else {
|
||||
Some(RawConstraintH::from(constraint_h).into())
|
||||
};
|
||||
|
||||
let constraint_v_opt: Option<ConstraintV> = if constraint_v == CONSTRAINT_NONE {
|
||||
None
|
||||
} else {
|
||||
Some(RawConstraintV::from(constraint_v).into())
|
||||
};
|
||||
|
||||
with_state_mut!(state, {
|
||||
// Select/create the shape
|
||||
state.use_shape(id);
|
||||
|
||||
// Set parent relationship
|
||||
state.set_parent_for_current_shape(parent_id);
|
||||
|
||||
// Mark shape as touched
|
||||
state.touch_current();
|
||||
|
||||
// Apply all properties to the current shape
|
||||
if let Some(shape) = state.current_shape_mut() {
|
||||
// Type
|
||||
shape.set_shape_type(shape_type_enum.into());
|
||||
|
||||
// Boolean flags
|
||||
shape.set_clip(clip_content);
|
||||
shape.set_hidden(hidden);
|
||||
|
||||
// Blend mode and opacity
|
||||
shape.set_blend_mode(blend_mode_enum);
|
||||
shape.set_opacity(opacity);
|
||||
|
||||
// Constraints
|
||||
shape.set_constraint_h(constraint_h_opt);
|
||||
shape.set_constraint_v(constraint_v_opt);
|
||||
|
||||
// Transform
|
||||
shape.set_rotation(rotation);
|
||||
shape.set_transform(
|
||||
transform_a,
|
||||
transform_b,
|
||||
transform_c,
|
||||
transform_d,
|
||||
transform_e,
|
||||
transform_f,
|
||||
);
|
||||
|
||||
// Geometry
|
||||
shape.set_selrect(selrect_x1, selrect_y1, selrect_x2, selrect_y2);
|
||||
shape.set_corners((corner_r1, corner_r2, corner_r3, corner_r4));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
mod base_props;
|
||||
|
||||
use macros::ToJs;
|
||||
|
||||
use crate::shapes::{Bool, Frame, Group, Path, Rect, SVGRaw, TextContent, Type};
|
||||
Reference in New Issue
Block a user