mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 03:48:11 +08:00
Merge origin/master into the async record refactor
Scaffolding merge for the reconcile; the final series to master is authored fresh. Rank plumbing resolves to our axis-IR model, the node macro and the LaneSource render walk stay ours, master's vector restructure and gradient vocabulary are adopted, and the paint and appearance adoption is deliberately deferred behind our fill and stroke markers.
This commit is contained in:
@@ -1,2 +1,2 @@
|
|||||||
https://github.com/Keavon/graphite-branded-assets/archive/0d004aa61e6b48d316e8e5db6d59ccc4788f192d.tar.gz
|
https://github.com/Keavon/graphite-branded-assets/archive/6687dc6d3d8552948458d00d35e2bee3a40ab66b.tar.gz
|
||||||
772d64518be43c99977ba56f69e574531c56e83d2df2f42ab066f77f74b0dd1f
|
7663b4a35f377a5d3363430a725a1d5f634fbdc56adca5e43167786b8fe1b253
|
||||||
|
|||||||
@@ -2,11 +2,25 @@
|
|||||||
"image": "mcr.microsoft.com/devcontainers/base:debian",
|
"image": "mcr.microsoft.com/devcontainers/base:debian",
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/rust:1": {
|
"ghcr.io/devcontainers/features/rust:1": {
|
||||||
"profile": "default"
|
"profile": "default",
|
||||||
|
"targets": "wasm32-unknown-unknown"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers/features/node:1": {}
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
|
// NOTICE: keep in sync with `.nvmrc`
|
||||||
|
"version": "24"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// NOTICE: keep these tool versions in sync with `.github/workflows/build.yml`, `.nix/dev.nix` and `.nix/pkgs/graphite.nix` (wasm-bindgen-cli), `tools/cargo-run/src/requirements.rs` (wasm-bindgen-cli, cargo-about), `tools/cargo-run/src/requirements/wasm_opt.rs` (Binaryen), and the `wasm-bindgen` crate in `Cargo.toml`
|
||||||
|
"containerEnv": {
|
||||||
|
"WASM_BINDGEN_CLI_VERSION": "0.2.121",
|
||||||
|
"CARGO_ABOUT_VERSION": "0.9.2",
|
||||||
|
"BINARYEN_VERSION": "130",
|
||||||
|
"CARGO_BINSTALL_VERSION": "1.23.0"
|
||||||
|
},
|
||||||
|
"onCreateCommand": {
|
||||||
|
"Install cargo-about and wasm-bindgen-cli": "curl -sSfL https://github.com/cargo-bins/cargo-binstall/releases/download/v$CARGO_BINSTALL_VERSION/cargo-binstall-$(uname -m)-unknown-linux-musl.tgz | tar xz -C $CARGO_HOME/bin && BINSTALL_DISABLE_TELEMETRY=true cargo binstall --no-confirm cargo-about@$CARGO_ABOUT_VERSION wasm-bindgen-cli@$WASM_BINDGEN_CLI_VERSION",
|
||||||
|
"Install Binaryen wasm-opt": "curl -sSfL https://github.com/WebAssembly/binaryen/releases/download/version_$BINARYEN_VERSION/binaryen-version_$BINARYEN_VERSION-$(uname -m)-linux.tar.gz | tar xz -C $CARGO_HOME/bin --strip-components=2 binaryen-version_$BINARYEN_VERSION/bin/wasm-opt"
|
||||||
},
|
},
|
||||||
"onCreateCommand": "cargo install cargo-about && cargo install -f wasm-bindgen-cli@0.2.121",
|
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
// NOTE: Keep this in sync with `.vscode/extensions.json`
|
// NOTE: Keep this in sync with `.vscode/extensions.json`
|
||||||
@@ -16,14 +30,16 @@
|
|||||||
"tamasfe.even-better-toml",
|
"tamasfe.even-better-toml",
|
||||||
// Web
|
// Web
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
"svelte.svelte-vscode",
|
"svelte.svelte-vscode",
|
||||||
"vitaliymaz.vscode-svg-previewer",
|
"vitaliymaz.vscode-svg-previewer",
|
||||||
// Code quality
|
// Code quality
|
||||||
"wayou.vscode-todo-highlight",
|
"wayou.vscode-todo-highlight",
|
||||||
"streetsidesoftware.code-spell-checker",
|
"streetsidesoftware.code-spell-checker",
|
||||||
// Helpful
|
// Git
|
||||||
"mhutchie.git-graph",
|
"mhutchie.git-graph",
|
||||||
"qezhu.gitlink",
|
"qezhu.gitlink",
|
||||||
|
// Helpful
|
||||||
"wmaurer.change-case"
|
"wmaurer.change-case"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Keavon Chambers (@Keavon keavon@keavon.com)
|
||||||
|
|
||||||
|
|
||||||
|
# Dennis Kobert (@TrueDoctor dennis@kobert.dev)
|
||||||
|
|
||||||
|
/document/format/ @TrueDoctor
|
||||||
|
|
||||||
|
/node-graph/graph-craft/ @TrueDoctor
|
||||||
|
/node-graph/preprocessor/ @TrueDoctor
|
||||||
|
/node-graph/node-macro/ @TrueDoctor
|
||||||
|
/node-graph/interpreted-executor/ @TrueDoctor
|
||||||
|
/node-graph/graphene-cli/ @TrueDoctor
|
||||||
|
|
||||||
|
# Timon Schelling (@timon-schelling me@timon.zip)
|
||||||
|
|
||||||
|
/.nix/ @timon-schelling
|
||||||
|
/flake.nix @timon-schelling
|
||||||
|
/flake.lock @timon-schelling
|
||||||
|
|
||||||
|
/desktop/ @timon-schelling
|
||||||
|
|
||||||
|
/editor/src/messages/future/ @timon-schelling
|
||||||
|
/editor/src/messages/resource_storage/ @timon-schelling
|
||||||
|
/editor/src/messages/network/ @timon-schelling
|
||||||
|
/editor/src/messages/app_window/ @timon-schelling
|
||||||
|
/editor/src/messages/clipboard/ @timon-schelling
|
||||||
|
/editor/src/messages/viewport/ @timon-schelling
|
||||||
|
|
||||||
|
/libraries/wgpu-sync/ @timon-schelling
|
||||||
|
|
||||||
|
/node-graph/libraries/canvas-utils/ @timon-schelling
|
||||||
|
|
||||||
|
/tools/cargo-run/ @timon-schelling
|
||||||
|
/tools/third-party-licenses/ @timon-schelling
|
||||||
+24
-10
@@ -58,6 +58,7 @@ jobs:
|
|||||||
RUSTC_WRAPPER: /usr/bin/sccache
|
RUSTC_WRAPPER: /usr/bin/sccache
|
||||||
CARGO_INCREMENTAL: 0
|
CARGO_INCREMENTAL: 0
|
||||||
SCCACHE_DIR: /var/lib/github-actions/.cache
|
SCCACHE_DIR: /var/lib/github-actions/.cache
|
||||||
|
# NOTICE: keep these tool versions in sync with the other jobs in this file, `.devcontainer/devcontainer.json`, `.nix/dev.nix` and `.nix/pkgs/graphite.nix` (wasm-bindgen-cli), `tools/cargo-run/src/requirements.rs` (wasm-bindgen-cli, cargo-about), `tools/cargo-run/src/requirements/wasm_opt.rs` (Binaryen), and the `wasm-bindgen` crate in `Cargo.toml`
|
||||||
WASM_BINDGEN_CLI_VERSION: "0.2.121"
|
WASM_BINDGEN_CLI_VERSION: "0.2.121"
|
||||||
BINARYEN_VERSION: "130"
|
BINARYEN_VERSION: "130"
|
||||||
steps:
|
steps:
|
||||||
@@ -302,9 +303,11 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
# NOTICE: keep these tool versions in sync with the other jobs in this file, `.devcontainer/devcontainer.json`, `.nix/dev.nix` and `.nix/pkgs/graphite.nix` (wasm-bindgen-cli), `tools/cargo-run/src/requirements.rs` (wasm-bindgen-cli, cargo-about), `tools/cargo-run/src/requirements/wasm_opt.rs` (Binaryen), and the `wasm-bindgen` crate in `Cargo.toml`
|
||||||
WASM_BINDGEN_CLI_VERSION: "0.2.121"
|
WASM_BINDGEN_CLI_VERSION: "0.2.121"
|
||||||
BINARYEN_VERSION: "130"
|
BINARYEN_VERSION: "130"
|
||||||
CARGO_ABOUT_VERSION: "0.9.0"
|
CARGO_ABOUT_VERSION: "0.9.2"
|
||||||
|
CARGO_BINSTALL_VERSION: "1.23.0"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 📥 Clone repository
|
- name: 📥 Clone repository
|
||||||
@@ -341,7 +344,9 @@ jobs:
|
|||||||
frontend/package-lock.json
|
frontend/package-lock.json
|
||||||
|
|
||||||
- name: 📦 Install Cargo-binstall
|
- name: 📦 Install Cargo-binstall
|
||||||
uses: cargo-bins/cargo-binstall@main
|
uses: cargo-bins/cargo-binstall@v1.23.0
|
||||||
|
with:
|
||||||
|
version: ${{ env.CARGO_BINSTALL_VERSION }}
|
||||||
|
|
||||||
- name: 🚧 Install native dependencies
|
- name: 🚧 Install native dependencies
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
@@ -497,9 +502,11 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
# NOTICE: keep these tool versions in sync with the other jobs in this file, `.devcontainer/devcontainer.json`, `.nix/dev.nix` and `.nix/pkgs/graphite.nix` (wasm-bindgen-cli), `tools/cargo-run/src/requirements.rs` (wasm-bindgen-cli, cargo-about), `tools/cargo-run/src/requirements/wasm_opt.rs` (Binaryen), and the `wasm-bindgen` crate in `Cargo.toml`
|
||||||
WASM_BINDGEN_CLI_VERSION: "0.2.121"
|
WASM_BINDGEN_CLI_VERSION: "0.2.121"
|
||||||
BINARYEN_VERSION: "130"
|
BINARYEN_VERSION: "130"
|
||||||
CARGO_ABOUT_VERSION: "0.9.0"
|
CARGO_ABOUT_VERSION: "0.9.2"
|
||||||
|
CARGO_BINSTALL_VERSION: "1.23.0"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 📥 Clone repository
|
- name: 📥 Clone repository
|
||||||
@@ -535,6 +542,11 @@ jobs:
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
frontend/package-lock.json
|
frontend/package-lock.json
|
||||||
|
|
||||||
|
- name: 📦 Install Cargo-binstall
|
||||||
|
uses: cargo-bins/cargo-binstall@v1.23.0
|
||||||
|
with:
|
||||||
|
version: ${{ env.CARGO_BINSTALL_VERSION }}
|
||||||
|
|
||||||
- name: 🚧 Install native dependencies
|
- name: 🚧 Install native dependencies
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
@@ -544,8 +556,7 @@ jobs:
|
|||||||
brew install \
|
brew install \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
openssl@3 \
|
openssl@3 \
|
||||||
llvm \
|
llvm
|
||||||
cargo-binstall
|
|
||||||
|
|
||||||
echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV
|
echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV
|
||||||
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
|
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
|
||||||
@@ -650,17 +661,20 @@ jobs:
|
|||||||
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Graphite Helper.app"
|
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Graphite Helper.app"
|
||||||
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Graphite Helper (GPU).app"
|
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Graphite Helper (GPU).app"
|
||||||
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Graphite Helper (Renderer).app"
|
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Graphite Helper (Renderer).app"
|
||||||
|
find "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries" -name "*.dylib" -exec codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" {} \;
|
||||||
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework"
|
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework"
|
||||||
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libcef_sandbox.dylib"
|
|
||||||
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libEGL.dylib"
|
|
||||||
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libGLESv2.dylib"
|
|
||||||
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libvk_swiftshader.dylib"
|
|
||||||
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH" --deep
|
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH" --deep
|
||||||
|
|
||||||
codesign --verify --deep --strict --verbose=4 "$APP_PATH"
|
codesign --verify --deep --strict --verbose=4 "$APP_PATH"
|
||||||
|
|
||||||
ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"
|
ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"
|
||||||
xcrun notarytool submit "$ZIP_PATH" --wait --apple-id "$APPLE_EMAIL" --team-id "$APPLE_TEAM_ID" --password "$APPLE_PASSWORD"
|
SUBMISSION_ID=$(xcrun notarytool submit "$ZIP_PATH" --wait --output-format json --apple-id "$APPLE_EMAIL" --team-id "$APPLE_TEAM_ID" --password "$APPLE_PASSWORD" | tee /dev/stderr | jq -r '.id')
|
||||||
|
STATUS=$(xcrun notarytool info "$SUBMISSION_ID" --output-format json --apple-id "$APPLE_EMAIL" --team-id "$APPLE_TEAM_ID" --password "$APPLE_PASSWORD" | jq -r '.status')
|
||||||
|
if [ "$STATUS" != "Accepted" ]; then
|
||||||
|
echo "Notarization failed with status: $STATUS"
|
||||||
|
xcrun notarytool log "$SUBMISSION_ID" --apple-id "$APPLE_EMAIL" --team-id "$APPLE_TEAM_ID" --password "$APPLE_PASSWORD"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
rm "$ZIP_PATH"
|
rm "$ZIP_PATH"
|
||||||
|
|
||||||
xcrun stapler staple -v "$APP_PATH"
|
xcrun stapler staple -v "$APP_PATH"
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ flamegraph.svg
|
|||||||
.direnv
|
.direnv
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.nvim.lua
|
.nvim.lua
|
||||||
|
/.devcontainer/devcontainer-lock.json
|
||||||
|
|||||||
+14
-12
@@ -1,10 +1,10 @@
|
|||||||
{ pkgs, ... }:
|
{ pkgs, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
version = "149.0.5+g6770623+chromium-149.0.7827.197";
|
version = "151.3.24+g2384915+chromium-151.0.7922.174";
|
||||||
hashes = {
|
hashes = {
|
||||||
aarch64-linux = "sha256-cBAvcvs1rAg5EKJkCt81RZYupCWpUNIC/nLt3PJow7Q=";
|
aarch64-linux = "sha256-R5ZbnDallYvdbW/bP+M2DzjRfWWRTvY2q63hSIHNxZs=";
|
||||||
x86_64-linux = "sha256-OPGMBJmvvLiLdBDniBQwx7LmTGGI59AcesJdILSeqcs=";
|
x86_64-linux = "sha256-21PEP9rOi37krw8AUSARbWlzqp2Ot3AsBhe635voaE4=";
|
||||||
};
|
};
|
||||||
|
|
||||||
selectSystem =
|
selectSystem =
|
||||||
@@ -12,17 +12,19 @@ let
|
|||||||
attrs.${pkgs.stdenv.hostPlatform.system}
|
attrs.${pkgs.stdenv.hostPlatform.system}
|
||||||
or (throw "Unsupported system ${pkgs.stdenv.hostPlatform.system}");
|
or (throw "Unsupported system ${pkgs.stdenv.hostPlatform.system}");
|
||||||
|
|
||||||
|
url = "https://cef-builds.spotifycdn.com/cef_binary_${version}_${
|
||||||
|
selectSystem {
|
||||||
|
aarch64-linux = "linuxarm64";
|
||||||
|
x86_64-linux = "linux64";
|
||||||
|
}
|
||||||
|
}_minimal.tar.bz2";
|
||||||
|
|
||||||
src = pkgs.fetchurl {
|
src = pkgs.fetchurl {
|
||||||
url = "https://cef-builds.spotifycdn.com/cef_binary_${version}_${
|
inherit url;
|
||||||
selectSystem {
|
|
||||||
aarch64-linux = "linuxarm64";
|
|
||||||
x86_64-linux = "linux64";
|
|
||||||
}
|
|
||||||
}_minimal.tar.bz2";
|
|
||||||
hash = selectSystem hashes;
|
hash = selectSystem hashes;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
pkgs.cef-binary.overrideAttrs (finalAttrs: {
|
pkgs.cef-binary.overrideAttrs {
|
||||||
version = builtins.head (builtins.split "\\+" version);
|
version = builtins.head (builtins.split "\\+" version);
|
||||||
inherit src;
|
inherit src;
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
@@ -38,9 +40,9 @@ pkgs.cef-binary.overrideAttrs (finalAttrs: {
|
|||||||
echo '${
|
echo '${
|
||||||
builtins.toJSON {
|
builtins.toJSON {
|
||||||
type = "minimal";
|
type = "minimal";
|
||||||
name = builtins.baseNameOf finalAttrs.src.url;
|
name = builtins.baseNameOf url;
|
||||||
sha1 = "";
|
sha1 = "";
|
||||||
}
|
}
|
||||||
}' > $out/archive.json
|
}' > $out/archive.json
|
||||||
'';
|
'';
|
||||||
})
|
}
|
||||||
|
|||||||
Vendored
+1
@@ -6,6 +6,7 @@
|
|||||||
"tamasfe.even-better-toml",
|
"tamasfe.even-better-toml",
|
||||||
// Web
|
// Web
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
"svelte.svelte-vscode",
|
"svelte.svelte-vscode",
|
||||||
"vitaliymaz.vscode-svg-previewer",
|
"vitaliymaz.vscode-svg-previewer",
|
||||||
// Code quality
|
// Code quality
|
||||||
|
|||||||
Generated
+176
-115
@@ -370,15 +370,31 @@ dependencies = [
|
|||||||
name = "brush-nodes"
|
name = "brush-nodes"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"brush-types",
|
||||||
|
"bytemuck",
|
||||||
"core-types",
|
"core-types",
|
||||||
"dyn-any",
|
"dyn-any",
|
||||||
"glam",
|
"glam",
|
||||||
"graphene-hash",
|
"graphene-hash",
|
||||||
|
"graphic-types",
|
||||||
|
"half",
|
||||||
"node-macro",
|
"node-macro",
|
||||||
"raster-nodes",
|
|
||||||
"raster-types",
|
"raster-types",
|
||||||
"serde",
|
"serde",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"wgpu",
|
||||||
|
"wgpu-executor",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "brush-types"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"core-types",
|
||||||
|
"dyn-any",
|
||||||
|
"glam",
|
||||||
|
"graphene-hash",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -594,8 +610,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cef"
|
name = "cef"
|
||||||
version = "149.2.0+149.0.5"
|
version = "151.8.0+151.3.24"
|
||||||
source = "git+https://github.com/timon-schelling/cef-rs.git?branch=graphite-149#877c9cd8f7776e6c52e365d501ad44c73e17117a"
|
source = "git+https://github.com/timon-schelling/cef-rs.git?branch=graphite-151#35712dc3948628245cf9810eadd045d9435442e8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cargo_metadata 0.23.1",
|
"cargo_metadata 0.23.1",
|
||||||
@@ -613,8 +629,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cef-dll-sys"
|
name = "cef-dll-sys"
|
||||||
version = "149.2.0+149.0.5"
|
version = "151.8.0+151.3.24"
|
||||||
source = "git+https://github.com/timon-schelling/cef-rs.git?branch=graphite-149#877c9cd8f7776e6c52e365d501ad44c73e17117a"
|
source = "git+https://github.com/timon-schelling/cef-rs.git?branch=graphite-151#35712dc3948628245cf9810eadd045d9435442e8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cmake",
|
"cmake",
|
||||||
@@ -654,6 +670,19 @@ dependencies = [
|
|||||||
"windows-link 0.1.3",
|
"windows-link 0.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chumsky"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "14377e276b2c8300513dff55ba4cc4142b44e5d6de6d00eb5b2307d650bb4ec1"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.15.5",
|
||||||
|
"regex-automata 0.3.9",
|
||||||
|
"serde",
|
||||||
|
"unicode-ident",
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ciborium"
|
name = "ciborium"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -946,7 +975,7 @@ dependencies = [
|
|||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"skrifa",
|
"skrifa 0.42.1",
|
||||||
"tinyvec",
|
"tinyvec",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tsify",
|
"tsify",
|
||||||
@@ -1141,6 +1170,15 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
|
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "delaunator"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3e1ee323c1275374f7e612d3724d12707079fb6a2117349fe144def656f5a880"
|
||||||
|
dependencies = [
|
||||||
|
"robust",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -1224,7 +1262,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users",
|
"redox_users",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1334,7 +1372,7 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "download-cef"
|
name = "download-cef"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
source = "git+https://github.com/timon-schelling/cef-rs.git?branch=graphite-149#877c9cd8f7776e6c52e365d501ad44c73e17117a"
|
source = "git+https://github.com/timon-schelling/cef-rs.git?branch=graphite-151#35712dc3948628245cf9810eadd045d9435442e8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bzip2",
|
"bzip2",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -1353,7 +1391,8 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "dpi"
|
name = "dpi"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -1502,7 +1541,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
|
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1527,8 +1566,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277"
|
checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bit-set 0.8.0",
|
"bit-set 0.8.0",
|
||||||
"regex-automata",
|
"regex-automata 0.4.14",
|
||||||
"regex-syntax",
|
"regex-syntax 0.8.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1647,6 +1686,15 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "font-types"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75382bc7392ef10aad10935f92fc3db36d2d4dad0e5d96d8d65e04f89a07ec39"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fontconfig-parser"
|
name = "fontconfig-parser"
|
||||||
version = "0.5.8"
|
version = "0.5.8"
|
||||||
@@ -1680,7 +1728,7 @@ dependencies = [
|
|||||||
"linebender_resource_handle",
|
"linebender_resource_handle",
|
||||||
"memmap2 0.9.10",
|
"memmap2 0.9.10",
|
||||||
"parlance",
|
"parlance",
|
||||||
"read-fonts",
|
"read-fonts 0.39.2",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2177,11 +2225,13 @@ dependencies = [
|
|||||||
name = "graphic-nodes"
|
name = "graphic-nodes"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"brush-types",
|
||||||
"core-types",
|
"core-types",
|
||||||
"dyn-any",
|
"dyn-any",
|
||||||
"glam",
|
"glam",
|
||||||
"graphic-types",
|
"graphic-types",
|
||||||
"node-macro",
|
"node-macro",
|
||||||
|
"rand",
|
||||||
"raster-types",
|
"raster-types",
|
||||||
"serde",
|
"serde",
|
||||||
"vector-types",
|
"vector-types",
|
||||||
@@ -2191,6 +2241,7 @@ dependencies = [
|
|||||||
name = "graphic-types"
|
name = "graphic-types"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"brush-types",
|
||||||
"core-types",
|
"core-types",
|
||||||
"dyn-any",
|
"dyn-any",
|
||||||
"glam",
|
"glam",
|
||||||
@@ -2333,6 +2384,7 @@ dependencies = [
|
|||||||
"base64",
|
"base64",
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"color",
|
"color",
|
||||||
|
"core-types",
|
||||||
"derivative",
|
"derivative",
|
||||||
"document-container",
|
"document-container",
|
||||||
"document-format",
|
"document-format",
|
||||||
@@ -2351,6 +2403,7 @@ dependencies = [
|
|||||||
"kurbo",
|
"kurbo",
|
||||||
"log",
|
"log",
|
||||||
"math-parser",
|
"math-parser",
|
||||||
|
"node-macro",
|
||||||
"num_enum",
|
"num_enum",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"preprocessor",
|
"preprocessor",
|
||||||
@@ -2358,6 +2411,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_bytes",
|
"serde_bytes",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"simplecss",
|
||||||
"spin",
|
"spin",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -2493,7 +2547,7 @@ dependencies = [
|
|||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"core_maths",
|
"core_maths",
|
||||||
"read-fonts",
|
"read-fonts 0.39.2",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3348,7 +3402,7 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"regex-automata",
|
"regex-automata 0.4.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3369,11 +3423,9 @@ dependencies = [
|
|||||||
name = "math-parser"
|
name = "math-parser"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chumsky",
|
||||||
"criterion",
|
"criterion",
|
||||||
"lazy_static",
|
|
||||||
"num-complex",
|
"num-complex",
|
||||||
"pest",
|
|
||||||
"pest_derive",
|
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3958,9 +4010,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell_polyfill"
|
name = "once_cell_polyfill"
|
||||||
@@ -4031,7 +4083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.45.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4087,7 +4139,7 @@ dependencies = [
|
|||||||
"linebender_resource_handle",
|
"linebender_resource_handle",
|
||||||
"parlance",
|
"parlance",
|
||||||
"parley_data",
|
"parley_data",
|
||||||
"skrifa",
|
"skrifa 0.42.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4138,50 +4190,6 @@ version = "2.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pest"
|
|
||||||
version = "2.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
"thiserror 2.0.18",
|
|
||||||
"ucd-trie",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pest_derive"
|
|
||||||
version = "2.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc"
|
|
||||||
dependencies = [
|
|
||||||
"pest",
|
|
||||||
"pest_generator",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pest_generator"
|
|
||||||
version = "2.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966"
|
|
||||||
dependencies = [
|
|
||||||
"pest",
|
|
||||||
"pest_meta",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.106",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pest_meta"
|
|
||||||
version = "2.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5"
|
|
||||||
dependencies = [
|
|
||||||
"pest",
|
|
||||||
"sha2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "petgraph"
|
name = "petgraph"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@@ -4603,7 +4611,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4787,7 +4795,18 @@ checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"core_maths",
|
"core_maths",
|
||||||
"font-types",
|
"font-types 0.11.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "read-fonts"
|
||||||
|
version = "0.41.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
"font-types 0.12.3",
|
||||||
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4824,8 +4843,19 @@ checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-automata",
|
"regex-automata 0.4.14",
|
||||||
"regex-syntax",
|
"regex-syntax 0.8.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax 0.7.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4836,9 +4866,15 @@ checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-syntax",
|
"regex-syntax 0.8.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.8.6"
|
version = "0.8.6"
|
||||||
@@ -4856,6 +4892,7 @@ name = "rendering"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
|
"brush-types",
|
||||||
"core-types",
|
"core-types",
|
||||||
"dyn-any",
|
"dyn-any",
|
||||||
"glam",
|
"glam",
|
||||||
@@ -4867,7 +4904,7 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
"parley",
|
"parley",
|
||||||
"serde",
|
"serde",
|
||||||
"skrifa",
|
"skrifa 0.42.1",
|
||||||
"text-nodes",
|
"text-nodes",
|
||||||
"usvg",
|
"usvg",
|
||||||
"vector-types",
|
"vector-types",
|
||||||
@@ -4991,6 +5028,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "robust"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ron"
|
name = "ron"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
@@ -5081,7 +5124,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5149,7 +5192,7 @@ dependencies = [
|
|||||||
"security-framework",
|
"security-framework",
|
||||||
"security-framework-sys",
|
"security-framework-sys",
|
||||||
"webpki-root-certs",
|
"webpki-root-certs",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5530,7 +5573,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564"
|
checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"read-fonts",
|
"read-fonts 0.39.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "skrifa"
|
||||||
|
version = "0.44.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
"read-fonts 0.41.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5882,7 +5935,7 @@ dependencies = [
|
|||||||
"getrandom 0.3.3",
|
"getrandom 0.3.3",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5922,7 +5975,7 @@ dependencies = [
|
|||||||
"raster-types",
|
"raster-types",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"skrifa",
|
"skrifa 0.42.1",
|
||||||
"titlecase",
|
"titlecase",
|
||||||
"tsify",
|
"tsify",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
@@ -6303,7 +6356,7 @@ dependencies = [
|
|||||||
"matchers",
|
"matchers",
|
||||||
"nu-ansi-term",
|
"nu-ansi-term",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"regex-automata",
|
"regex-automata 0.4.14",
|
||||||
"sharded-slab",
|
"sharded-slab",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thread_local",
|
"thread_local",
|
||||||
@@ -6382,12 +6435,6 @@ version = "1.18.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ucd-trie"
|
|
||||||
version = "0.1.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.18"
|
version = "0.3.18"
|
||||||
@@ -6574,6 +6621,7 @@ name = "vector-nodes"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"core-types",
|
"core-types",
|
||||||
|
"delaunator",
|
||||||
"dyn-any",
|
"dyn-any",
|
||||||
"futures",
|
"futures",
|
||||||
"glam",
|
"glam",
|
||||||
@@ -6600,6 +6648,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
|
"color",
|
||||||
"core-types",
|
"core-types",
|
||||||
"dyn-any",
|
"dyn-any",
|
||||||
"fixedbitset",
|
"fixedbitset",
|
||||||
@@ -6614,6 +6663,7 @@ dependencies = [
|
|||||||
"polycool",
|
"polycool",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"tinyvec",
|
"tinyvec",
|
||||||
"tsify",
|
"tsify",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -6621,16 +6671,16 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vello"
|
name = "vello"
|
||||||
version = "0.9.0"
|
version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "261359dbef879f8110ef7e1c442246c838d33d3d91cb05e0ea9288d432760c9f"
|
checksum = "af76ceb17b2869be23598baef40a9c8db4de86b87c60510c3bc245559275a617"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"futures-intrusive",
|
"futures-intrusive",
|
||||||
"log",
|
"log",
|
||||||
"peniko",
|
"peniko",
|
||||||
"png 0.18.1",
|
"png 0.18.1",
|
||||||
"skrifa",
|
"skrifa 0.44.0",
|
||||||
"static_assertions",
|
"static_assertions",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"vello_encoding",
|
"vello_encoding",
|
||||||
@@ -6640,22 +6690,22 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vello_encoding"
|
name = "vello_encoding"
|
||||||
version = "0.9.0"
|
version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2346f5f0d7dccb3582fcd397b4a57b43165209f1424e0d76e85dd814db164af7"
|
checksum = "1e31cd622201690d8dfe9fd8fea8d1ae59db1bfeea414856a617d1d03438418c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"guillotiere",
|
"guillotiere",
|
||||||
"peniko",
|
"peniko",
|
||||||
"skrifa",
|
"skrifa 0.44.0",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vello_shaders"
|
name = "vello_shaders"
|
||||||
version = "0.9.0"
|
version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9dd38937516fa4b47423d9255bb5e4a65e839ec9d57c38c4af6189ce56bf46b7"
|
checksum = "abf943bd2920bfd22928a9c1bad39866f7ffcc1109b6ed924ab52773e3868a83"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"log",
|
"log",
|
||||||
@@ -7173,7 +7223,7 @@ version = "0.1.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22"
|
checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7639,8 +7689,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winit"
|
name = "winit"
|
||||||
version = "0.30.12"
|
version = "0.31.0-beta.2"
|
||||||
source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2879d2854d1a43e48f67322d4bd097afcb6eb8f8f775c8de0260a71aea1df1aa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
@@ -7666,8 +7717,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winit-android"
|
name = "winit-android"
|
||||||
version = "0.30.12"
|
version = "0.31.0-beta.2"
|
||||||
source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51d9c0d2cd93efec3a9f9ad819cfaf0834782403af7c0d248c784ec0c61761df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-activity",
|
"android-activity",
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
@@ -7681,8 +7733,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winit-appkit"
|
name = "winit-appkit"
|
||||||
version = "0.30.12"
|
version = "0.31.0-beta.2"
|
||||||
source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21310ca07851a49c348e0c2cc768e36b52ca65afda2c2354d78ed4b90074d8aa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"block2 0.6.2",
|
"block2 0.6.2",
|
||||||
@@ -7703,8 +7756,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winit-common"
|
name = "winit-common"
|
||||||
version = "0.30.12"
|
version = "0.31.0-beta.2"
|
||||||
source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "45375fbac4cbb77260d83a30b1f9d8105880dbac99a9ae97f56656694680ff69"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memmap2 0.9.10",
|
"memmap2 0.9.10",
|
||||||
"objc2 0.6.3",
|
"objc2 0.6.3",
|
||||||
@@ -7718,8 +7772,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winit-core"
|
name = "winit-core"
|
||||||
version = "0.30.12"
|
version = "0.31.0-beta.2"
|
||||||
source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e4f0ccd7abb43740e2c6124ac7cae7d865ecec74eec63783e8922577ac232583"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"cursor-icon",
|
"cursor-icon",
|
||||||
@@ -7733,8 +7788,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winit-orbital"
|
name = "winit-orbital"
|
||||||
version = "0.30.12"
|
version = "0.31.0-beta.2"
|
||||||
source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51ea1fb262e7209f265f12bd0cc792c399b14355675e65531e9c8a87db287d46"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"dpi",
|
"dpi",
|
||||||
@@ -7748,8 +7804,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winit-uikit"
|
name = "winit-uikit"
|
||||||
version = "0.30.12"
|
version = "0.31.0-beta.2"
|
||||||
source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "680a356e798837d8eb274d4556e83bceaf81698194e31aafc5cfb8a9f2fab643"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"block2 0.6.2",
|
"block2 0.6.2",
|
||||||
@@ -7769,8 +7826,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winit-wayland"
|
name = "winit-wayland"
|
||||||
version = "0.30.12"
|
version = "0.31.0-beta.2"
|
||||||
source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ce5afb2ba07da603f84b722c95f9f9396d2cedae3944fb6c0cda4a6f88de545"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
@@ -7795,8 +7853,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winit-web"
|
name = "winit-web"
|
||||||
version = "0.30.12"
|
version = "0.31.0-beta.2"
|
||||||
source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8c2490a953fb776fbbd5e295d54f1c3847f4f15b6c3929ec53c09acda6487a92"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
@@ -7817,8 +7876,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winit-win32"
|
name = "winit-win32"
|
||||||
version = "0.30.12"
|
version = "0.31.0-beta.2"
|
||||||
source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "644ea78af0e858aa3b092e5d1c67c41995a98220c81813f1353b28bc8bb91eaa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"cursor-icon",
|
"cursor-icon",
|
||||||
@@ -7833,8 +7893,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winit-x11"
|
name = "winit-x11"
|
||||||
version = "0.30.12"
|
version = "0.31.0-beta.2"
|
||||||
source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aa5b600756534c7041aa93cd0d244d44b09fca1b89e202bd1cd80dd9f3636c46"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
|
|||||||
+12
-11
@@ -56,7 +56,7 @@ edition = "2024"
|
|||||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||||
homepage = "https://graphite.art"
|
homepage = "https://graphite.art"
|
||||||
repository = "https://github.com/GraphiteEditor/Graphite"
|
repository = "https://github.com/GraphiteEditor/Graphite"
|
||||||
license = "Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
publish = false
|
publish = false
|
||||||
@@ -80,6 +80,7 @@ no-std-types = { path = "node-graph/libraries/no-std-types" }
|
|||||||
raster-types = { path = "node-graph/libraries/raster-types" }
|
raster-types = { path = "node-graph/libraries/raster-types" }
|
||||||
vector-types = { path = "node-graph/libraries/vector-types" }
|
vector-types = { path = "node-graph/libraries/vector-types" }
|
||||||
graphic-types = { path = "node-graph/libraries/graphic-types" }
|
graphic-types = { path = "node-graph/libraries/graphic-types" }
|
||||||
|
brush-types = { path = "node-graph/libraries/brush-types" }
|
||||||
rendering = { path = "node-graph/libraries/rendering" }
|
rendering = { path = "node-graph/libraries/rendering" }
|
||||||
brush-nodes = { path = "node-graph/nodes/brush" }
|
brush-nodes = { path = "node-graph/nodes/brush" }
|
||||||
blending-nodes = { path = "node-graph/nodes/blending" }
|
blending-nodes = { path = "node-graph/nodes/blending" }
|
||||||
@@ -139,7 +140,7 @@ wgpu = { version = "29.0", features = [
|
|||||||
"strict_asserts",
|
"strict_asserts",
|
||||||
] }
|
] }
|
||||||
once_cell = "1.13" # Remove and replace with `core::cell::LazyCell` (<https://doc.rust-lang.org/core/cell/struct.LazyCell.html>)
|
once_cell = "1.13" # Remove and replace with `core::cell::LazyCell` (<https://doc.rust-lang.org/core/cell/struct.LazyCell.html>)
|
||||||
wasm-bindgen = "=0.2.121" # NOTICE: keep in sync with the `wasm-bindgen-cli` version pinned across CI workflows, devcontainer, Nix, and the `cargo-run` tool. We pin this version because wasm-bindgen upgrades may break various things.
|
wasm-bindgen = "=0.2.121" # NOTICE: keep in sync with the `wasm-bindgen-cli` version pinned in `.github/workflows/build.yml`, `.devcontainer/devcontainer.json`, `.nix/dev.nix`, `.nix/pkgs/graphite.nix`, and `tools/cargo-run/src/requirements.rs`. We pin this version because wasm-bindgen upgrades may break various things.
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
js-sys = "=0.3.98"
|
js-sys = "=0.3.98"
|
||||||
web-sys = { version = "=0.3.98", features = [
|
web-sys = { version = "=0.3.98", features = [
|
||||||
@@ -160,16 +161,17 @@ web-sys = { version = "=0.3.98", features = [
|
|||||||
"HtmlImageElement",
|
"HtmlImageElement",
|
||||||
"ImageBitmapRenderingContext",
|
"ImageBitmapRenderingContext",
|
||||||
] }
|
] }
|
||||||
winit = { git = "https://github.com/rust-windowing/winit.git" }
|
winit = "0.31.0-beta.2"
|
||||||
keyboard-types = "0.8"
|
keyboard-types = "0.8"
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
tokio = { version = "1.29", features = ["fs", "macros", "io-std", "rt", "rt-multi-thread"] }
|
tokio = { version = "1.29", features = ["fs", "macros", "io-std", "rt", "rt-multi-thread"] }
|
||||||
# Linebender ecosystem (BEGIN)
|
# Linebender ecosystem (BEGIN)
|
||||||
kurbo = { version = "0.13", features = ["serde"] }
|
kurbo = { version = "0.13", features = ["serde"] }
|
||||||
vello = "0.9"
|
vello = "0.10"
|
||||||
vello_encoding = "0.9"
|
vello_encoding = "0.10"
|
||||||
resvg = "0.47"
|
resvg = "0.47"
|
||||||
usvg = "0.47"
|
usvg = "0.47"
|
||||||
|
simplecss = "0.2"
|
||||||
parley = { version = "0.9", default-features = false, features = ["std"] }
|
parley = { version = "0.9", default-features = false, features = ["std"] }
|
||||||
skrifa = "0.42"
|
skrifa = "0.42"
|
||||||
polycool = "0.4"
|
polycool = "0.4"
|
||||||
@@ -209,6 +211,7 @@ syn = { version = "2.0", default-features = false, features = [
|
|||||||
"proc-macro",
|
"proc-macro",
|
||||||
] }
|
] }
|
||||||
lyon_geom = "1.0"
|
lyon_geom = "1.0"
|
||||||
|
delaunator = "1.0"
|
||||||
petgraph = { version = "0.7", default-features = false, features = ["graphmap"] }
|
petgraph = { version = "0.7", default-features = false, features = ["graphmap"] }
|
||||||
half = { version = "2.4", default-features = false, features = ["bytemuck"] }
|
half = { version = "2.4", default-features = false, features = ["bytemuck"] }
|
||||||
tinyvec = { version = "1", features = ["std"] }
|
tinyvec = { version = "1", features = ["std"] }
|
||||||
@@ -217,8 +220,8 @@ gungraun = { version = "0.18" }
|
|||||||
ndarray = "0.16"
|
ndarray = "0.16"
|
||||||
strum = { version = "0.27", features = ["derive"] }
|
strum = { version = "0.27", features = ["derive"] }
|
||||||
dirs = "6.0"
|
dirs = "6.0"
|
||||||
cef = "149"
|
cef = "151"
|
||||||
cef-dll-sys = "149"
|
cef-dll-sys = "151"
|
||||||
include_dir = "0.7"
|
include_dir = "0.7"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
@@ -257,8 +260,6 @@ lto = "thin"
|
|||||||
debug = true
|
debug = true
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# Force cargo to use only one version of the dpi crate (vendoring breaks without this)
|
|
||||||
dpi = { git = "https://github.com/rust-windowing/winit.git" }
|
|
||||||
rfd = { git = "https://github.com/timon-schelling/rfd.git", branch = "graphite" } # TODO: Remove this once https://github.com/PolyMeilex/rfd/pull/317 is merged and released
|
rfd = { git = "https://github.com/timon-schelling/rfd.git", branch = "graphite" } # TODO: Remove this once https://github.com/PolyMeilex/rfd/pull/317 is merged and released
|
||||||
cef = { git = "https://github.com/timon-schelling/cef-rs.git", branch = "graphite-149" }
|
cef = { git = "https://github.com/timon-schelling/cef-rs.git", branch = "graphite-151" }
|
||||||
cef-dll-sys = { git = "https://github.com/timon-schelling/cef-rs.git", branch = "graphite-149" }
|
cef-dll-sys = { git = "https://github.com/timon-schelling/cef-rs.git", branch = "graphite-151" }
|
||||||
|
|||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) Graphite contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -80,4 +80,6 @@ Graphite is 100% community built and funded. Please become a part of keeping the
|
|||||||
|
|
||||||
Are you a graphics programmer or Rust developer? Graphite aims to be one of the most approachable projects for putting your engineering skills to use in the world of open source. See [instructions here](https://graphite.art/volunteer/guide/) for setting up the project and getting started.
|
Are you a graphics programmer or Rust developer? Graphite aims to be one of the most approachable projects for putting your engineering skills to use in the world of open source. See [instructions here](https://graphite.art/volunteer/guide/) for setting up the project and getting started.
|
||||||
|
|
||||||
*By submitting code for inclusion in the project, you are agreeing to license your changes under the Apache 2.0 license, and that you have the authority to do so. Some directories may have other licenses, like dual-licensed MIT/Apache 2.0, and code submissions to those directories mean you agree to the applicable license(s).*
|
## License
|
||||||
|
|
||||||
|
Graphite's source code is dual-licensed under your choice of either the [MIT license](LICENSE-MIT) or the [Apache License 2.0](LICENSE-APACHE), at your option, unless otherwise noted within the repository. By submitting code for inclusion in the project, you agree to license your contributions under both licenses, and that you have the authority to do so.
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ accepted = [
|
|||||||
workarounds = ["ring"]
|
workarounds = ["ring"]
|
||||||
ignore-build-dependencies = true
|
ignore-build-dependencies = true
|
||||||
ignore-dev-dependencies = true
|
ignore-dev-dependencies = true
|
||||||
# Clearly Defined's API would occasionally (every few months) return errors for at least a full day (maybe some weird rate limiting?), but we can just disable to perform local checking (see #1653)
|
|
||||||
no-clearly-defined = true
|
|
||||||
|
|
||||||
# https://raw.githubusercontent.com/briansmith/webpki/main/LICENSE
|
# https://raw.githubusercontent.com/briansmith/webpki/main/LICENSE
|
||||||
# is the ISC license but test code within the repo is BSD-3-Clause, but is not compiled into the crate when we use it
|
# is the ISC license but test code within the repo is BSD-3-Clause, but is not compiled into the crate when we use it
|
||||||
|
|||||||
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -3,7 +3,7 @@ name = "graphite-desktop"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Graphite Desktop"
|
description = "Graphite Desktop"
|
||||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||||
license = "Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = ""
|
repository = ""
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.87"
|
rust-version = "1.87"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name = "graphite-desktop-bundle"
|
|||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
description = "Graphite Desktop Bundle"
|
description = "Graphite Desktop Bundle"
|
||||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||||
license = "Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = ""
|
repository = ""
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.87"
|
rust-version = "1.87"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name = "graphite-desktop-embedded-resources"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Graphite Desktop Embedded Resources"
|
description = "Graphite Desktop Embedded Resources"
|
||||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||||
license = "Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = ""
|
repository = ""
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.87"
|
rust-version = "1.87"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name = "graphite-desktop-platform-linux"
|
|||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
description = "Graphite Desktop Platform Linux"
|
description = "Graphite Desktop Platform Linux"
|
||||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||||
license = "Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = ""
|
repository = ""
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.87"
|
rust-version = "1.87"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name = "graphite-desktop-platform-mac"
|
|||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
description = "Graphite Desktop Platform Mac"
|
description = "Graphite Desktop Platform Mac"
|
||||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||||
license = "Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = ""
|
repository = ""
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.87"
|
rust-version = "1.87"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name = "graphite-desktop-platform-win"
|
|||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
description = "Graphite Desktop Platform Windows"
|
description = "Graphite Desktop Platform Windows"
|
||||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||||
license = "Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = ""
|
repository = ""
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.87"
|
rust-version = "1.87"
|
||||||
|
|||||||
+38
-63
@@ -9,19 +9,21 @@ use std::sync::mpsc::{Receiver, SyncSender};
|
|||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use winit::application::ApplicationHandler;
|
use winit::application::ApplicationHandler;
|
||||||
use winit::dpi::{PhysicalPosition, PhysicalSize};
|
use winit::dpi::PhysicalSize;
|
||||||
use winit::event::{ButtonSource, ElementState, MouseButton, StartCause, WindowEvent};
|
use winit::event::{ElementState, MouseButton, StartCause, WindowEvent};
|
||||||
|
use winit::event_loop::run_on_demand::EventLoopExtRunOnDemand;
|
||||||
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||||
use winit::window::WindowId;
|
use winit::window::WindowId;
|
||||||
|
|
||||||
use crate::dirs;
|
use crate::dirs;
|
||||||
use crate::event::{AppEvent, AppEventScheduler};
|
use crate::event::{AppEvent, AppEventScheduler};
|
||||||
|
use crate::input::InputState;
|
||||||
use crate::persist;
|
use crate::persist;
|
||||||
use crate::preferences;
|
use crate::preferences;
|
||||||
use crate::render::{RenderError, RenderState};
|
use crate::render::{RenderError, RenderState};
|
||||||
use crate::ui::{UiCommand, UiInstance};
|
use crate::ui::{InputEvent, UiCommand, UiInstance};
|
||||||
use crate::window::Window;
|
use crate::window::Window;
|
||||||
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState, Preferences};
|
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, Preferences};
|
||||||
use crate::wrapper::{DesktopWrapper, MmapResourceStorage, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
|
use crate::wrapper::{DesktopWrapper, MmapResourceStorage, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
|
||||||
|
|
||||||
pub(crate) struct App {
|
pub(crate) struct App {
|
||||||
@@ -33,8 +35,7 @@ pub(crate) struct App {
|
|||||||
window_maximized: bool,
|
window_maximized: bool,
|
||||||
window_fullscreen: bool,
|
window_fullscreen: bool,
|
||||||
window_pending_drag: bool,
|
window_pending_drag: bool,
|
||||||
pointer_position: PhysicalPosition<f64>,
|
input_state: InputState,
|
||||||
pointer_lock_position: Option<PhysicalPosition<f64>>,
|
|
||||||
ui_scale: f64,
|
ui_scale: f64,
|
||||||
app_event_receiver: Receiver<AppEvent>,
|
app_event_receiver: Receiver<AppEvent>,
|
||||||
app_event_scheduler: AppEventScheduler,
|
app_event_scheduler: AppEventScheduler,
|
||||||
@@ -111,8 +112,7 @@ impl App {
|
|||||||
window_maximized: false,
|
window_maximized: false,
|
||||||
window_fullscreen: false,
|
window_fullscreen: false,
|
||||||
window_pending_drag: false,
|
window_pending_drag: false,
|
||||||
pointer_position: Default::default(),
|
input_state: InputState::new(),
|
||||||
pointer_lock_position: Default::default(),
|
|
||||||
ui_scale: 1.,
|
ui_scale: 1.,
|
||||||
app_event_receiver,
|
app_event_receiver,
|
||||||
app_event_scheduler,
|
app_event_scheduler,
|
||||||
@@ -130,8 +130,8 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn run(mut self, event_loop: EventLoop) -> ExitReason {
|
pub(crate) fn run(mut self, mut event_loop: EventLoop) -> ExitReason {
|
||||||
event_loop.run_app(&mut self).unwrap();
|
event_loop.run_app_on_demand(&mut self).unwrap();
|
||||||
self.exit_reason
|
self.exit_reason
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,6 +269,8 @@ impl App {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
DesktopFrontendMessage::UpdateViewportPhysicalBounds { x, y, width, height } => {
|
DesktopFrontendMessage::UpdateViewportPhysicalBounds { x, y, width, height } => {
|
||||||
|
self.input_state.set_viewport_info(x, y, width, height, self.window_scale);
|
||||||
|
|
||||||
if let Some(render_state) = &mut self.render_state
|
if let Some(render_state) = &mut self.render_state
|
||||||
&& let Some(window) = &self.window
|
&& let Some(window) = &self.window
|
||||||
{
|
{
|
||||||
@@ -283,6 +285,9 @@ impl App {
|
|||||||
render_state.set_viewport_scale([viewport_scale_x as f32, viewport_scale_y as f32]);
|
render_state.set_viewport_scale([viewport_scale_x as f32, viewport_scale_y as f32]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DesktopFrontendMessage::WindowUpdateDirectInput { enabled } => {
|
||||||
|
self.input_state.set_direct_input(enabled);
|
||||||
|
}
|
||||||
DesktopFrontendMessage::UpdateUIScale { scale } => {
|
DesktopFrontendMessage::UpdateUIScale { scale } => {
|
||||||
self.ui_scale = scale;
|
self.ui_scale = scale;
|
||||||
self.resize();
|
self.resize();
|
||||||
@@ -347,7 +352,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
DesktopFrontendMessage::PointerLock => {
|
DesktopFrontendMessage::PointerLock => {
|
||||||
self.pointer_lock_position = Some(self.pointer_position);
|
self.input_state.lock_pointer();
|
||||||
if let Some(window) = &self.window {
|
if let Some(window) = &self.window {
|
||||||
window.start_pointer_lock();
|
window.start_pointer_lock();
|
||||||
}
|
}
|
||||||
@@ -540,26 +545,26 @@ impl ApplicationHandler for App {
|
|||||||
|
|
||||||
fn window_event(&mut self, _event_loop: &dyn ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
|
fn window_event(&mut self, _event_loop: &dyn ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
|
||||||
// Handle pointer lock release
|
// Handle pointer lock release
|
||||||
if let Some(pointer_lock_position) = self.pointer_lock_position
|
if let WindowEvent::PointerButton {
|
||||||
&& let WindowEvent::PointerButton {
|
state: ElementState::Released,
|
||||||
state: ElementState::Released,
|
button,
|
||||||
button: ButtonSource::Mouse(MouseButton::Left),
|
..
|
||||||
..
|
} = &event && button.clone().mouse_button() == Some(MouseButton::Left)
|
||||||
} = event
|
&& let Some(pointer_lock_position) = self.input_state.unlock_pointer()
|
||||||
{
|
{
|
||||||
self.pointer_lock_position = None;
|
|
||||||
if let Some(window) = &self.window {
|
if let Some(window) = &self.window {
|
||||||
window.end_pointer_lock();
|
window.end_pointer_lock();
|
||||||
}
|
}
|
||||||
self.ui.send(UiCommand::Input(WindowEvent::PointerMoved {
|
self.ui.send(UiCommand::Input(
|
||||||
device_id: None,
|
InputEvent::pointer().position(pointer_lock_position).moved().modifiers(self.input_state.modifiers()).build(),
|
||||||
position: pointer_lock_position,
|
));
|
||||||
primary: true,
|
|
||||||
source: winit::event::PointerSource::Mouse,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.ui.send(UiCommand::Input(event.clone()));
|
self.input_state.process(
|
||||||
|
&event,
|
||||||
|
|message| self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::Input(message))),
|
||||||
|
|input| self.ui.send(UiCommand::Input(input)),
|
||||||
|
);
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
WindowEvent::CloseRequested => {
|
WindowEvent::CloseRequested => {
|
||||||
@@ -616,50 +621,20 @@ impl ApplicationHandler for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forward and Back buttons are not supported by CEF and thus need to be directly forwarded the editor
|
WindowEvent::PointerMoved { .. } | WindowEvent::PointerLeft { position: Some(_), .. } | WindowEvent::PointerEntered { .. }
|
||||||
WindowEvent::PointerButton {
|
if !self.input_state.pointer_locked() && self.window_pending_drag =>
|
||||||
button: ButtonSource::Mouse(button),
|
|
||||||
state: ElementState::Pressed,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let mouse_keys = match button {
|
|
||||||
MouseButton::Back => Some(MouseKeys::BACK),
|
|
||||||
MouseButton::Forward => Some(MouseKeys::FORWARD),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
if let Some(mouse_keys) = mouse_keys {
|
|
||||||
let message = DesktopWrapperMessage::Input(InputMessage::PointerDown {
|
|
||||||
editor_mouse_state: MouseState { mouse_keys, ..Default::default() },
|
|
||||||
modifier_keys: Default::default(),
|
|
||||||
});
|
|
||||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
|
||||||
|
|
||||||
let message = DesktopWrapperMessage::Input(InputMessage::PointerUp {
|
|
||||||
editor_mouse_state: Default::default(),
|
|
||||||
modifier_keys: Default::default(),
|
|
||||||
});
|
|
||||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WindowEvent::PointerMoved { position, .. } | WindowEvent::PointerLeft { position: Some(position), .. } | WindowEvent::PointerEntered { position, .. }
|
|
||||||
if self.pointer_lock_position.is_none() =>
|
|
||||||
{
|
{
|
||||||
self.pointer_position = position;
|
self.window_pending_drag = false;
|
||||||
|
if let Some(window) = &self.window {
|
||||||
if self.window_pending_drag {
|
window.start_drag();
|
||||||
self.window_pending_drag = false;
|
|
||||||
if let Some(window) = &self.window {
|
|
||||||
window.start_drag();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowEvent::PointerButton {
|
WindowEvent::PointerButton {
|
||||||
button: ButtonSource::Mouse(MouseButton::Left),
|
button,
|
||||||
state: ElementState::Released,
|
state: ElementState::Released,
|
||||||
..
|
..
|
||||||
} => {
|
} if button.clone().mouse_button() == Some(MouseButton::Left) => {
|
||||||
self.window_pending_drag = false;
|
self.window_pending_drag = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -668,7 +643,7 @@ impl ApplicationHandler for App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn device_event(&mut self, _event_loop: &dyn ActiveEventLoop, _device_id: Option<winit::event::DeviceId>, event: winit::event::DeviceEvent) {
|
fn device_event(&mut self, _event_loop: &dyn ActiveEventLoop, _device_id: Option<winit::event::DeviceId>, event: winit::event::DeviceEvent) {
|
||||||
if self.pointer_lock_position.is_some()
|
if self.input_state.pointer_locked()
|
||||||
&& let winit::event::DeviceEvent::PointerMotion { delta: (x, y) } = event
|
&& let winit::event::DeviceEvent::PointerMotion { delta: (x, y) } = event
|
||||||
{
|
{
|
||||||
let message = DesktopWrapperMessage::PointerLockMove { x, y };
|
let message = DesktopWrapperMessage::PointerLockMove { x, y };
|
||||||
|
|||||||
@@ -0,0 +1,453 @@
|
|||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use winit::dpi::PhysicalPosition;
|
||||||
|
use winit::event::{ButtonSource, ElementState, MouseButton, MouseScrollDelta, PointerSource, TabletToolData, TabletToolKind, WindowEvent};
|
||||||
|
use winit::keyboard::ModifiersState;
|
||||||
|
|
||||||
|
use crate::ui::{InputEvent, MULTICLICK_ALLOWED_TRAVEL, MULTICLICK_TIMEOUT, PINCH_ZOOM_SPEED, SCROLL_LINE_HEIGHT, SCROLL_LINE_WIDTH, SCROLL_SPEED_X, SCROLL_SPEED_Y};
|
||||||
|
use crate::wrapper::messages::{EditorPointerState, InputMessage, ModifierKeys, MouseKeys, ScrollDelta};
|
||||||
|
|
||||||
|
// Marks input event as observe-only, meaning it should not result in any input messages being send by the frontend.
|
||||||
|
// And should instead be ignored by frontend input processing and only be used to update the UI state (hover, cursor).
|
||||||
|
// TODO(Timon): Remove and find a less hacky solution
|
||||||
|
trait ObserveOnlyExt {
|
||||||
|
fn observe_only(self) -> Self;
|
||||||
|
}
|
||||||
|
impl ObserveOnlyExt for crate::ui::InputEventBuilder {
|
||||||
|
fn observe_only(self) -> Self {
|
||||||
|
self.num_lock(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct InputState {
|
||||||
|
start: Instant,
|
||||||
|
viewport_info: Option<ViewportInfo>,
|
||||||
|
direct_input: bool,
|
||||||
|
modifiers: ModifiersState,
|
||||||
|
pointer_position: PhysicalPosition<f64>,
|
||||||
|
pointer_state: PointerState,
|
||||||
|
click_tracker: ClickTracker,
|
||||||
|
shake_tracker: ShakeTracker,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputState {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
start: Instant::now(),
|
||||||
|
viewport_info: None,
|
||||||
|
direct_input: false,
|
||||||
|
modifiers: ModifiersState::default(),
|
||||||
|
pointer_position: PhysicalPosition::default(),
|
||||||
|
pointer_state: PointerState::Hover { route: Route::Ui },
|
||||||
|
click_tracker: ClickTracker::default(),
|
||||||
|
shake_tracker: ShakeTracker::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_viewport_info(&mut self, x: f64, y: f64, width: f64, height: f64, scale: f64) {
|
||||||
|
self.viewport_info = Some(ViewportInfo { x, y, width, height, scale });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_direct_input(&mut self, enabled: bool) {
|
||||||
|
self.direct_input = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn lock_pointer(&mut self) {
|
||||||
|
self.pointer_state = match self.pointer_state {
|
||||||
|
PointerState::Hover { route } => PointerState::Locked {
|
||||||
|
route,
|
||||||
|
keys: MouseKeys::empty(),
|
||||||
|
position: self.pointer_position,
|
||||||
|
},
|
||||||
|
PointerState::Stroke { route, keys } | PointerState::Locked { route, keys, .. } => PointerState::Locked {
|
||||||
|
route,
|
||||||
|
keys,
|
||||||
|
position: self.pointer_position,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn unlock_pointer(&mut self) -> Option<PhysicalPosition<f64>> {
|
||||||
|
let PointerState::Locked {
|
||||||
|
route: resume,
|
||||||
|
keys,
|
||||||
|
position: restore,
|
||||||
|
} = self.pointer_state
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
self.pointer_position = restore;
|
||||||
|
self.pointer_state = match keys.is_empty() {
|
||||||
|
true => PointerState::Hover { route: Route::Ui },
|
||||||
|
false => PointerState::Stroke { route: resume, keys },
|
||||||
|
};
|
||||||
|
Some(restore)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn pointer_locked(&self) -> bool {
|
||||||
|
matches!(self.pointer_state, PointerState::Locked { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn modifiers(&self) -> ModifiersState {
|
||||||
|
self.modifiers
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn process(&mut self, event: &WindowEvent, mut editor_callback: impl FnMut(InputMessage), mut ui_callback: impl FnMut(InputEvent)) {
|
||||||
|
match event {
|
||||||
|
WindowEvent::PointerMoved { position, source, .. } => {
|
||||||
|
self.pointer_position = *position;
|
||||||
|
|
||||||
|
let route = match self.pointer_state {
|
||||||
|
PointerState::Hover { .. } => {
|
||||||
|
let next = self.route(*position);
|
||||||
|
self.pointer_state = PointerState::Hover { route: next };
|
||||||
|
next
|
||||||
|
}
|
||||||
|
PointerState::Stroke { route, .. } => route,
|
||||||
|
PointerState::Locked { keys, route: resume, .. } => match keys.is_empty() {
|
||||||
|
true => Route::Ui,
|
||||||
|
false => resume,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
match route {
|
||||||
|
Route::Ui => ui_callback(InputEvent::pointer().position(*position).moved().modifiers(self.modifiers).build()),
|
||||||
|
Route::Editor => {
|
||||||
|
if !self.pointer_locked() {
|
||||||
|
ui_callback(InputEvent::pointer().position(*position).moved().modifiers(self.modifiers).observe_only().build());
|
||||||
|
}
|
||||||
|
let editor_mouse_state = match source {
|
||||||
|
PointerSource::TabletTool { kind, data } => self.tablet_pointer_state(kind, data),
|
||||||
|
_ => self.pointer_state(),
|
||||||
|
};
|
||||||
|
let modifier_keys = self.modifier_keys();
|
||||||
|
if self.shake_tracker.detect(*position, self.start.elapsed()) {
|
||||||
|
editor_callback(InputMessage::PointerShake { editor_mouse_state, modifier_keys });
|
||||||
|
}
|
||||||
|
editor_callback(InputMessage::PointerMove { editor_mouse_state, modifier_keys });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowEvent::PointerEntered { position, .. } => {
|
||||||
|
self.pointer_position = *position;
|
||||||
|
ui_callback(InputEvent::pointer().position(*position).entered().modifiers(self.modifiers).build())
|
||||||
|
}
|
||||||
|
WindowEvent::PointerLeft { position: Some(position), .. } => {
|
||||||
|
self.pointer_position = *position;
|
||||||
|
ui_callback(InputEvent::pointer().position(*position).exited().modifiers(self.modifiers).build())
|
||||||
|
}
|
||||||
|
WindowEvent::PointerLeft { position: None, .. } => ui_callback(InputEvent::pointer().exited().modifiers(self.modifiers).build()),
|
||||||
|
WindowEvent::PointerButton { state, button, position, .. } => {
|
||||||
|
self.pointer_position = *position;
|
||||||
|
|
||||||
|
let mouse_button = button.clone().mouse_button();
|
||||||
|
let keys = match mouse_button {
|
||||||
|
Some(MouseButton::Left) => MouseKeys::LEFT,
|
||||||
|
Some(MouseButton::Right) => MouseKeys::RIGHT,
|
||||||
|
Some(MouseButton::Middle) => MouseKeys::MIDDLE,
|
||||||
|
Some(MouseButton::Back) => MouseKeys::BACK,
|
||||||
|
Some(MouseButton::Forward) => MouseKeys::FORWARD,
|
||||||
|
_ => MouseKeys::NONE,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (pointer, route) = match self.pointer_state {
|
||||||
|
PointerState::Hover { route } => match (state.is_pressed(), keys.is_empty()) {
|
||||||
|
(true, false) => {
|
||||||
|
let route = self.route(*position);
|
||||||
|
(PointerState::Stroke { route, keys }, route)
|
||||||
|
}
|
||||||
|
(true, true) => (PointerState::Hover { route }, self.route(*position)),
|
||||||
|
(false, _) => (PointerState::Hover { route }, route),
|
||||||
|
},
|
||||||
|
PointerState::Stroke { route, keys: mut held } => {
|
||||||
|
match state.is_pressed() {
|
||||||
|
true => held.insert(keys),
|
||||||
|
false => held.remove(keys),
|
||||||
|
}
|
||||||
|
match held.is_empty() {
|
||||||
|
true => (PointerState::Hover { route }, route),
|
||||||
|
false => (PointerState::Stroke { route, keys: held }, route),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PointerState::Locked { route, keys: mut held, position } => {
|
||||||
|
let resume = if state.is_pressed() && held.is_empty() { Route::Ui } else { route };
|
||||||
|
match state.is_pressed() {
|
||||||
|
true => held.insert(keys),
|
||||||
|
false => held.remove(keys),
|
||||||
|
}
|
||||||
|
(PointerState::Locked { route: resume, keys: held, position }, Route::Ui)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.pointer_state = pointer;
|
||||||
|
|
||||||
|
let count = mouse_button.map_or(1, |button| self.click_tracker.input(*position, button, *state));
|
||||||
|
|
||||||
|
let back_or_forward = matches!(mouse_button, Some(MouseButton::Back | MouseButton::Forward));
|
||||||
|
let pointer = InputEvent::pointer().position(*position);
|
||||||
|
let input = match state {
|
||||||
|
ElementState::Pressed => pointer.pressed(button.clone(), count),
|
||||||
|
ElementState::Released => pointer.released(button.clone(), count),
|
||||||
|
}
|
||||||
|
.modifiers(self.modifiers);
|
||||||
|
if self.pointer_locked() || keys.is_empty() || !(back_or_forward || route == Route::Editor) {
|
||||||
|
ui_callback(input.build());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if route == Route::Editor {
|
||||||
|
ui_callback(input.observe_only().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
let editor_mouse_state = match button {
|
||||||
|
ButtonSource::TabletTool { kind, data, .. } => self.tablet_pointer_state(kind, data),
|
||||||
|
_ => self.pointer_state(),
|
||||||
|
};
|
||||||
|
let modifier_keys = self.modifier_keys();
|
||||||
|
match state {
|
||||||
|
ElementState::Pressed => editor_callback(InputMessage::PointerDown { editor_mouse_state, modifier_keys }),
|
||||||
|
ElementState::Released if count % 2 == 0 => {
|
||||||
|
editor_callback(InputMessage::PointerUp { editor_mouse_state, modifier_keys });
|
||||||
|
editor_callback(InputMessage::DoubleClick {
|
||||||
|
editor_mouse_state: EditorPointerState {
|
||||||
|
mouse_keys: keys,
|
||||||
|
..editor_mouse_state
|
||||||
|
},
|
||||||
|
modifier_keys,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ElementState::Released => editor_callback(InputMessage::PointerUp { editor_mouse_state, modifier_keys }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowEvent::MouseWheel { delta, .. } => {
|
||||||
|
if self.pointer_locked() || self.ui_captures(self.pointer_position) {
|
||||||
|
let input = match delta {
|
||||||
|
MouseScrollDelta::LineDelta(x, y) => InputEvent::pointer().scrolled_lines(f64::from(*x), f64::from(*y)),
|
||||||
|
MouseScrollDelta::PixelDelta(position) => InputEvent::pointer().scrolled_pixels(position.x, position.y),
|
||||||
|
};
|
||||||
|
ui_callback(input.modifiers(self.modifiers).build());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (x, y) = match delta {
|
||||||
|
MouseScrollDelta::LineDelta(x, y) => (f64::from(*x) * SCROLL_LINE_WIDTH, f64::from(*y) * SCROLL_LINE_HEIGHT),
|
||||||
|
MouseScrollDelta::PixelDelta(position) => (position.x, position.y),
|
||||||
|
};
|
||||||
|
|
||||||
|
let scroll_delta = ScrollDelta::new(-x * SCROLL_SPEED_X, -y * SCROLL_SPEED_Y, 0.);
|
||||||
|
|
||||||
|
editor_callback(InputMessage::WheelScroll {
|
||||||
|
editor_mouse_state: EditorPointerState { scroll_delta, ..self.pointer_state() },
|
||||||
|
modifier_keys: self.modifier_keys(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
WindowEvent::PinchGesture { delta, .. } => {
|
||||||
|
if self.pointer_locked() || self.ui_captures(self.pointer_position) || !delta.is_normal() {
|
||||||
|
ui_callback(InputEvent::pointer().zoomed(*delta).modifiers(self.modifiers).build());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This is a temporary solution to handle pinch gestures, we should handle pinch gestures editor-side instead.
|
||||||
|
let scroll_delta = ScrollDelta::new(0., -delta * PINCH_ZOOM_SPEED, 0.);
|
||||||
|
editor_callback(InputMessage::WheelScroll {
|
||||||
|
editor_mouse_state: EditorPointerState { scroll_delta, ..self.pointer_state() },
|
||||||
|
modifier_keys: self.modifier_keys() | ModifierKeys::CONTROL,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
WindowEvent::ModifiersChanged(modifiers) => {
|
||||||
|
self.modifiers = modifiers.state();
|
||||||
|
}
|
||||||
|
WindowEvent::KeyboardInput { event, .. } => ui_callback(InputEvent::key(event).modifiers(self.modifiers).build()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scale(&self) -> f64 {
|
||||||
|
self.viewport_info.as_ref().map_or(1., |info| info.scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ui_captures(&self, position: PhysicalPosition<f64>) -> bool {
|
||||||
|
!self.direct_input || !self.viewport_info.as_ref().is_some_and(|info| info.contains(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn route(&self, position: PhysicalPosition<f64>) -> Route {
|
||||||
|
if self.ui_captures(position) { Route::Ui } else { Route::Editor }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pointer_keys(&self) -> MouseKeys {
|
||||||
|
match self.pointer_state {
|
||||||
|
PointerState::Hover { .. } => MouseKeys::empty(),
|
||||||
|
PointerState::Stroke { keys, .. } | PointerState::Locked { keys, .. } => keys,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pointer_state(&self) -> EditorPointerState {
|
||||||
|
EditorPointerState {
|
||||||
|
editor_position: (self.pointer_position.x / self.scale(), self.pointer_position.y / self.scale()).into(),
|
||||||
|
mouse_keys: self.pointer_keys(),
|
||||||
|
time: Some(self.start.elapsed().as_secs_f64() * 1000.),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tablet_pointer_state(&self, kind: &TabletToolKind, data: &TabletToolData) -> EditorPointerState {
|
||||||
|
EditorPointerState {
|
||||||
|
pressure: data.force.map(|force| force.normalized(None)),
|
||||||
|
tilt: data.clone().tilt().map(|tilt| (f64::from(tilt.x), f64::from(tilt.y)).into()),
|
||||||
|
twist: data.twist.map(f64::from),
|
||||||
|
wheel: data.tangential_force.map(f64::from),
|
||||||
|
eraser: matches!(kind, TabletToolKind::Eraser),
|
||||||
|
..self.pointer_state()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modifier_keys(&self) -> ModifierKeys {
|
||||||
|
let mut keys = ModifierKeys::empty();
|
||||||
|
keys.set(ModifierKeys::SHIFT, self.modifiers.shift_key());
|
||||||
|
keys.set(ModifierKeys::CONTROL, self.modifiers.control_key());
|
||||||
|
keys.set(ModifierKeys::ALT, self.modifiers.alt_key());
|
||||||
|
keys.set(ModifierKeys::META_OR_COMMAND, self.modifiers.meta_key());
|
||||||
|
keys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum PointerState {
|
||||||
|
Hover { route: Route },
|
||||||
|
Stroke { route: Route, keys: MouseKeys },
|
||||||
|
Locked { route: Route, keys: MouseKeys, position: PhysicalPosition<f64> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
enum Route {
|
||||||
|
Ui,
|
||||||
|
Editor,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ViewportInfo {
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
width: f64,
|
||||||
|
height: f64,
|
||||||
|
scale: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViewportInfo {
|
||||||
|
fn contains(&self, position: PhysicalPosition<f64>) -> bool {
|
||||||
|
position.x >= self.x && position.y >= self.y && position.x <= self.x + self.width && position.y <= self.y + self.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct ClickTracker {
|
||||||
|
left: ClickChains,
|
||||||
|
right: ClickChains,
|
||||||
|
middle: ClickChains,
|
||||||
|
back: ClickChains,
|
||||||
|
forward: ClickChains,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClickTracker {
|
||||||
|
fn input(&mut self, position: PhysicalPosition<f64>, button: MouseButton, state: ElementState) -> u32 {
|
||||||
|
let position = (position.x as i32, position.y as i32);
|
||||||
|
let clicks = match button {
|
||||||
|
MouseButton::Left => &mut self.left,
|
||||||
|
MouseButton::Right => &mut self.right,
|
||||||
|
MouseButton::Middle => &mut self.middle,
|
||||||
|
MouseButton::Back => &mut self.back,
|
||||||
|
MouseButton::Forward => &mut self.forward,
|
||||||
|
_ => return 1,
|
||||||
|
};
|
||||||
|
let chain = match state {
|
||||||
|
ElementState::Pressed => &mut clicks.down,
|
||||||
|
ElementState::Released => &mut clicks.up,
|
||||||
|
};
|
||||||
|
|
||||||
|
let now = Instant::now();
|
||||||
|
let count = match chain {
|
||||||
|
Some(previous) => {
|
||||||
|
let within_time = now.saturating_duration_since(previous.time) <= MULTICLICK_TIMEOUT;
|
||||||
|
let dx = position.0.abs_diff(previous.position.0) as usize;
|
||||||
|
let dy = position.1.abs_diff(previous.position.1) as usize;
|
||||||
|
let within_distance = dx <= MULTICLICK_ALLOWED_TRAVEL && dy <= MULTICLICK_ALLOWED_TRAVEL;
|
||||||
|
if within_time && within_distance { previous.count.saturating_add(1) } else { 1 }
|
||||||
|
}
|
||||||
|
None => 1,
|
||||||
|
};
|
||||||
|
*chain = Some(Click { time: now, position, count });
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct ClickChains {
|
||||||
|
down: Option<Click>,
|
||||||
|
up: Option<Click>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Click {
|
||||||
|
time: Instant,
|
||||||
|
position: (i32, i32),
|
||||||
|
count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct ShakeTracker {
|
||||||
|
samples: VecDeque<(PhysicalPosition<f64>, Duration)>,
|
||||||
|
last: Option<Duration>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep heuristic and constants in sync with `detectShake` in `frontend/src/utility-functions/input.ts`.
|
||||||
|
impl ShakeTracker {
|
||||||
|
const SENSITIVITY_DIRECTION_CHANGES: usize = 3;
|
||||||
|
const SENSITIVITY_DISTANCE_TO_DISPLACEMENT_RATIO: f64 = 0.1;
|
||||||
|
const DETECTION_WINDOW: Duration = Duration::from_millis(500);
|
||||||
|
const DEBOUNCE: Duration = Duration::from_millis(1000);
|
||||||
|
|
||||||
|
fn detect(&mut self, position: PhysicalPosition<f64>, now: Duration) -> bool {
|
||||||
|
self.samples.push_back((position, now));
|
||||||
|
|
||||||
|
while self.samples.front().is_some_and(|&(_, time)| now.saturating_sub(time) > Self::DETECTION_WINDOW) {
|
||||||
|
self.samples.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.samples.len() <= 3 || self.last.is_some_and(|time| now.saturating_sub(time) <= Self::DEBOUNCE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut total_distance_squared = 0.;
|
||||||
|
for i in 1..self.samples.len() {
|
||||||
|
let (p1, _) = self.samples[i - 1];
|
||||||
|
let (p2, _) = self.samples[i];
|
||||||
|
total_distance_squared += (p2.x - p1.x).powi(2) + (p2.y - p1.y).powi(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut direction_changes = 0;
|
||||||
|
let mut average = PhysicalPosition::new(0., 0.);
|
||||||
|
let interior_count = self.samples.len() - 2;
|
||||||
|
for i in 0..interior_count {
|
||||||
|
let (p1, _) = self.samples[i];
|
||||||
|
let (p2, _) = self.samples[i + 1];
|
||||||
|
let (p3, _) = self.samples[i + 2];
|
||||||
|
|
||||||
|
let vector1 = (p2.x - p1.x, p2.y - p1.y);
|
||||||
|
let vector2 = (p3.x - p2.x, p3.y - p2.y);
|
||||||
|
if vector1.0 * vector2.0 + vector1.1 * vector2.1 < 0. {
|
||||||
|
direction_changes += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
average.x += p2.x;
|
||||||
|
average.y += p2.y;
|
||||||
|
}
|
||||||
|
average.x /= interior_count as f64;
|
||||||
|
average.y /= interior_count as f64;
|
||||||
|
|
||||||
|
let (last, _) = self.samples[self.samples.len() - 1];
|
||||||
|
let displacement_squared = (last.x - average.x).powi(2) + (last.y - average.y).powi(2);
|
||||||
|
|
||||||
|
if Self::SENSITIVITY_DISTANCE_TO_DISPLACEMENT_RATIO * total_distance_squared >= displacement_squared && direction_changes >= Self::SENSITIVITY_DIRECTION_CHANGES {
|
||||||
|
self.last = Some(now);
|
||||||
|
self.samples.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ mod cli;
|
|||||||
mod dirs;
|
mod dirs;
|
||||||
mod event;
|
mod event;
|
||||||
mod gpu_context;
|
mod gpu_context;
|
||||||
|
mod input;
|
||||||
mod persist;
|
mod persist;
|
||||||
mod preferences;
|
mod preferences;
|
||||||
mod render;
|
mod render;
|
||||||
|
|||||||
@@ -52,13 +52,12 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
|||||||
return srgb_to_linear(immediates.background_color);
|
return srgb_to_linear(immediates.background_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
let ui_linear = srgb_to_linear(textureSample(t_ui, s_diffuse, ui_coordinate));
|
let ui_sample = textureSample(t_ui, s_diffuse, ui_coordinate);
|
||||||
if (ui_linear.a >= 0.999) {
|
if (ui_sample.a >= 0.999) {
|
||||||
return ui_linear;
|
return srgb_to_linear(ui_sample);
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI texture is premultiplied, we need to unpremultiply before blending
|
let ui_srgb = unpremultiply(ui_sample);
|
||||||
let ui_srgb = linear_to_srgb(unpremultiply(ui_linear));
|
|
||||||
|
|
||||||
let viewport_coordinate = (in.tex_coords - immediates.viewport_offset) * immediates.viewport_scale;
|
let viewport_coordinate = (in.tex_coords - immediates.viewport_offset) * immediates.viewport_scale;
|
||||||
if (viewport_coordinate.x < 0.0 || viewport_coordinate.x > 1.0 ||
|
if (viewport_coordinate.x < 0.0 || viewport_coordinate.x > 1.0 ||
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use wgpu::PresentMode;
|
use wgpu::PresentMode;
|
||||||
|
|
||||||
use crate::window::Window;
|
use crate::window::Window;
|
||||||
use crate::wrapper::{WgpuContext, WgpuCurrentSurfaceTexture, WgpuExecutor, WgpuSurface};
|
use crate::wrapper::{Texture, WgpuContext, WgpuCurrentSurfaceTexture, WgpuExecutor, WgpuSurface};
|
||||||
|
|
||||||
#[derive(derivative::Derivative)]
|
#[derive(derivative::Derivative)]
|
||||||
#[derivative(Debug)]
|
#[derivative(Debug)]
|
||||||
@@ -10,14 +10,14 @@ pub(crate) struct RenderState {
|
|||||||
executor: WgpuExecutor,
|
executor: WgpuExecutor,
|
||||||
config: wgpu::SurfaceConfiguration,
|
config: wgpu::SurfaceConfiguration,
|
||||||
render_pipeline: wgpu::RenderPipeline,
|
render_pipeline: wgpu::RenderPipeline,
|
||||||
transparent_texture: std::sync::Arc<wgpu::Texture>,
|
transparent_texture: Texture,
|
||||||
sampler: wgpu::Sampler,
|
sampler: wgpu::Sampler,
|
||||||
desired_width: u32,
|
desired_width: u32,
|
||||||
desired_height: u32,
|
desired_height: u32,
|
||||||
viewport_scale: [f32; 2],
|
viewport_scale: [f32; 2],
|
||||||
viewport_offset: [f32; 2],
|
viewport_offset: [f32; 2],
|
||||||
viewport_texture: Option<std::sync::Arc<wgpu::Texture>>,
|
viewport_texture: Option<Texture>,
|
||||||
overlays_texture: Option<std::sync::Arc<wgpu::Texture>>,
|
overlays_texture: Option<Texture>,
|
||||||
ui_texture: Option<wgpu::Texture>,
|
ui_texture: Option<wgpu::Texture>,
|
||||||
bind_group: Option<wgpu::BindGroup>,
|
bind_group: Option<wgpu::BindGroup>,
|
||||||
#[derivative(Debug = "ignore")]
|
#[derivative(Debug = "ignore")]
|
||||||
@@ -46,8 +46,8 @@ impl RenderState {
|
|||||||
|
|
||||||
surface.configure(&context.device, &config);
|
surface.configure(&context.device, &config);
|
||||||
|
|
||||||
let transparent_texture = std::sync::Arc::new(context.device.create_texture(&wgpu::TextureDescriptor {
|
let transparent_texture = Texture::from(context.device.create_texture(&wgpu::TextureDescriptor {
|
||||||
label: Some("Transparent Texture"),
|
label: Some("transparent_fallback"),
|
||||||
size: wgpu::Extent3d {
|
size: wgpu::Extent3d {
|
||||||
width: 1,
|
width: 1,
|
||||||
height: 1,
|
height: 1,
|
||||||
@@ -193,7 +193,7 @@ impl RenderState {
|
|||||||
self.surface_outdated = true;
|
self.surface_outdated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn bind_viewport_texture(&mut self, viewport_texture: std::sync::Arc<wgpu::Texture>) {
|
pub(crate) fn bind_viewport_texture(&mut self, viewport_texture: Texture) {
|
||||||
self.viewport_texture = Some(viewport_texture);
|
self.viewport_texture = Some(viewport_texture);
|
||||||
self.update_bindgroup();
|
self.update_bindgroup();
|
||||||
}
|
}
|
||||||
@@ -231,7 +231,7 @@ impl RenderState {
|
|||||||
let result = self.executor.render_vello_scene(&scene, size, &Default::default(), None);
|
let result = self.executor.render_vello_scene(&scene, size, &Default::default(), None);
|
||||||
match result {
|
match result {
|
||||||
Ok(texture) => {
|
Ok(texture) => {
|
||||||
self.overlays_texture = Some(texture.into());
|
self.overlays_texture = Some(texture);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.overlays_texture = None;
|
self.overlays_texture = None;
|
||||||
|
|||||||
@@ -344,6 +344,14 @@ unsafe fn position_helper(main: HWND, helper: HWND) {
|
|||||||
let h = (r.bottom - r.top) + RESIZE_BAND_THICKNESS * 2;
|
let h = (r.bottom - r.top) + RESIZE_BAND_THICKNESS * 2;
|
||||||
|
|
||||||
let _ = unsafe { SetWindowPos(helper, Some(main), x, y, w, h, SWP_NOACTIVATE | SWP_NOSENDCHANGING) };
|
let _ = unsafe { SetWindowPos(helper, Some(main), x, y, w, h, SWP_NOACTIVATE | SWP_NOSENDCHANGING) };
|
||||||
|
|
||||||
|
let outer = unsafe { CreateRectRgn(0, 0, w, h) };
|
||||||
|
let inner = unsafe { CreateRectRgn(RESIZE_BAND_THICKNESS, RESIZE_BAND_THICKNESS, w - RESIZE_BAND_THICKNESS, h - RESIZE_BAND_THICKNESS) };
|
||||||
|
let _ = unsafe { CombineRgn(Some(outer), Some(outer), Some(inner), RGN_DIFF) };
|
||||||
|
let _ = unsafe { DeleteObject(inner.into()) };
|
||||||
|
if unsafe { SetWindowRgn(helper, Some(outer), false) } == 0 {
|
||||||
|
let _ = unsafe { DeleteObject(outer.into()) };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn calculate_hit(helper: HWND, lparam: LPARAM) -> u32 {
|
unsafe fn calculate_hit(helper: HWND, lparam: LPARAM) -> u32 {
|
||||||
|
|||||||
+18
-18
@@ -1,5 +1,23 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub const MULTICLICK_TIMEOUT: Duration = Duration::from_millis(500);
|
||||||
|
pub const MULTICLICK_ALLOWED_TRAVEL: usize = 4;
|
||||||
|
|
||||||
|
pub const SCROLL_LINE_HEIGHT: f64 = 40.;
|
||||||
|
pub const SCROLL_LINE_WIDTH: f64 = 40.;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub const SCROLL_SPEED_X: f64 = 3.;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub const SCROLL_SPEED_Y: f64 = 3.;
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
pub const SCROLL_SPEED_X: f64 = 1.;
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
pub const SCROLL_SPEED_Y: f64 = 1.;
|
||||||
|
|
||||||
|
pub const PINCH_ZOOM_SPEED: f64 = 300.;
|
||||||
|
|
||||||
pub(crate) const RESOURCE_SCHEME: &str = "resources";
|
pub(crate) const RESOURCE_SCHEME: &str = "resources";
|
||||||
pub(crate) const RESOURCE_DOMAIN: &str = "resources";
|
pub(crate) const RESOURCE_DOMAIN: &str = "resources";
|
||||||
|
|
||||||
@@ -17,21 +35,3 @@ pub(crate) const HOST_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
|
|||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub(crate) const IPC_BOOTSTRAP_PREFIX: &str = "art.graphite.Graphite.ipc.";
|
pub(crate) const IPC_BOOTSTRAP_PREFIX: &str = "art.graphite.Graphite.ipc.";
|
||||||
|
|
||||||
pub(crate) const SCROLL_LINE_HEIGHT: usize = 40;
|
|
||||||
pub(crate) const SCROLL_LINE_WIDTH: usize = 40;
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub(crate) const SCROLL_SPEED_X: f32 = 3.;
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub(crate) const SCROLL_SPEED_Y: f32 = 3.;
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
pub(crate) const SCROLL_SPEED_X: f32 = 1.;
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
pub(crate) const SCROLL_SPEED_Y: f32 = 1.;
|
|
||||||
|
|
||||||
pub(crate) const PINCH_ZOOM_SPEED: f64 = 300.;
|
|
||||||
|
|
||||||
pub(crate) const MULTICLICK_TIMEOUT: Duration = Duration::from_millis(500);
|
|
||||||
pub(crate) const MULTICLICK_ALLOWED_TRAVEL: usize = 4;
|
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ fn create_browser(delegate: BrowserDelegate, frames: FrameStreamer, view_info_se
|
|||||||
.map(|browser| BrowserContext {
|
.map(|browser| BrowserContext {
|
||||||
delegate,
|
delegate,
|
||||||
browser,
|
browser,
|
||||||
|
input: input::InputState::default(),
|
||||||
view_info_sender,
|
view_info_sender,
|
||||||
_instance_dir: instance_dir,
|
_instance_dir: instance_dir,
|
||||||
})
|
})
|
||||||
@@ -252,12 +253,8 @@ pub(crate) enum InitError {
|
|||||||
pub(crate) struct CefContextHandle;
|
pub(crate) struct CefContextHandle;
|
||||||
|
|
||||||
impl CefContextHandle {
|
impl CefContextHandle {
|
||||||
pub(crate) fn apply_input(&self, events: Vec<InputEvent>) {
|
pub(crate) fn process_input(&self, event: InputEvent) {
|
||||||
with_context(move |context| {
|
with_context(move |context| input::process(&mut context.input, &context.browser, &event));
|
||||||
for event in &events {
|
|
||||||
input::apply(&context.browser, event);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn update_view_info(&self, update: ViewInfoUpdate) {
|
pub(crate) fn update_view_info(&self, update: ViewInfoUpdate) {
|
||||||
@@ -276,6 +273,7 @@ impl CefContextHandle {
|
|||||||
struct BrowserContext {
|
struct BrowserContext {
|
||||||
delegate: BrowserDelegate,
|
delegate: BrowserDelegate,
|
||||||
browser: Browser,
|
browser: Browser,
|
||||||
|
input: input::InputState,
|
||||||
view_info_sender: Sender<ViewInfoUpdate>,
|
view_info_sender: Sender<ViewInfoUpdate>,
|
||||||
_instance_dir: TempDir,
|
_instance_dir: TempDir,
|
||||||
}
|
}
|
||||||
|
|||||||
+195
-211
@@ -1,251 +1,235 @@
|
|||||||
use cef::sys::{cef_key_event_type_t, cef_mouse_button_type_t};
|
pub(crate) mod event;
|
||||||
use cef::{Browser, ImplBrowser, ImplBrowserHost, KeyEvent, MouseEvent};
|
pub(crate) use event::InputEvent;
|
||||||
use winit::event::{ButtonSource, ElementState, MouseButton, MouseScrollDelta, WindowEvent};
|
|
||||||
|
|
||||||
mod keymap;
|
mod keymap;
|
||||||
use keymap::{ToCharRepresentation, ToNativeKeycode, ToVKBits};
|
use keymap::{ToCharRepresentation, ToNativeKeycode, ToVKBits};
|
||||||
|
|
||||||
mod state;
|
use cef::sys::{cef_event_flags_t, cef_key_event_type_t, cef_mouse_button_type_t};
|
||||||
pub(crate) use state::{CefModifiers, InputState};
|
use cef::{Browser, ImplBrowser, ImplBrowserHost, KeyEvent, MouseEvent};
|
||||||
|
use winit::dpi::PhysicalPosition;
|
||||||
|
use winit::keyboard::{Key, KeyLocation, NamedKey};
|
||||||
|
|
||||||
use super::consts::{PINCH_ZOOM_SPEED, SCROLL_LINE_HEIGHT, SCROLL_LINE_WIDTH, SCROLL_SPEED_X, SCROLL_SPEED_Y};
|
use super::consts::{PINCH_ZOOM_SPEED, SCROLL_LINE_HEIGHT, SCROLL_LINE_WIDTH, SCROLL_SPEED_X, SCROLL_SPEED_Y};
|
||||||
|
use event::{InputEventKind, KeyAction, Modifiers, MouseButton, PointerAction};
|
||||||
|
|
||||||
/// A window input translated into the plain data CEF consumes — no winit types, so it can
|
#[derive(Default)]
|
||||||
/// be applied to a browser living in another process.
|
pub(crate) struct InputState {
|
||||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
position: PhysicalPosition<f64>,
|
||||||
pub(crate) enum InputEvent {
|
buttons: ButtonStates,
|
||||||
MouseMove { data: MouseData, leave: bool },
|
|
||||||
MouseClick { data: MouseData, button: MouseButtonKind, up: bool, click_count: i32 },
|
|
||||||
MouseWheel { data: MouseData, delta_x: i32, delta_y: i32 },
|
|
||||||
Key(KeyData),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
impl InputState {
|
||||||
pub(crate) struct MouseData {
|
fn pointer_move(&mut self, position: PhysicalPosition<f64>) -> bool {
|
||||||
pub(crate) x: i32,
|
let moved = (position.x as i32, position.y as i32) != (self.position.x as i32, self.position.y as i32);
|
||||||
pub(crate) y: i32,
|
self.position = position;
|
||||||
pub(crate) modifiers: u32,
|
moved
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
pub(crate) fn process(state: &mut InputState, browser: &Browser, event: &InputEvent) {
|
||||||
pub(crate) enum MouseButtonKind {
|
let Some(host) = browser.host() else { return };
|
||||||
Left,
|
match &event.kind {
|
||||||
Right,
|
InputEventKind::Pointer { position, action } => {
|
||||||
Middle,
|
let position = position.unwrap_or(state.position);
|
||||||
}
|
match *action {
|
||||||
|
PointerAction::Move => {
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
if !state.pointer_move(position) {
|
||||||
pub(crate) enum KeyEventKind {
|
return;
|
||||||
RawKeyDown,
|
}
|
||||||
KeyUp,
|
let flags = event_flags(&event.modifiers, &state.buttons);
|
||||||
Char,
|
host.send_mouse_move_event(Some(&mouse_event(position, flags)), 0);
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub(crate) struct KeyData {
|
|
||||||
pub(crate) kind: KeyEventKind,
|
|
||||||
pub(crate) modifiers: u32,
|
|
||||||
pub(crate) windows_key_code: i32,
|
|
||||||
pub(crate) native_key_code: i32,
|
|
||||||
pub(crate) character: u16,
|
|
||||||
pub(crate) unmodified_character: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Turns a winit event into zero or more [`InputEvent`]s, updating the tracked input state
|
|
||||||
/// (cursor position, click counting, modifiers) along the way.
|
|
||||||
pub(crate) fn translate(input_state: &mut InputState, event: &WindowEvent) -> Vec<InputEvent> {
|
|
||||||
match event {
|
|
||||||
WindowEvent::PointerMoved { position, .. } => {
|
|
||||||
if !input_state.cursor_move(position) {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
vec![InputEvent::MouseMove {
|
|
||||||
data: input_state.mouse_data(),
|
|
||||||
leave: false,
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
WindowEvent::PointerEntered { position, .. } => {
|
|
||||||
let _ = input_state.cursor_move(position);
|
|
||||||
vec![InputEvent::MouseMove {
|
|
||||||
data: input_state.mouse_data(),
|
|
||||||
leave: false,
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
WindowEvent::PointerLeft { position, .. } => {
|
|
||||||
if let Some(position) = position {
|
|
||||||
let _ = input_state.cursor_move(position);
|
|
||||||
}
|
|
||||||
vec![InputEvent::MouseMove {
|
|
||||||
data: input_state.mouse_data(),
|
|
||||||
leave: true,
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
WindowEvent::PointerButton { state, button, position, .. } => {
|
|
||||||
let mouse_button = match button {
|
|
||||||
ButtonSource::Mouse(mouse_button) => mouse_button,
|
|
||||||
_ => {
|
|
||||||
return Vec::new(); // TODO: Handle touch input
|
|
||||||
}
|
}
|
||||||
};
|
PointerAction::Enter => {
|
||||||
|
state.pointer_move(position);
|
||||||
|
let flags = event_flags(&event.modifiers, &state.buttons);
|
||||||
|
host.send_mouse_move_event(Some(&mouse_event(position, flags)), 0);
|
||||||
|
}
|
||||||
|
PointerAction::Exit => {
|
||||||
|
state.pointer_move(position);
|
||||||
|
let flags = event_flags(&event.modifiers, &state.buttons);
|
||||||
|
host.send_mouse_move_event(Some(&mouse_event(position, flags)), 1);
|
||||||
|
}
|
||||||
|
PointerAction::Press { button, count } | PointerAction::Release { button, count } => {
|
||||||
|
state.pointer_move(position);
|
||||||
|
let cef_button = match button {
|
||||||
|
MouseButton::Left => cef_mouse_button_type_t::MBT_LEFT,
|
||||||
|
MouseButton::Right => cef_mouse_button_type_t::MBT_RIGHT,
|
||||||
|
MouseButton::Middle => cef_mouse_button_type_t::MBT_MIDDLE,
|
||||||
|
MouseButton::Unknown => return,
|
||||||
|
};
|
||||||
|
let up = matches!(action, PointerAction::Release { .. });
|
||||||
|
state.buttons.update(button, !up);
|
||||||
|
|
||||||
let _ = input_state.cursor_move(position);
|
// CEF only understands single, double and triple clicks; further clicks alternate between double and triple.
|
||||||
let click_count = input_state.mouse_input(mouse_button, state).into();
|
let count = match count {
|
||||||
let up = matches!(state, ElementState::Released);
|
0 | 1 => 1,
|
||||||
let button = match mouse_button {
|
count if count % 2 == 0 => 2,
|
||||||
MouseButton::Left => MouseButtonKind::Left,
|
_ => 3,
|
||||||
MouseButton::Right => MouseButtonKind::Right,
|
};
|
||||||
MouseButton::Middle => MouseButtonKind::Middle,
|
|
||||||
_ => return Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
vec![InputEvent::MouseClick {
|
let flags = event_flags(&event.modifiers, &state.buttons);
|
||||||
data: input_state.mouse_data(),
|
host.send_mouse_click_event(Some(&mouse_event(position, flags)), cef::MouseButtonType::from(cef_button), up as i32, count);
|
||||||
button,
|
}
|
||||||
up,
|
PointerAction::ScrollLines { x, y } => {
|
||||||
click_count,
|
let flags = event_flags(&event.modifiers, &state.buttons);
|
||||||
}]
|
let delta_x = (x * SCROLL_LINE_WIDTH * SCROLL_SPEED_X) as i32;
|
||||||
|
let delta_y = (y * SCROLL_LINE_HEIGHT * SCROLL_SPEED_Y) as i32;
|
||||||
|
host.send_mouse_wheel_event(Some(&mouse_event(position, flags)), delta_x, delta_y);
|
||||||
|
}
|
||||||
|
PointerAction::ScrollPixels { x, y } => {
|
||||||
|
let flags = event_flags(&event.modifiers, &state.buttons) | PRECISION_SCROLLING_DELTA;
|
||||||
|
host.send_mouse_wheel_event(Some(&mouse_event(position, flags)), (x * SCROLL_SPEED_X) as i32, (y * SCROLL_SPEED_Y) as i32);
|
||||||
|
}
|
||||||
|
PointerAction::Zoom(delta) => {
|
||||||
|
if !delta.is_normal() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let flags = CONTROL_DOWN | PRECISION_SCROLLING_DELTA;
|
||||||
|
host.send_mouse_wheel_event(Some(&mouse_event(position, flags)), 0, (delta * PINCH_ZOOM_SPEED).round() as i32);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
WindowEvent::MouseWheel { delta, phase: _, device_id: _, .. } => {
|
InputEventKind::Key {
|
||||||
let (mut delta_x, mut delta_y) = match delta {
|
key,
|
||||||
MouseScrollDelta::LineDelta(x, y) => (x * SCROLL_LINE_WIDTH as f32, y * SCROLL_LINE_HEIGHT as f32),
|
key_without_modifiers,
|
||||||
MouseScrollDelta::PixelDelta(physical_position) => (physical_position.x as f32, physical_position.y as f32),
|
physical_key,
|
||||||
};
|
location,
|
||||||
delta_x *= SCROLL_SPEED_X;
|
text,
|
||||||
delta_y *= SCROLL_SPEED_Y;
|
action,
|
||||||
|
} => {
|
||||||
|
let mut flags = event_flags(&event.modifiers, &state.buttons);
|
||||||
|
|
||||||
vec![InputEvent::MouseWheel {
|
let own_flag = match key {
|
||||||
data: input_state.mouse_data(),
|
Key::Named(NamedKey::Shift) => SHIFT_DOWN,
|
||||||
delta_x: delta_x as i32,
|
Key::Named(NamedKey::Control) => CONTROL_DOWN,
|
||||||
delta_y: delta_y as i32,
|
Key::Named(NamedKey::Alt) => ALT_DOWN,
|
||||||
}]
|
Key::Named(NamedKey::AltGraph) => ALTGR_DOWN,
|
||||||
}
|
Key::Named(NamedKey::Meta) => COMMAND_DOWN,
|
||||||
WindowEvent::ModifiersChanged(modifiers) => {
|
|
||||||
input_state.modifiers_changed(&modifiers.state());
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
WindowEvent::KeyboardInput { device_id: _, event, is_synthetic: _ } => {
|
|
||||||
input_state.modifiers_apply_key_event(&event.logical_key, &event.state);
|
|
||||||
|
|
||||||
let mut kind = match (event.state, &event.logical_key) {
|
|
||||||
(ElementState::Pressed, winit::keyboard::Key::Character(_)) => KeyEventKind::Char,
|
|
||||||
(ElementState::Pressed, _) => KeyEventKind::RawKeyDown,
|
|
||||||
(ElementState::Released, _) => KeyEventKind::KeyUp,
|
|
||||||
};
|
|
||||||
|
|
||||||
let modifiers = input_state.cef_modifiers(&event.location, event.repeat).into();
|
|
||||||
|
|
||||||
let windows_key_code = match &event.logical_key {
|
|
||||||
winit::keyboard::Key::Named(named) => named.to_vk_bits(),
|
|
||||||
winit::keyboard::Key::Character(char) => char.chars().next().unwrap_or_default().to_vk_bits(),
|
|
||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
|
match action {
|
||||||
|
KeyAction::Press | KeyAction::Repeat => flags |= own_flag,
|
||||||
|
KeyAction::Release => flags &= !own_flag,
|
||||||
|
}
|
||||||
|
|
||||||
let native_key_code = event.physical_key.to_native_keycode();
|
flags |= match location {
|
||||||
|
KeyLocation::Left => IS_LEFT,
|
||||||
|
KeyLocation::Right => IS_RIGHT,
|
||||||
|
KeyLocation::Numpad => IS_KEY_PAD,
|
||||||
|
KeyLocation::Standard => 0,
|
||||||
|
};
|
||||||
|
if *action == KeyAction::Repeat {
|
||||||
|
flags |= IS_REPEAT;
|
||||||
|
}
|
||||||
|
|
||||||
let char_representation = event.logical_key.to_char_representation();
|
let windows_key_code = match key {
|
||||||
|
Key::Named(named) => named.to_vk_bits(),
|
||||||
|
Key::Character(char) => char.chars().next().unwrap_or_default().to_vk_bits(),
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
let native_key_code = physical_key.to_native_keycode();
|
||||||
|
|
||||||
|
let char_representation = key.to_char_representation();
|
||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
let mut character = char_representation as u16;
|
let mut character = char_representation as u16;
|
||||||
|
let unmodified_character = key_without_modifiers.to_char_representation() as u16;
|
||||||
if event.state == ElementState::Pressed && character != 0 {
|
|
||||||
kind = KeyEventKind::Char;
|
|
||||||
}
|
|
||||||
|
|
||||||
let unmodified_character = event.key_without_modifiers.to_char_representation() as u16;
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")] // See https://www.magpcss.org/ceforum/viewtopic.php?start=10&t=11650
|
#[cfg(target_os = "macos")] // See https://www.magpcss.org/ceforum/viewtopic.php?start=10&t=11650
|
||||||
if character == 0 && unmodified_character == 0 && event.text_with_all_modifiers.is_some() {
|
if character == 0 && unmodified_character == 0 && text.is_some() {
|
||||||
character = 1;
|
character = 1;
|
||||||
}
|
}
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let _ = text;
|
||||||
|
|
||||||
let key = KeyData {
|
let key_event = |kind: cef_key_event_type_t, windows_key_code: i32| KeyEvent {
|
||||||
kind,
|
type_: kind.into(),
|
||||||
modifiers,
|
modifiers: flags,
|
||||||
windows_key_code,
|
windows_key_code,
|
||||||
native_key_code,
|
native_key_code,
|
||||||
character,
|
character,
|
||||||
unmodified_character,
|
unmodified_character,
|
||||||
};
|
|
||||||
|
|
||||||
if kind == KeyEventKind::Char {
|
|
||||||
// CEF expects a raw key-down before the character event it produces.
|
|
||||||
vec![
|
|
||||||
InputEvent::Key(KeyData {
|
|
||||||
kind: KeyEventKind::RawKeyDown,
|
|
||||||
..key
|
|
||||||
}),
|
|
||||||
InputEvent::Key(KeyData {
|
|
||||||
windows_key_code: char_representation as i32,
|
|
||||||
..key
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
vec![InputEvent::Key(key)]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WindowEvent::PinchGesture { delta, .. } => {
|
|
||||||
if !delta.is_normal() {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = MouseData {
|
|
||||||
modifiers: CefModifiers::PINCH_MODIFIERS.into(),
|
|
||||||
..input_state.mouse_data()
|
|
||||||
};
|
|
||||||
|
|
||||||
vec![InputEvent::MouseWheel {
|
|
||||||
data,
|
|
||||||
delta_x: 0,
|
|
||||||
delta_y: (delta * PINCH_ZOOM_SPEED).round() as i32,
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
_ => Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends a translated [`InputEvent`] to the browser. Must run on the thread owning the browser.
|
|
||||||
pub(crate) fn apply(browser: &Browser, event: &InputEvent) {
|
|
||||||
let Some(host) = browser.host() else { return };
|
|
||||||
match event {
|
|
||||||
InputEvent::MouseMove { data, leave } => {
|
|
||||||
host.send_mouse_move_event(Some(&data.into()), *leave as i32);
|
|
||||||
}
|
|
||||||
InputEvent::MouseClick { data, button, up, click_count } => {
|
|
||||||
let cef_button = cef::MouseButtonType::from(match button {
|
|
||||||
MouseButtonKind::Left => cef_mouse_button_type_t::MBT_LEFT,
|
|
||||||
MouseButtonKind::Right => cef_mouse_button_type_t::MBT_RIGHT,
|
|
||||||
MouseButtonKind::Middle => cef_mouse_button_type_t::MBT_MIDDLE,
|
|
||||||
});
|
|
||||||
host.send_mouse_click_event(Some(&data.into()), cef_button, *up as i32, *click_count);
|
|
||||||
}
|
|
||||||
InputEvent::MouseWheel { data, delta_x, delta_y } => {
|
|
||||||
host.send_mouse_wheel_event(Some(&data.into()), *delta_x, *delta_y);
|
|
||||||
}
|
|
||||||
InputEvent::Key(key) => {
|
|
||||||
let key_event = KeyEvent {
|
|
||||||
type_: match key.kind {
|
|
||||||
KeyEventKind::RawKeyDown => cef_key_event_type_t::KEYEVENT_RAWKEYDOWN,
|
|
||||||
KeyEventKind::KeyUp => cef_key_event_type_t::KEYEVENT_KEYUP,
|
|
||||||
KeyEventKind::Char => cef_key_event_type_t::KEYEVENT_CHAR,
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
modifiers: key.modifiers,
|
|
||||||
windows_key_code: key.windows_key_code,
|
|
||||||
native_key_code: key.native_key_code,
|
|
||||||
character: key.character,
|
|
||||||
unmodified_character: key.unmodified_character,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
host.send_key_event(Some(&key_event));
|
|
||||||
|
match action {
|
||||||
|
KeyAction::Press | KeyAction::Repeat if char_representation != '\0' => {
|
||||||
|
host.send_key_event(Some(&key_event(cef_key_event_type_t::KEYEVENT_RAWKEYDOWN, windows_key_code)));
|
||||||
|
host.send_key_event(Some(&key_event(cef_key_event_type_t::KEYEVENT_CHAR, char_representation as i32)));
|
||||||
|
}
|
||||||
|
KeyAction::Press | KeyAction::Repeat => {
|
||||||
|
host.send_key_event(Some(&key_event(cef_key_event_type_t::KEYEVENT_RAWKEYDOWN, windows_key_code)));
|
||||||
|
}
|
||||||
|
KeyAction::Release => {
|
||||||
|
host.send_key_event(Some(&key_event(cef_key_event_type_t::KEYEVENT_KEYUP, windows_key_code)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&MouseData> for MouseEvent {
|
#[derive(Default)]
|
||||||
fn from(data: &MouseData) -> Self {
|
struct ButtonStates {
|
||||||
MouseEvent {
|
left: bool,
|
||||||
x: data.x,
|
right: bool,
|
||||||
y: data.y,
|
middle: bool,
|
||||||
modifiers: data.modifiers,
|
}
|
||||||
|
|
||||||
|
impl ButtonStates {
|
||||||
|
fn update(&mut self, button: MouseButton, held: bool) {
|
||||||
|
match button {
|
||||||
|
MouseButton::Left => self.left = held,
|
||||||
|
MouseButton::Right => self.right = held,
|
||||||
|
MouseButton::Middle => self.middle = held,
|
||||||
|
MouseButton::Unknown => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mouse_event(position: PhysicalPosition<f64>, flags: u32) -> MouseEvent {
|
||||||
|
MouseEvent {
|
||||||
|
x: position.x as i32,
|
||||||
|
y: position.y as i32,
|
||||||
|
modifiers: flags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event_flags(modifiers: &Modifiers, buttons: &ButtonStates) -> u32 {
|
||||||
|
let bit = |condition: bool, flag: u32| if condition { flag } else { 0 };
|
||||||
|
bit(modifiers.shift, SHIFT_DOWN)
|
||||||
|
| bit(modifiers.control, CONTROL_DOWN)
|
||||||
|
| bit(modifiers.alt, ALT_DOWN)
|
||||||
|
| bit(modifiers.alt_graph, ALTGR_DOWN)
|
||||||
|
| bit(modifiers.meta, COMMAND_DOWN)
|
||||||
|
| bit(modifiers.caps_lock, CAPS_LOCK_ON)
|
||||||
|
| bit(modifiers.num_lock, NUM_LOCK_ON)
|
||||||
|
| bit(buttons.left, LEFT_MOUSE_BUTTON)
|
||||||
|
| bit(buttons.right, RIGHT_MOUSE_BUTTON)
|
||||||
|
| bit(buttons.middle, MIDDLE_MOUSE_BUTTON)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn flag(flag: cef_event_flags_t) -> u32 {
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
flag.0
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
flag.0 as u32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SHIFT_DOWN: u32 = flag(cef_event_flags_t::EVENTFLAG_SHIFT_DOWN);
|
||||||
|
const CONTROL_DOWN: u32 = flag(cef_event_flags_t::EVENTFLAG_CONTROL_DOWN);
|
||||||
|
const ALT_DOWN: u32 = flag(cef_event_flags_t::EVENTFLAG_ALT_DOWN);
|
||||||
|
const ALTGR_DOWN: u32 = flag(cef_event_flags_t::EVENTFLAG_ALTGR_DOWN);
|
||||||
|
const COMMAND_DOWN: u32 = flag(cef_event_flags_t::EVENTFLAG_COMMAND_DOWN);
|
||||||
|
const CAPS_LOCK_ON: u32 = flag(cef_event_flags_t::EVENTFLAG_CAPS_LOCK_ON);
|
||||||
|
const NUM_LOCK_ON: u32 = flag(cef_event_flags_t::EVENTFLAG_NUM_LOCK_ON);
|
||||||
|
const LEFT_MOUSE_BUTTON: u32 = flag(cef_event_flags_t::EVENTFLAG_LEFT_MOUSE_BUTTON);
|
||||||
|
const MIDDLE_MOUSE_BUTTON: u32 = flag(cef_event_flags_t::EVENTFLAG_MIDDLE_MOUSE_BUTTON);
|
||||||
|
const RIGHT_MOUSE_BUTTON: u32 = flag(cef_event_flags_t::EVENTFLAG_RIGHT_MOUSE_BUTTON);
|
||||||
|
const IS_LEFT: u32 = flag(cef_event_flags_t::EVENTFLAG_IS_LEFT);
|
||||||
|
const IS_RIGHT: u32 = flag(cef_event_flags_t::EVENTFLAG_IS_RIGHT);
|
||||||
|
const IS_KEY_PAD: u32 = flag(cef_event_flags_t::EVENTFLAG_IS_KEY_PAD);
|
||||||
|
const IS_REPEAT: u32 = flag(cef_event_flags_t::EVENTFLAG_IS_REPEAT);
|
||||||
|
const PRECISION_SCROLLING_DELTA: u32 = flag(cef_event_flags_t::EVENTFLAG_PRECISION_SCROLLING_DELTA);
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
use winit::dpi::PhysicalPosition;
|
||||||
|
use winit::event::{ElementState, KeyEvent};
|
||||||
|
use winit::keyboard::{Key, KeyLocation, ModifiersState, PhysicalKey, SmolStr};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct InputEvent {
|
||||||
|
pub(crate) kind: InputEventKind,
|
||||||
|
pub(crate) modifiers: Modifiers,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputEvent {
|
||||||
|
pub fn pointer() -> PointerInputEventBuilder {
|
||||||
|
PointerInputEventBuilder { position: UnknownPosition }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(event: &KeyEvent) -> InputEventBuilder {
|
||||||
|
let action = match (event.state, event.repeat) {
|
||||||
|
(ElementState::Pressed, false) => KeyAction::Press,
|
||||||
|
(ElementState::Pressed, true) => KeyAction::Repeat,
|
||||||
|
(ElementState::Released, _) => KeyAction::Release,
|
||||||
|
};
|
||||||
|
InputEventBuilder::new(InputEventKind::Key {
|
||||||
|
key: event.logical_key.clone(),
|
||||||
|
key_without_modifiers: event.key_without_modifiers.clone(),
|
||||||
|
physical_key: event.physical_key,
|
||||||
|
location: event.location,
|
||||||
|
text: event.text_with_all_modifiers.clone(),
|
||||||
|
action,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum MouseButton {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Middle,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<winit::event::MouseButton> for MouseButton {
|
||||||
|
fn from(button: winit::event::MouseButton) -> Self {
|
||||||
|
match button {
|
||||||
|
winit::event::MouseButton::Left => Self::Left,
|
||||||
|
winit::event::MouseButton::Right => Self::Right,
|
||||||
|
winit::event::MouseButton::Middle => Self::Middle,
|
||||||
|
_ => Self::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<winit::event::ButtonSource> for MouseButton {
|
||||||
|
fn from(button: winit::event::ButtonSource) -> Self {
|
||||||
|
button.mouse_button().map_or(Self::Unknown, Self::from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct PointerInputEventBuilder<P = UnknownPosition> {
|
||||||
|
position: P,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointerInputEventBuilder<UnknownPosition> {
|
||||||
|
pub fn position(self, position: PhysicalPosition<f64>) -> PointerInputEventBuilder<PhysicalPosition<f64>> {
|
||||||
|
PointerInputEventBuilder { position }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointerInputEventBuilder<PhysicalPosition<f64>> {
|
||||||
|
pub fn moved(self) -> InputEventBuilder {
|
||||||
|
self.finish(PointerAction::Move)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn entered(self) -> InputEventBuilder {
|
||||||
|
self.finish(PointerAction::Enter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: PointerPosition> PointerInputEventBuilder<P> {
|
||||||
|
pub fn exited(self) -> InputEventBuilder {
|
||||||
|
self.finish(PointerAction::Exit)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pressed(self, button: impl Into<MouseButton>, count: u32) -> InputEventBuilder {
|
||||||
|
self.finish(PointerAction::Press { button: button.into(), count })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn released(self, button: impl Into<MouseButton>, count: u32) -> InputEventBuilder {
|
||||||
|
self.finish(PointerAction::Release { button: button.into(), count })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scrolled_lines(self, x: f64, y: f64) -> InputEventBuilder {
|
||||||
|
self.finish(PointerAction::ScrollLines { x, y })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scrolled_pixels(self, x: f64, y: f64) -> InputEventBuilder {
|
||||||
|
self.finish(PointerAction::ScrollPixels { x, y })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zoomed(self, delta: f64) -> InputEventBuilder {
|
||||||
|
self.finish(PointerAction::Zoom(delta))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(self, action: PointerAction) -> InputEventBuilder {
|
||||||
|
InputEventBuilder::new(InputEventKind::Pointer {
|
||||||
|
position: self.position.position(),
|
||||||
|
action,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct InputEventBuilder {
|
||||||
|
kind: InputEventKind,
|
||||||
|
modifiers: Modifiers,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputEventBuilder {
|
||||||
|
fn new(kind: InputEventKind) -> Self {
|
||||||
|
Self {
|
||||||
|
kind,
|
||||||
|
modifiers: Modifiers::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shift(mut self, on: bool) -> Self {
|
||||||
|
self.modifiers.shift = on;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn control(mut self, on: bool) -> Self {
|
||||||
|
self.modifiers.control = on;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn alt(mut self, on: bool) -> Self {
|
||||||
|
self.modifiers.alt = on;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn alt_graph(mut self, on: bool) -> Self {
|
||||||
|
self.modifiers.alt_graph = on;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn meta(mut self, on: bool) -> Self {
|
||||||
|
self.modifiers.meta = on;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn caps_lock(mut self, on: bool) -> Self {
|
||||||
|
self.modifiers.caps_lock = on;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn num_lock(mut self, on: bool) -> Self {
|
||||||
|
self.modifiers.num_lock = on;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn modifiers(mut self, modifiers: ModifiersState) -> Self {
|
||||||
|
self.modifiers.shift = modifiers.shift_key();
|
||||||
|
self.modifiers.control = modifiers.control_key();
|
||||||
|
self.modifiers.alt = modifiers.alt_key();
|
||||||
|
self.modifiers.meta = modifiers.meta_key();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> InputEvent {
|
||||||
|
InputEvent {
|
||||||
|
kind: self.kind,
|
||||||
|
modifiers: self.modifiers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[expect(private_bounds)]
|
||||||
|
pub trait PointerPosition: OptionalPosition {}
|
||||||
|
impl PointerPosition for UnknownPosition {}
|
||||||
|
impl PointerPosition for PhysicalPosition<f64> {}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct UnknownPosition;
|
||||||
|
|
||||||
|
trait OptionalPosition {
|
||||||
|
fn position(self) -> Option<PhysicalPosition<f64>>;
|
||||||
|
}
|
||||||
|
impl OptionalPosition for UnknownPosition {
|
||||||
|
fn position(self) -> Option<PhysicalPosition<f64>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl OptionalPosition for PhysicalPosition<f64> {
|
||||||
|
fn position(self) -> Option<PhysicalPosition<f64>> {
|
||||||
|
Some(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub(crate) enum InputEventKind {
|
||||||
|
Pointer {
|
||||||
|
position: Option<PhysicalPosition<f64>>,
|
||||||
|
action: PointerAction,
|
||||||
|
},
|
||||||
|
Key {
|
||||||
|
key: Key,
|
||||||
|
key_without_modifiers: Key,
|
||||||
|
physical_key: PhysicalKey,
|
||||||
|
location: KeyLocation,
|
||||||
|
text: Option<SmolStr>,
|
||||||
|
action: KeyAction,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub(crate) enum PointerAction {
|
||||||
|
Move,
|
||||||
|
Enter,
|
||||||
|
Exit,
|
||||||
|
Press { button: MouseButton, count: u32 },
|
||||||
|
Release { button: MouseButton, count: u32 },
|
||||||
|
ScrollLines { x: f64, y: f64 },
|
||||||
|
ScrollPixels { x: f64, y: f64 },
|
||||||
|
Zoom(f64),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub(crate) enum KeyAction {
|
||||||
|
Press,
|
||||||
|
Repeat,
|
||||||
|
Release,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub(crate) struct Modifiers {
|
||||||
|
pub(crate) shift: bool,
|
||||||
|
pub(crate) control: bool,
|
||||||
|
pub(crate) alt: bool,
|
||||||
|
pub(crate) alt_graph: bool,
|
||||||
|
pub(crate) meta: bool,
|
||||||
|
pub(crate) caps_lock: bool,
|
||||||
|
pub(crate) num_lock: bool,
|
||||||
|
}
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
use cef::sys::cef_event_flags_t;
|
|
||||||
use std::time::Instant;
|
|
||||||
use winit::dpi::PhysicalPosition;
|
|
||||||
use winit::event::{ElementState, MouseButton};
|
|
||||||
use winit::keyboard::{Key, KeyLocation, ModifiersState, NamedKey};
|
|
||||||
|
|
||||||
use super::MouseData;
|
|
||||||
use crate::consts::{MULTICLICK_ALLOWED_TRAVEL, MULTICLICK_TIMEOUT};
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub(crate) struct InputState {
|
|
||||||
modifiers: ModifiersState,
|
|
||||||
mouse_position: MousePosition,
|
|
||||||
mouse_state: MouseState,
|
|
||||||
mouse_click_tracker: ClickTracker,
|
|
||||||
}
|
|
||||||
impl InputState {
|
|
||||||
pub(crate) fn modifiers_changed(&mut self, modifiers: &ModifiersState) {
|
|
||||||
self.modifiers = *modifiers;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn modifiers_apply_key_event(&mut self, key: &Key, state: &ElementState) {
|
|
||||||
let bits = match key {
|
|
||||||
Key::Named(NamedKey::Shift) => ModifiersState::SHIFT,
|
|
||||||
Key::Named(NamedKey::Control) => ModifiersState::CONTROL,
|
|
||||||
Key::Named(NamedKey::Alt) => ModifiersState::ALT,
|
|
||||||
Key::Named(NamedKey::Meta) => ModifiersState::META,
|
|
||||||
_ => return,
|
|
||||||
};
|
|
||||||
let is_pressed = matches!(state, ElementState::Pressed);
|
|
||||||
self.modifiers.set(bits, is_pressed);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn cursor_move(&mut self, position: &PhysicalPosition<f64>) -> bool {
|
|
||||||
let new = position.into();
|
|
||||||
if self.mouse_position == new {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
self.mouse_position = new;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn mouse_input(&mut self, button: &MouseButton, state: &ElementState) -> ClickCount {
|
|
||||||
self.mouse_state.update(button, state);
|
|
||||||
self.mouse_click_tracker.input(button, state, self.mouse_position)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn cef_modifiers(&self, location: &KeyLocation, is_repeat: bool) -> CefModifiers {
|
|
||||||
CefModifiers::new(self, location, is_repeat)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn cef_mouse_modifiers(&self) -> CefModifiers {
|
|
||||||
self.cef_modifiers(&KeyLocation::Standard, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn mouse_data(&self) -> MouseData {
|
|
||||||
MouseData {
|
|
||||||
x: self.mouse_position.x,
|
|
||||||
y: self.mouse_position.y,
|
|
||||||
modifiers: self.cef_mouse_modifiers().into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone, Copy, Eq, PartialEq)]
|
|
||||||
pub(crate) struct MousePosition {
|
|
||||||
x: i32,
|
|
||||||
y: i32,
|
|
||||||
}
|
|
||||||
impl From<&PhysicalPosition<f64>> for MousePosition {
|
|
||||||
fn from(position: &PhysicalPosition<f64>) -> Self {
|
|
||||||
Self {
|
|
||||||
x: position.x as i32,
|
|
||||||
y: position.y as i32,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
pub(crate) struct MouseState {
|
|
||||||
left: bool,
|
|
||||||
right: bool,
|
|
||||||
middle: bool,
|
|
||||||
}
|
|
||||||
impl MouseState {
|
|
||||||
pub(crate) fn update(&mut self, button: &MouseButton, state: &ElementState) {
|
|
||||||
match state {
|
|
||||||
ElementState::Pressed => match button {
|
|
||||||
MouseButton::Left => self.left = true,
|
|
||||||
MouseButton::Right => self.right = true,
|
|
||||||
MouseButton::Middle => self.middle = true,
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
ElementState::Released => match button {
|
|
||||||
MouseButton::Left => self.left = false,
|
|
||||||
MouseButton::Right => self.right = false,
|
|
||||||
MouseButton::Middle => self.middle = false,
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct ClickTracker {
|
|
||||||
left: Option<ClickRecord>,
|
|
||||||
middle: Option<ClickRecord>,
|
|
||||||
right: Option<ClickRecord>,
|
|
||||||
}
|
|
||||||
impl ClickTracker {
|
|
||||||
fn input(&mut self, button: &MouseButton, state: &ElementState, position: MousePosition) -> ClickCount {
|
|
||||||
let record = match button {
|
|
||||||
MouseButton::Left => &mut self.left,
|
|
||||||
MouseButton::Right => &mut self.right,
|
|
||||||
MouseButton::Middle => &mut self.middle,
|
|
||||||
_ => return ClickCount::Single,
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(record) = record else {
|
|
||||||
*record = Some(ClickRecord {
|
|
||||||
down_position: position,
|
|
||||||
up_position: position,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
return ClickCount::Single;
|
|
||||||
};
|
|
||||||
|
|
||||||
let now = Instant::now();
|
|
||||||
let within_time = now.saturating_duration_since(record.time) <= MULTICLICK_TIMEOUT;
|
|
||||||
|
|
||||||
let (prev_count, prev_position) = match state {
|
|
||||||
ElementState::Pressed => (record.down_count, record.down_position),
|
|
||||||
ElementState::Released => (record.up_count, record.up_position),
|
|
||||||
};
|
|
||||||
|
|
||||||
let dx = position.x.abs_diff(prev_position.x) as usize;
|
|
||||||
let dy = position.y.abs_diff(prev_position.y) as usize;
|
|
||||||
let within_dist = dx <= MULTICLICK_ALLOWED_TRAVEL && dy <= MULTICLICK_ALLOWED_TRAVEL;
|
|
||||||
|
|
||||||
let count = match (prev_count, within_time, within_dist) {
|
|
||||||
(ClickCount::Single, true, true) => ClickCount::Double,
|
|
||||||
(ClickCount::Double, true, true) => ClickCount::Triple,
|
|
||||||
(ClickCount::Triple, true, true) => ClickCount::Double,
|
|
||||||
_ => ClickCount::Single,
|
|
||||||
};
|
|
||||||
|
|
||||||
record.time = now;
|
|
||||||
|
|
||||||
match state {
|
|
||||||
ElementState::Pressed => {
|
|
||||||
record.down_position = position;
|
|
||||||
record.down_count = count;
|
|
||||||
}
|
|
||||||
ElementState::Released => {
|
|
||||||
record.up_position = position;
|
|
||||||
record.up_count = count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Default)]
|
|
||||||
pub(crate) enum ClickCount {
|
|
||||||
#[default]
|
|
||||||
Single,
|
|
||||||
Double,
|
|
||||||
Triple,
|
|
||||||
}
|
|
||||||
impl From<ClickCount> for i32 {
|
|
||||||
fn from(count: ClickCount) -> i32 {
|
|
||||||
match count {
|
|
||||||
ClickCount::Single => 1,
|
|
||||||
ClickCount::Double => 2,
|
|
||||||
ClickCount::Triple => 3,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
struct ClickRecord {
|
|
||||||
time: Instant,
|
|
||||||
down_position: MousePosition,
|
|
||||||
up_position: MousePosition,
|
|
||||||
down_count: ClickCount,
|
|
||||||
up_count: ClickCount,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ClickRecord {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
time: Instant::now(),
|
|
||||||
down_position: Default::default(),
|
|
||||||
up_position: Default::default(),
|
|
||||||
down_count: Default::default(),
|
|
||||||
up_count: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct CefModifiers(cef_event_flags_t);
|
|
||||||
impl CefModifiers {
|
|
||||||
fn new(input_state: &InputState, location: &KeyLocation, is_repeat: bool) -> Self {
|
|
||||||
let mut inner = cef_event_flags_t::EVENTFLAG_NONE;
|
|
||||||
|
|
||||||
if input_state.modifiers.shift_key() {
|
|
||||||
inner |= cef_event_flags_t::EVENTFLAG_SHIFT_DOWN;
|
|
||||||
}
|
|
||||||
if input_state.modifiers.control_key() {
|
|
||||||
inner |= cef_event_flags_t::EVENTFLAG_CONTROL_DOWN;
|
|
||||||
}
|
|
||||||
if input_state.modifiers.alt_key() {
|
|
||||||
inner |= cef_event_flags_t::EVENTFLAG_ALT_DOWN;
|
|
||||||
}
|
|
||||||
if input_state.modifiers.meta_key() {
|
|
||||||
inner |= cef_event_flags_t::EVENTFLAG_COMMAND_DOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
if input_state.mouse_state.left {
|
|
||||||
inner |= cef_event_flags_t::EVENTFLAG_LEFT_MOUSE_BUTTON;
|
|
||||||
}
|
|
||||||
if input_state.mouse_state.right {
|
|
||||||
inner |= cef_event_flags_t::EVENTFLAG_RIGHT_MOUSE_BUTTON;
|
|
||||||
}
|
|
||||||
if input_state.mouse_state.middle {
|
|
||||||
inner |= cef_event_flags_t::EVENTFLAG_MIDDLE_MOUSE_BUTTON;
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_repeat {
|
|
||||||
inner |= cef_event_flags_t::EVENTFLAG_IS_REPEAT;
|
|
||||||
}
|
|
||||||
|
|
||||||
inner |= match location {
|
|
||||||
KeyLocation::Left => cef_event_flags_t::EVENTFLAG_IS_LEFT,
|
|
||||||
KeyLocation::Right => cef_event_flags_t::EVENTFLAG_IS_RIGHT,
|
|
||||||
KeyLocation::Numpad => cef_event_flags_t::EVENTFLAG_IS_KEY_PAD,
|
|
||||||
KeyLocation::Standard => cef_event_flags_t::EVENTFLAG_NONE,
|
|
||||||
};
|
|
||||||
|
|
||||||
Self(inner)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) const PINCH_MODIFIERS: Self = Self(cef_event_flags_t(
|
|
||||||
cef_event_flags_t::EVENTFLAG_CONTROL_DOWN.0 | cef_event_flags_t::EVENTFLAG_PRECISION_SCROLLING_DELTA.0,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<CefModifiers> for u32 {
|
|
||||||
fn from(val: CefModifiers) -> Self {
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
return val.0.0;
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
return val.0.0 as u32;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+5
-15
@@ -22,6 +22,9 @@ mod resources;
|
|||||||
mod utility;
|
mod utility;
|
||||||
mod view;
|
mod view;
|
||||||
|
|
||||||
|
pub use consts::{MULTICLICK_ALLOWED_TRAVEL, MULTICLICK_TIMEOUT, PINCH_ZOOM_SPEED, SCROLL_LINE_HEIGHT, SCROLL_LINE_WIDTH, SCROLL_SPEED_X, SCROLL_SPEED_Y};
|
||||||
|
pub use input::event::{InputEvent, InputEventBuilder, MouseButton, PointerInputEventBuilder, PointerPosition, UnknownPosition};
|
||||||
|
|
||||||
pub struct UiContext<S: Stage = Started> {
|
pub struct UiContext<S: Stage = Started> {
|
||||||
inner: S::ContextData,
|
inner: S::ContextData,
|
||||||
}
|
}
|
||||||
@@ -67,7 +70,6 @@ impl UiContext<Started> {
|
|||||||
Ok(UiInstance {
|
Ok(UiInstance {
|
||||||
inner: Arc::new(UiInstanceInner {
|
inner: Arc::new(UiInstanceInner {
|
||||||
host: self.inner.clone(),
|
host: self.inner.clone(),
|
||||||
input: Mutex::new(input::InputState::default()),
|
|
||||||
events: Mutex::new(events),
|
events: Mutex::new(events),
|
||||||
queue,
|
queue,
|
||||||
shutdown_complete: Mutex::new(shutdown_complete),
|
shutdown_complete: Mutex::new(shutdown_complete),
|
||||||
@@ -141,7 +143,6 @@ pub struct UiInstance {
|
|||||||
|
|
||||||
pub(crate) struct UiInstanceInner {
|
pub(crate) struct UiInstanceInner {
|
||||||
host: Arc<HostHandle>,
|
host: Arc<HostHandle>,
|
||||||
input: Mutex<input::InputState>,
|
|
||||||
events: Mutex<Receiver<UiEvent>>,
|
events: Mutex<Receiver<UiEvent>>,
|
||||||
queue: events::EventQueue,
|
queue: events::EventQueue,
|
||||||
shutdown_complete: Mutex<Receiver<()>>,
|
shutdown_complete: Mutex<Receiver<()>>,
|
||||||
@@ -152,18 +153,7 @@ impl UiInstance {
|
|||||||
pub fn send(&self, command: UiCommand) {
|
pub fn send(&self, command: UiCommand) {
|
||||||
let shared = &self.inner;
|
let shared = &self.inner;
|
||||||
match command {
|
match command {
|
||||||
UiCommand::Input(event) => {
|
UiCommand::Input(event) => shared.host.send(HostControlMessage::Input(event)),
|
||||||
let events = {
|
|
||||||
let Ok(mut input) = shared.input.lock() else {
|
|
||||||
tracing::error!("Failed to lock the input state");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
input::translate(&mut input, &event)
|
|
||||||
};
|
|
||||||
if !events.is_empty() {
|
|
||||||
shared.host.send(HostControlMessage::Input(events));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
UiCommand::Resized { width, height } => shared.host.send(HostControlMessage::UpdateViewInfo(view::ViewInfoUpdate::Size { width, height })),
|
UiCommand::Resized { width, height } => shared.host.send(HostControlMessage::UpdateViewInfo(view::ViewInfoUpdate::Size { width, height })),
|
||||||
UiCommand::ScaleChanged(scale) => shared.host.send(HostControlMessage::UpdateViewInfo(view::ViewInfoUpdate::Scale(scale))),
|
UiCommand::ScaleChanged(scale) => shared.host.send(HostControlMessage::UpdateViewInfo(view::ViewInfoUpdate::Scale(scale))),
|
||||||
UiCommand::Refresh => shared.host.send(HostControlMessage::RefreshViewInfo),
|
UiCommand::Refresh => shared.host.send(HostControlMessage::RefreshViewInfo),
|
||||||
@@ -220,7 +210,7 @@ impl Drop for UiInstanceInner {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum UiCommand {
|
pub enum UiCommand {
|
||||||
Input(winit::event::WindowEvent),
|
Input(InputEvent),
|
||||||
Resized { width: u32, height: u32 },
|
Resized { width: u32, height: u32 },
|
||||||
ScaleChanged(f64),
|
ScaleChanged(f64),
|
||||||
Refresh,
|
Refresh,
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ enum ControlOutcome {
|
|||||||
fn control_loop(receiver: &IpcReceiver<HostControlMessage>, context: &CefContextHandle, sequence: &SequenceState) -> ControlOutcome {
|
fn control_loop(receiver: &IpcReceiver<HostControlMessage>, context: &CefContextHandle, sequence: &SequenceState) -> ControlOutcome {
|
||||||
loop {
|
loop {
|
||||||
match receiver.recv() {
|
match receiver.recv() {
|
||||||
Ok(HostControlMessage::Input(events)) => context.apply_input(events),
|
Ok(HostControlMessage::Input(event)) => context.process_input(event),
|
||||||
Ok(HostControlMessage::UpdateViewInfo(update)) => context.update_view_info(update),
|
Ok(HostControlMessage::UpdateViewInfo(update)) => context.update_view_info(update),
|
||||||
Ok(HostControlMessage::RefreshViewInfo) => context.refresh_view_info(),
|
Ok(HostControlMessage::RefreshViewInfo) => context.refresh_view_info(),
|
||||||
Ok(HostControlMessage::SendWebMessage(message)) => context.send_web_message(message),
|
Ok(HostControlMessage::SendWebMessage(message)) => context.send_web_message(message),
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::view::ViewInfoUpdate;
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub(crate) enum HostControlMessage {
|
pub(crate) enum HostControlMessage {
|
||||||
Input(Vec<InputEvent>),
|
Input(InputEvent),
|
||||||
UpdateViewInfo(ViewInfoUpdate),
|
UpdateViewInfo(ViewInfoUpdate),
|
||||||
RefreshViewInfo,
|
RefreshViewInfo,
|
||||||
SendWebMessage(Vec<u8>),
|
SendWebMessage(Vec<u8>),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name = "graphite-desktop-wrapper"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Graphite Desktop Wrapper"
|
description = "Graphite Desktop Wrapper"
|
||||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||||
license = "Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = ""
|
repository = ""
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.87"
|
rust-version = "1.87"
|
||||||
|
|||||||
@@ -3,25 +3,25 @@ use graphite_editor::messages::layout::utility_types::layout_widget::LayoutTarge
|
|||||||
use graphite_editor::messages::prelude::FrontendMessage;
|
use graphite_editor::messages::prelude::FrontendMessage;
|
||||||
|
|
||||||
use super::DesktopWrapperMessageDispatcher;
|
use super::DesktopWrapperMessageDispatcher;
|
||||||
use super::messages::{DesktopFrontendMessage, FileFilter, OpenFileDialogContext, SaveFileDialogContext};
|
use super::messages::{DesktopFrontendMessage, OpenFileDialogContext, SaveFileDialogContext};
|
||||||
|
|
||||||
pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: FrontendMessage) -> Option<FrontendMessage> {
|
pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: FrontendMessage) -> Option<FrontendMessage> {
|
||||||
match message {
|
match message {
|
||||||
FrontendMessage::RenderOverlays { context } => {
|
FrontendMessage::RenderOverlays { context } => {
|
||||||
dispatcher.respond(DesktopFrontendMessage::UpdateOverlays(context.take_scene()));
|
dispatcher.respond(DesktopFrontendMessage::UpdateOverlays(context.take_scene()));
|
||||||
}
|
}
|
||||||
FrontendMessage::TriggerOpen => {
|
FrontendMessage::TriggerOpen { filters } => {
|
||||||
dispatcher.respond(DesktopFrontendMessage::OpenFileDialog {
|
dispatcher.respond(DesktopFrontendMessage::OpenFileDialog {
|
||||||
title: "Open Document".to_string(),
|
title: "Open Document".to_string(),
|
||||||
filters: vec![],
|
filters,
|
||||||
multiple: true,
|
multiple: true,
|
||||||
context: OpenFileDialogContext::Open,
|
context: OpenFileDialogContext::Open,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
FrontendMessage::TriggerImport => {
|
FrontendMessage::TriggerImport { filters } => {
|
||||||
dispatcher.respond(DesktopFrontendMessage::OpenFileDialog {
|
dispatcher.respond(DesktopFrontendMessage::OpenFileDialog {
|
||||||
title: "Import File".to_string(),
|
title: "Import File".to_string(),
|
||||||
filters: vec![],
|
filters,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
context: OpenFileDialogContext::Import,
|
context: OpenFileDialogContext::Import,
|
||||||
});
|
});
|
||||||
@@ -31,32 +31,29 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
|
|||||||
name,
|
name,
|
||||||
path,
|
path,
|
||||||
folder,
|
folder,
|
||||||
|
filters,
|
||||||
content,
|
content,
|
||||||
} => {
|
} => {
|
||||||
let content = content.into_vec();
|
let content = content.into_vec();
|
||||||
if let Some(path) = path {
|
if let Some(path) = path {
|
||||||
dispatcher.respond(DesktopFrontendMessage::WriteFile { path, content });
|
dispatcher.respond(DesktopFrontendMessage::WriteFile { path, content });
|
||||||
} else {
|
} else {
|
||||||
let extension = std::path::Path::new(&name).extension().and_then(|extension| extension.to_str()).unwrap_or("*").to_string();
|
|
||||||
dispatcher.respond(DesktopFrontendMessage::SaveFileDialog {
|
dispatcher.respond(DesktopFrontendMessage::SaveFileDialog {
|
||||||
title: "Save Document".to_string(),
|
title: "Save Document".to_string(),
|
||||||
default_filename: name,
|
default_filename: name,
|
||||||
default_folder: folder,
|
default_folder: folder,
|
||||||
filters: vec![FileFilter {
|
filters,
|
||||||
name: "Graphite Document".to_string(),
|
|
||||||
extensions: vec![extension],
|
|
||||||
}],
|
|
||||||
context: SaveFileDialogContext::Document { document_id, content },
|
context: SaveFileDialogContext::Document { document_id, content },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FrontendMessage::TriggerSaveFile { name, folder, content } => {
|
FrontendMessage::TriggerSaveFile { name, folder, filters, content } => {
|
||||||
let content = content.into_vec();
|
let content = content.into_vec();
|
||||||
dispatcher.respond(DesktopFrontendMessage::SaveFileDialog {
|
dispatcher.respond(DesktopFrontendMessage::SaveFileDialog {
|
||||||
title: "Save File".to_string(),
|
title: "Save File".to_string(),
|
||||||
default_filename: name,
|
default_filename: name,
|
||||||
default_folder: folder,
|
default_folder: folder,
|
||||||
filters: Vec::new(),
|
filters,
|
||||||
context: SaveFileDialogContext::File { content },
|
context: SaveFileDialogContext::File { content },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -125,6 +122,9 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
|
|||||||
FrontendMessage::WindowPointerLock => {
|
FrontendMessage::WindowPointerLock => {
|
||||||
dispatcher.respond(DesktopFrontendMessage::PointerLock);
|
dispatcher.respond(DesktopFrontendMessage::PointerLock);
|
||||||
}
|
}
|
||||||
|
FrontendMessage::WindowUpdateDirectInput { enabled } => {
|
||||||
|
dispatcher.respond(DesktopFrontendMessage::WindowUpdateDirectInput { enabled });
|
||||||
|
}
|
||||||
FrontendMessage::WindowClose => {
|
FrontendMessage::WindowClose => {
|
||||||
dispatcher.respond(DesktopFrontendMessage::WindowClose);
|
dispatcher.respond(DesktopFrontendMessage::WindowClose);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
pub use graph_craft::application_io::resource::MmapResourceStorage;
|
pub use graph_craft::application_io::resource::MmapResourceStorage;
|
||||||
pub use graphite_editor::consts::{DOUBLE_CLICK_MILLISECONDS, FILE_EXTENSION};
|
pub use graphite_editor::consts::{DOUBLE_CLICK_MILLISECONDS, FILE_EXTENSION};
|
||||||
|
pub use wgpu_executor::Texture;
|
||||||
pub use wgpu_executor::WgpuBackends;
|
pub use wgpu_executor::WgpuBackends;
|
||||||
pub use wgpu_executor::WgpuContext;
|
pub use wgpu_executor::WgpuContext;
|
||||||
pub use wgpu_executor::WgpuContextBuilder;
|
pub use wgpu_executor::WgpuContextBuilder;
|
||||||
@@ -59,14 +60,14 @@ impl DesktopWrapper {
|
|||||||
pub async fn execute_node_graph() -> NodeGraphExecutionResult {
|
pub async fn execute_node_graph() -> NodeGraphExecutionResult {
|
||||||
let result = graphite_editor::node_graph_executor::run_node_graph().await;
|
let result = graphite_editor::node_graph_executor::run_node_graph().await;
|
||||||
match result {
|
match result {
|
||||||
(true, texture) => NodeGraphExecutionResult::HasRun(texture.map(Into::into)),
|
(true, texture) => NodeGraphExecutionResult::HasRun(texture),
|
||||||
(false, _) => NodeGraphExecutionResult::NotRun,
|
(false, _) => NodeGraphExecutionResult::NotRun,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum NodeGraphExecutionResult {
|
pub enum NodeGraphExecutionResult {
|
||||||
HasRun(Option<std::sync::Arc<wgpu::Texture>>),
|
HasRun(Option<Texture>),
|
||||||
NotRun,
|
NotRun,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
pub(crate) use graphite_editor::messages::prelude::Message as EditorMessage;
|
pub(crate) use graphite_editor::messages::prelude::Message as EditorMessage;
|
||||||
|
|
||||||
pub use graphite_editor::messages::frontend::utility_types::{DocumentInfo, PersistedState};
|
pub use graphite_editor::messages::frontend::utility_types::{DocumentInfo, FileFilter, PersistedState};
|
||||||
pub use graphite_editor::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys};
|
pub use graphite_editor::messages::input_mapper::utility_types::keyboard::{Key, ModifierKeys};
|
||||||
pub use graphite_editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState as MouseState, EditorPosition as Position, MouseKeys};
|
pub use graphite_editor::messages::input_mapper::utility_types::pointer::{EditorPointerState, MouseKeys, ScrollDelta};
|
||||||
pub use graphite_editor::messages::prelude::DocumentId;
|
pub use graphite_editor::messages::prelude::DocumentId;
|
||||||
pub use graphite_editor::messages::prelude::InputPreprocessorMessage as InputMessage;
|
pub use graphite_editor::messages::prelude::InputPreprocessorMessage as InputMessage;
|
||||||
pub use graphite_editor::messages::prelude::PreferencesMessageHandler as Preferences;
|
pub use graphite_editor::messages::prelude::PreferencesMessageHandler as Preferences;
|
||||||
@@ -40,6 +40,9 @@ pub enum DesktopFrontendMessage {
|
|||||||
UpdateUIScale {
|
UpdateUIScale {
|
||||||
scale: f64,
|
scale: f64,
|
||||||
},
|
},
|
||||||
|
WindowUpdateDirectInput {
|
||||||
|
enabled: bool,
|
||||||
|
},
|
||||||
UpdateOverlays(vello::Scene),
|
UpdateOverlays(vello::Scene),
|
||||||
PersistenceWriteDocument {
|
PersistenceWriteDocument {
|
||||||
id: DocumentId,
|
id: DocumentId,
|
||||||
@@ -100,11 +103,6 @@ pub enum DesktopWrapperMessage {
|
|||||||
LoadThirdPartyLicenses { text: String },
|
LoadThirdPartyLicenses { text: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FileFilter {
|
|
||||||
pub name: String,
|
|
||||||
pub extensions: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum OpenFileDialogContext {
|
pub enum OpenFileDialogContext {
|
||||||
Open,
|
Open,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ pub(crate) mod menu {
|
|||||||
use base64::engine::Engine;
|
use base64::engine::Engine;
|
||||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||||
|
|
||||||
use graphite_editor::messages::input_mapper::utility_types::input_keyboard::{Key, LabeledKeyOrMouseMotion, LabeledShortcut};
|
use graphite_editor::messages::input_mapper::utility_types::keyboard::{Key, LabeledKeyOrMouseMotion, LabeledShortcut};
|
||||||
use graphite_editor::messages::input_mapper::utility_types::misc::ActionShortcut;
|
use graphite_editor::messages::input_mapper::utility_types::misc::ActionShortcut;
|
||||||
use graphite_editor::messages::layout::LayoutMessage;
|
use graphite_editor::messages::layout::LayoutMessage;
|
||||||
use graphite_editor::messages::tool::tool_messages::tool_prelude::{Layout, LayoutGroup, LayoutTarget, MenuListEntry, Widget, WidgetId, WidgetRow};
|
use graphite_editor::messages::tool::tool_messages::tool_prelude::{Layout, LayoutGroup, LayoutTarget, MenuListEntry, Widget, WidgetId, WidgetRow};
|
||||||
|
|||||||
+4
-1
@@ -8,7 +8,7 @@ edition = "2024"
|
|||||||
readme = "../README.md"
|
readme = "../README.md"
|
||||||
homepage = "https://graphite.art"
|
homepage = "https://graphite.art"
|
||||||
repository = "https://github.com/GraphiteEditor/Graphite"
|
repository = "https://github.com/GraphiteEditor/Graphite"
|
||||||
license = "Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["gpu"]
|
default = ["gpu"]
|
||||||
@@ -44,6 +44,7 @@ tsify = { workspace = true }
|
|||||||
dyn-any = { workspace = true }
|
dyn-any = { workspace = true }
|
||||||
num_enum = { workspace = true }
|
num_enum = { workspace = true }
|
||||||
usvg = { workspace = true }
|
usvg = { workspace = true }
|
||||||
|
simplecss = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
web-sys = { workspace = true }
|
web-sys = { workspace = true }
|
||||||
vello = { workspace = true }
|
vello = { workspace = true }
|
||||||
@@ -71,3 +72,5 @@ wasm-bindgen-futures = { workspace = true }
|
|||||||
# Workspace dependencies
|
# Workspace dependencies
|
||||||
env_logger = { workspace = true }
|
env_logger = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
|
node-macro = { workspace = true }
|
||||||
|
core-types = { workspace = true }
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
const GRAPHITE_RELEASE_SERIES: &str = "Alpha 4";
|
const GRAPHITE_RELEASE_SERIES: &str = "Alpha 5";
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Instruct Cargo to rerun this build script if any of these environment variables change.
|
// Instruct Cargo to rerun this build script if any of these environment variables change.
|
||||||
|
|||||||
@@ -135,7 +135,9 @@ pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;
|
|||||||
|
|
||||||
// BRUSH TOOL
|
// BRUSH TOOL
|
||||||
pub const BRUSH_SIZE_CHANGE_KEYBOARD: f64 = 5.;
|
pub const BRUSH_SIZE_CHANGE_KEYBOARD: f64 = 5.;
|
||||||
pub const DEFAULT_BRUSH_SIZE: f64 = 20.;
|
pub const BRUSH_SIZE_DEFAULT: f64 = 40.;
|
||||||
|
pub const BRUSH_HARDNESS_DEFAULT: f64 = 0.;
|
||||||
|
pub const BRUSH_FLOW_DEFAULT: f64 = 100.;
|
||||||
|
|
||||||
// EYEDROPPER TOOL
|
// EYEDROPPER TOOL
|
||||||
pub const EYEDROPPER_PREVIEW_AREA_RESOLUTION: u32 = 11;
|
pub const EYEDROPPER_PREVIEW_AREA_RESOLUTION: u32 = 11;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ pub struct DispatcherMessageHandlers {
|
|||||||
pub(crate) portfolio_message_handler: PortfolioMessageHandler,
|
pub(crate) portfolio_message_handler: PortfolioMessageHandler,
|
||||||
preferences_message_handler: PreferencesMessageHandler,
|
preferences_message_handler: PreferencesMessageHandler,
|
||||||
pub(crate) resource_storage_message_handler: ResourceStorageMessageHandler,
|
pub(crate) resource_storage_message_handler: ResourceStorageMessageHandler,
|
||||||
tool_message_handler: ToolMessageHandler,
|
pub(crate) tool_message_handler: ToolMessageHandler,
|
||||||
viewport_message_handler: ViewportMessageHandler,
|
viewport_message_handler: ViewportMessageHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,8 +96,9 @@ impl Dispatcher {
|
|||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
pub fn with_executor(executor: crate::node_graph_executor::NodeGraphExecutor) -> Self {
|
pub fn with_executor(executor: crate::node_graph_executor::NodeGraphExecutor) -> Self {
|
||||||
let mut s = Self::default();
|
let mut s = Self::new(Arc::new(graph_craft::application_io::resource::HashMapResourceStorage::new()), None);
|
||||||
s.message_handlers.portfolio_message_handler = PortfolioMessageHandler::with_executor(executor);
|
s.message_handlers.portfolio_message_handler = PortfolioMessageHandler::with_executor(executor);
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use crate::messages::prelude::*;
|
|||||||
pub enum AppWindowMessage {
|
pub enum AppWindowMessage {
|
||||||
PointerLock,
|
PointerLock,
|
||||||
PointerLockMove { x: f64, y: f64 },
|
PointerLockMove { x: f64, y: f64 },
|
||||||
|
DirectInput { enabled: bool },
|
||||||
Restart,
|
Restart,
|
||||||
Close,
|
Close,
|
||||||
Minimize,
|
Minimize,
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
|
|||||||
AppWindowMessage::PointerLockMove { x, y } => {
|
AppWindowMessage::PointerLockMove { x, y } => {
|
||||||
responses.add(FrontendMessage::WindowPointerLockMove { position: (x, y) });
|
responses.add(FrontendMessage::WindowPointerLockMove { position: (x, y) });
|
||||||
}
|
}
|
||||||
|
AppWindowMessage::DirectInput { enabled } => {
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
responses.add(FrontendMessage::WindowUpdateDirectInput { enabled });
|
||||||
|
#[cfg(target_family = "wasm")]
|
||||||
|
let _ = enabled;
|
||||||
|
}
|
||||||
AppWindowMessage::Close => {
|
AppWindowMessage::Close => {
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
responses.add(FrontendMessage::WindowClose);
|
responses.add(FrontendMessage::WindowClose);
|
||||||
|
|||||||
@@ -8,5 +8,7 @@ pub enum EventMessage {
|
|||||||
CanvasTransformed,
|
CanvasTransformed,
|
||||||
ToolAbort,
|
ToolAbort,
|
||||||
SelectionChanged,
|
SelectionChanged,
|
||||||
|
/// The document graph's nodes or wires changed, so state derived from the selection's chains may be stale
|
||||||
|
GraphChanged,
|
||||||
WorkingColorChanged,
|
WorkingColorChanged,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ use graph_craft::application_io::resource::{DataSource, ResourceHash};
|
|||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
use graphene_std::raster::Image;
|
use graphene_std::raster::Image;
|
||||||
use graphene_std::subpath::BezierHandles;
|
use graphene_std::vector::misc::{BezierHandles, HandleId, point_to_dvec2, segment_to_handles};
|
||||||
use graphene_std::vector::misc::HandleId;
|
|
||||||
use graphene_std::vector::{PointId, SegmentId, VectorModificationType};
|
use graphene_std::vector::{PointId, SegmentId, VectorModificationType};
|
||||||
use graphite_proc_macros::{ExtractField, message_handler_data};
|
use graphite_proc_macros::{ExtractField, message_handler_data};
|
||||||
|
use kurbo::ParamCurve;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
const CLIPBOARD_PREFIX: &str = "graphite: ";
|
const CLIPBOARD_PREFIX: &str = "graphite: ";
|
||||||
@@ -182,13 +182,8 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> for Clipboard
|
|||||||
let mut resource_ids = HashSet::new();
|
let mut resource_ids = HashSet::new();
|
||||||
for item in &items {
|
for item in &items {
|
||||||
match item {
|
match item {
|
||||||
ClipboardItem::Layer(entry) => entry
|
ClipboardItem::Layer(entry) => entry.nodes.iter().for_each(|(_, template)| network_interface::collect_template_resources(template, &mut resource_ids)),
|
||||||
.nodes
|
ClipboardItem::Nodes(nodes) => nodes.iter().for_each(|(_, template)| network_interface::collect_template_resources(template, &mut resource_ids)),
|
||||||
.iter()
|
|
||||||
.for_each(|(_, template)| network_interface::collect_node_resources(&template.document_node, &mut resource_ids)),
|
|
||||||
ClipboardItem::Nodes(nodes) => nodes
|
|
||||||
.iter()
|
|
||||||
.for_each(|(_, template)| network_interface::collect_node_resources(&template.document_node, &mut resource_ids)),
|
|
||||||
ClipboardItem::Vector(_) | ClipboardItem::Resource(_) => {}
|
ClipboardItem::Vector(_) | ClipboardItem::Resource(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,9 +247,20 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> for Clipboard
|
|||||||
let mut vectors = Vec::new();
|
let mut vectors = Vec::new();
|
||||||
let mut resources = Vec::new();
|
let mut resources = Vec::new();
|
||||||
for item in items {
|
for item in items {
|
||||||
|
// Pasted templates skip document migration, so stored types normalize here
|
||||||
match item {
|
match item {
|
||||||
ClipboardItem::Layer(entry) => layers.push(entry),
|
ClipboardItem::Layer(mut entry) => {
|
||||||
ClipboardItem::Nodes(nodes) => node_groups.push(nodes),
|
for (_, template) in &mut entry.nodes {
|
||||||
|
template.normalize_stored_types();
|
||||||
|
}
|
||||||
|
layers.push(entry);
|
||||||
|
}
|
||||||
|
ClipboardItem::Nodes(mut nodes) => {
|
||||||
|
for (_, template) in &mut nodes {
|
||||||
|
template.normalize_stored_types();
|
||||||
|
}
|
||||||
|
node_groups.push(nodes);
|
||||||
|
}
|
||||||
ClipboardItem::Vector(vector) => vectors.push(vector),
|
ClipboardItem::Vector(vector) => vectors.push(vector),
|
||||||
ClipboardItem::Resource(resource) => resources.push(resource),
|
ClipboardItem::Resource(resource) => resources.push(resource),
|
||||||
}
|
}
|
||||||
@@ -419,7 +425,7 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> for Clipboard
|
|||||||
|
|
||||||
// Create new segment ids and add the segments into the existing Vector path
|
// Create new segment ids and add the segments into the existing Vector path
|
||||||
let mut segments_map = HashMap::new();
|
let mut segments_map = HashMap::new();
|
||||||
for (segment_id, bezier, start, end) in new_vector.segment_bezier_iter() {
|
for (segment_id, segment, start, end) in new_vector.segment_iter() {
|
||||||
let (Some(&start_point), Some(&end_point)) = (points_map.get(&start), points_map.get(&end)) else {
|
let (Some(&start_point), Some(&end_point)) = (points_map.get(&start), points_map.get(&end)) else {
|
||||||
warn!("Skipping pasted vector segment with an unknown endpoint");
|
warn!("Skipping pasted vector segment with an unknown endpoint");
|
||||||
continue;
|
continue;
|
||||||
@@ -428,10 +434,12 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> for Clipboard
|
|||||||
let new_segment_id = SegmentId::generate();
|
let new_segment_id = SegmentId::generate();
|
||||||
segments_map.insert(segment_id, new_segment_id);
|
segments_map.insert(segment_id, new_segment_id);
|
||||||
|
|
||||||
let handles = match bezier.handles {
|
let segment_start = point_to_dvec2(segment.start());
|
||||||
|
let segment_end = point_to_dvec2(segment.end());
|
||||||
|
let handles = match segment_to_handles(&segment) {
|
||||||
BezierHandles::Linear => [None, None],
|
BezierHandles::Linear => [None, None],
|
||||||
BezierHandles::Quadratic { handle } => [Some(handle - bezier.start), None],
|
BezierHandles::Quadratic { handle } => [Some(handle - segment_start), None],
|
||||||
BezierHandles::Cubic { handle_start, handle_end } => [Some(handle_start - bezier.start), Some(handle_end - bezier.end)],
|
BezierHandles::Cubic { handle_start, handle_end } => [Some(handle_start - segment_start), Some(handle_end - segment_end)],
|
||||||
};
|
};
|
||||||
|
|
||||||
let points = [start_point, end_point];
|
let points = [start_point, end_point];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::messages::layout::utility_types::widgets::input_widgets::{SpectrumInputUpdate, VisualColorPickersInputUpdate};
|
use crate::messages::layout::utility_types::widgets::input_widgets::{SpectrumInputUpdate, VisualColorPickersInputUpdate};
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use graphene_std::vector::style::FillChoice;
|
use graphene_std::vector::style::{FillChoice, GradientHueDirection, GradientInterpolation, GradientSpace, GradientSpread};
|
||||||
|
|
||||||
/// Identifies which RGB channel a numeric input change targets.
|
/// Identifies which RGB channel a numeric input change targets.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
@@ -47,6 +47,16 @@ pub enum ColorPickerMessage {
|
|||||||
|
|
||||||
/// `SpectrumInput` change: marker move/insert/delete, midpoint move/reset, or active marker selection changed.
|
/// `SpectrumInput` change: marker move/insert/delete, midpoint move/reset, or active marker selection changed.
|
||||||
GradientUpdate { update: SpectrumInputUpdate },
|
GradientUpdate { update: SpectrumInputUpdate },
|
||||||
|
/// Gradient spread choice from the gradient "Ends" selection.
|
||||||
|
SetGradientSpread { gradient_spread: GradientSpread },
|
||||||
|
/// Gradient cyclic choice: whether the stops wrap as a cycle, from the "Cyclic" checkbox.
|
||||||
|
SetGradientCyclic { gradient_cyclic: bool },
|
||||||
|
/// Gradient space choice: the color space the stops interpolate in, from the "Space" dropdown.
|
||||||
|
SetGradientSpace { gradient_space: GradientSpace },
|
||||||
|
/// Gradient hue direction choice: which way around the hue wheel the stops interpolate in a polar space, from the "Arc" dropdown.
|
||||||
|
SetGradientHueDirection { gradient_hue_direction: GradientHueDirection },
|
||||||
|
/// Gradient interpolation choice: the path the stops interpolate along, from the "Intrp." dropdown.
|
||||||
|
SetGradientInterpolation { gradient_interpolation: GradientInterpolation },
|
||||||
|
|
||||||
/// Tell the frontend to start an undo transaction (forwarded as a `FrontendMessage` it bridges out to the picker's parent).
|
/// Tell the frontend to start an undo transaction (forwarded as a `FrontendMessage` it bridges out to the picker's parent).
|
||||||
StartTransaction,
|
StartTransaction,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::messages::prelude::*;
|
|||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
use graphene_std::color::SRGBA8;
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::core_types::misc::parse_css_color;
|
use graphene_std::core_types::misc::parse_css_color;
|
||||||
use graphene_std::vector::style::{FillChoice, FillChoiceUI, GradientStops, GradientStopsUI};
|
use graphene_std::vector::style::{FillChoice, Gradient, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, GradientStops};
|
||||||
|
|
||||||
/// Bounds for a midpoint position (relative to the interval between two adjacent gradient stops).
|
/// Bounds for a midpoint position (relative to the interval between two adjacent gradient stops).
|
||||||
const MIN_MIDPOINT: f64 = 0.01;
|
const MIN_MIDPOINT: f64 = 0.01;
|
||||||
@@ -28,7 +28,12 @@ pub struct ColorPickerMessageHandler {
|
|||||||
old_is_none: bool,
|
old_is_none: bool,
|
||||||
|
|
||||||
// When set, the picker is editing a gradient: the visual pickers and inputs target the active stop's color.
|
// When set, the picker is editing a gradient: the visual pickers and inputs target the active stop's color.
|
||||||
gradient: Option<GradientStops>,
|
gradient: Option<Gradient>,
|
||||||
|
gradient_spread: GradientSpread,
|
||||||
|
gradient_space: GradientSpace,
|
||||||
|
gradient_cyclic: bool,
|
||||||
|
gradient_hue_direction: GradientHueDirection,
|
||||||
|
gradient_interpolation: GradientInterpolation,
|
||||||
active_marker_index: Option<u32>,
|
active_marker_index: Option<u32>,
|
||||||
active_marker_is_midpoint: bool,
|
active_marker_is_midpoint: bool,
|
||||||
|
|
||||||
@@ -50,6 +55,11 @@ impl Default for ColorPickerMessageHandler {
|
|||||||
old_alpha: 1.,
|
old_alpha: 1.,
|
||||||
old_is_none: true,
|
old_is_none: true,
|
||||||
gradient: None,
|
gradient: None,
|
||||||
|
gradient_spread: GradientSpread::default(),
|
||||||
|
gradient_space: GradientSpace::default(),
|
||||||
|
gradient_cyclic: false,
|
||||||
|
gradient_hue_direction: GradientHueDirection::default(),
|
||||||
|
gradient_interpolation: GradientInterpolation::default(),
|
||||||
active_marker_index: None,
|
active_marker_index: None,
|
||||||
active_marker_is_midpoint: false,
|
active_marker_is_midpoint: false,
|
||||||
allow_none: true,
|
allow_none: true,
|
||||||
@@ -70,20 +80,36 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
|
|||||||
FillChoice::None => {
|
FillChoice::None => {
|
||||||
self.set_new_hsva(0., 0., 0., 1., true);
|
self.set_new_hsva(0., 0., 0., 1., true);
|
||||||
self.gradient = None;
|
self.gradient = None;
|
||||||
|
self.gradient_spread = GradientSpread::default();
|
||||||
|
self.gradient_space = GradientSpace::default();
|
||||||
|
self.gradient_cyclic = false;
|
||||||
|
self.gradient_hue_direction = GradientHueDirection::default();
|
||||||
|
self.gradient_interpolation = GradientInterpolation::default();
|
||||||
self.active_marker_index = None;
|
self.active_marker_index = None;
|
||||||
self.active_marker_is_midpoint = false;
|
self.active_marker_is_midpoint = false;
|
||||||
}
|
}
|
||||||
FillChoice::Solid(color) => {
|
FillChoice::Solid(color) => {
|
||||||
self.gradient = None;
|
self.gradient = None;
|
||||||
|
self.gradient_spread = GradientSpread::default();
|
||||||
|
self.gradient_space = GradientSpace::default();
|
||||||
|
self.gradient_cyclic = false;
|
||||||
|
self.gradient_hue_direction = GradientHueDirection::default();
|
||||||
|
self.gradient_interpolation = GradientInterpolation::default();
|
||||||
self.active_marker_index = None;
|
self.active_marker_index = None;
|
||||||
self.active_marker_is_midpoint = false;
|
self.active_marker_is_midpoint = false;
|
||||||
self.adopt_color(color);
|
self.adopt_color(color);
|
||||||
}
|
}
|
||||||
FillChoice::Gradient(stops) => {
|
FillChoice::Gradient(ramp) => {
|
||||||
self.active_marker_index = Some(0);
|
self.active_marker_index = Some(0);
|
||||||
self.active_marker_is_midpoint = false;
|
self.active_marker_is_midpoint = false;
|
||||||
let first_color = stops.color.first().copied().unwrap_or(Color::BLACK);
|
self.gradient_spread = ramp.gradient_spread;
|
||||||
self.gradient = Some(stops);
|
self.gradient_space = ramp.gradient_space;
|
||||||
|
self.gradient_cyclic = ramp.gradient_cyclic;
|
||||||
|
self.gradient_hue_direction = ramp.gradient_hue_direction;
|
||||||
|
self.gradient_interpolation = ramp.gradient_interpolation;
|
||||||
|
let gradient = Gradient::from(ramp);
|
||||||
|
let first_color = gradient.color(0).unwrap_or(Color::BLACK);
|
||||||
|
self.gradient = Some(gradient);
|
||||||
self.adopt_color(first_color);
|
self.adopt_color(first_color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,7 +180,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
|
|||||||
match preset {
|
match preset {
|
||||||
FillChoice::None => {
|
FillChoice::None => {
|
||||||
self.set_new_hsva(0., 0., 0., 1., true);
|
self.set_new_hsva(0., 0., 0., 1., true);
|
||||||
responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoiceUI::None });
|
responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoice::<SRGBA8>::None });
|
||||||
}
|
}
|
||||||
FillChoice::Solid(color) => {
|
FillChoice::Solid(color) => {
|
||||||
self.adopt_color(color);
|
self.adopt_color(color);
|
||||||
@@ -179,13 +205,62 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
|
|||||||
self.set_old_hsva(temp.0, temp.1, temp.2, temp.3, temp.4);
|
self.set_old_hsva(temp.0, temp.1, temp.2, temp.3, temp.4);
|
||||||
|
|
||||||
if self.is_none {
|
if self.is_none {
|
||||||
responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoiceUI::None });
|
responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoice::<SRGBA8>::None });
|
||||||
} else {
|
} else {
|
||||||
self.emit_color(responses);
|
self.emit_color(responses);
|
||||||
}
|
}
|
||||||
self.send_layouts(responses);
|
self.send_layouts(responses);
|
||||||
}
|
}
|
||||||
ColorPickerMessage::GradientUpdate { update } => self.apply_gradient_update(update, responses),
|
ColorPickerMessage::GradientUpdate { update } => self.apply_gradient_update(update, responses),
|
||||||
|
ColorPickerMessage::SetGradientSpread { gradient_spread } => {
|
||||||
|
let Some(gradient) = &self.gradient else { return };
|
||||||
|
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
|
||||||
|
self.gradient_spread = gradient_spread;
|
||||||
|
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||||
|
value: FillChoice::Gradient(GradientRamp::from(gradient).with_settings(self.gradient_settings())),
|
||||||
|
});
|
||||||
|
self.send_layouts(responses);
|
||||||
|
}
|
||||||
|
ColorPickerMessage::SetGradientCyclic { gradient_cyclic } => {
|
||||||
|
let Some(gradient) = &mut self.gradient else { return };
|
||||||
|
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
|
||||||
|
|
||||||
|
let previous_cyclic = std::mem::replace(&mut self.gradient_cyclic, gradient_cyclic);
|
||||||
|
gradient.hold_positions_across_cyclic_change(previous_cyclic, gradient_cyclic);
|
||||||
|
|
||||||
|
let ramp = GradientRamp::from(&*gradient);
|
||||||
|
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||||
|
value: FillChoice::Gradient(ramp.with_settings(self.gradient_settings())),
|
||||||
|
});
|
||||||
|
self.send_layouts(responses);
|
||||||
|
}
|
||||||
|
ColorPickerMessage::SetGradientSpace { gradient_space } => {
|
||||||
|
let Some(gradient) = &self.gradient else { return };
|
||||||
|
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
|
||||||
|
self.gradient_space = gradient_space;
|
||||||
|
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||||
|
value: FillChoice::Gradient(GradientRamp::from(gradient).with_settings(self.gradient_settings())),
|
||||||
|
});
|
||||||
|
self.send_layouts(responses);
|
||||||
|
}
|
||||||
|
ColorPickerMessage::SetGradientHueDirection { gradient_hue_direction } => {
|
||||||
|
let Some(gradient) = &self.gradient else { return };
|
||||||
|
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
|
||||||
|
self.gradient_hue_direction = gradient_hue_direction;
|
||||||
|
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||||
|
value: FillChoice::Gradient(GradientRamp::from(gradient).with_settings(self.gradient_settings())),
|
||||||
|
});
|
||||||
|
self.send_layouts(responses);
|
||||||
|
}
|
||||||
|
ColorPickerMessage::SetGradientInterpolation { gradient_interpolation } => {
|
||||||
|
let Some(gradient) = &self.gradient else { return };
|
||||||
|
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
|
||||||
|
self.gradient_interpolation = gradient_interpolation;
|
||||||
|
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||||
|
value: FillChoice::Gradient(GradientRamp::from(gradient).with_settings(self.gradient_settings())),
|
||||||
|
});
|
||||||
|
self.send_layouts(responses);
|
||||||
|
}
|
||||||
ColorPickerMessage::StartTransaction => {
|
ColorPickerMessage::StartTransaction => {
|
||||||
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
|
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
|
||||||
}
|
}
|
||||||
@@ -234,6 +309,17 @@ impl ColorPickerMessageHandler {
|
|||||||
self.old_is_none = is_none;
|
self.old_is_none = is_none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The whole-ramp settings the picker is currently editing with, bundled for sampling and for emitting ramps.
|
||||||
|
fn gradient_settings(&self) -> GradientSettings {
|
||||||
|
GradientSettings {
|
||||||
|
spread: self.gradient_spread,
|
||||||
|
cyclic: self.gradient_cyclic,
|
||||||
|
space: self.gradient_space,
|
||||||
|
hue_direction: self.gradient_hue_direction,
|
||||||
|
interpolation: self.gradient_interpolation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn snapshot_old(&mut self) {
|
fn snapshot_old(&mut self) {
|
||||||
self.old_hue = self.hue;
|
self.old_hue = self.hue;
|
||||||
self.old_saturation = self.saturation;
|
self.old_saturation = self.saturation;
|
||||||
@@ -266,18 +352,16 @@ impl ColorPickerMessageHandler {
|
|||||||
|
|
||||||
if let Some(gradient) = &mut self.gradient
|
if let Some(gradient) = &mut self.gradient
|
||||||
&& let Some(active_index) = self.active_marker_index
|
&& let Some(active_index) = self.active_marker_index
|
||||||
&& let Some(stop_color) = gradient.color.get_mut(active_index as usize)
|
&& (active_index as usize) < gradient.len()
|
||||||
{
|
{
|
||||||
*stop_color = color;
|
gradient.set_color(active_index as usize, color);
|
||||||
let stops = gradient.clone();
|
let ramp = GradientRamp::from(&*gradient);
|
||||||
let fill_choice = FillChoice::Gradient(stops);
|
|
||||||
responses.add(FrontendMessage::ColorPickerColorChanged {
|
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||||
value: FillChoiceUI::from(&fill_choice),
|
value: FillChoice::Gradient(ramp.with_settings(self.gradient_settings())),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let fill_choice = FillChoice::Solid(color);
|
|
||||||
responses.add(FrontendMessage::ColorPickerColorChanged {
|
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||||
value: FillChoiceUI::from(&fill_choice),
|
value: FillChoice::Solid(SRGBA8::from(color)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,7 +389,7 @@ impl ColorPickerMessageHandler {
|
|||||||
self.active_marker_is_midpoint = active_marker_is_midpoint;
|
self.active_marker_is_midpoint = active_marker_is_midpoint;
|
||||||
if let Some(index) = active_marker_index
|
if let Some(index) = active_marker_index
|
||||||
&& let Some(gradient) = &self.gradient
|
&& let Some(gradient) = &self.gradient
|
||||||
&& let Some(color) = gradient.color.get(index as usize).copied()
|
&& let Some(color) = gradient.color(index as usize)
|
||||||
{
|
{
|
||||||
self.adopt_color(color);
|
self.adopt_color(color);
|
||||||
self.snapshot_old();
|
self.snapshot_old();
|
||||||
@@ -318,30 +402,31 @@ impl ColorPickerMessageHandler {
|
|||||||
|
|
||||||
match update {
|
match update {
|
||||||
SpectrumInputUpdate::MoveMarker { index, position } => {
|
SpectrumInputUpdate::MoveMarker { index, position } => {
|
||||||
let new_index = gradient.move_stop(index as usize, position);
|
let new_index = gradient.move_stop(index as usize, position, self.gradient_cyclic);
|
||||||
if Some(index) == self.active_marker_index {
|
if Some(index) == self.active_marker_index {
|
||||||
self.active_marker_index = Some(new_index as u32);
|
self.active_marker_index = Some(new_index as u32);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SpectrumInputUpdate::MoveMidpoint { index, position } => {
|
SpectrumInputUpdate::MoveMidpoint { index, position } => {
|
||||||
if let Some(midpoint) = gradient.midpoint.get_mut(index as usize) {
|
if (index as usize) >= gradient.len() {
|
||||||
*midpoint = position.clamp(MIN_MIDPOINT, MAX_MIDPOINT);
|
|
||||||
} else {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
gradient.set_midpoint(index as usize, position.clamp(MIN_MIDPOINT, MAX_MIDPOINT));
|
||||||
}
|
}
|
||||||
SpectrumInputUpdate::InsertMarker { position } => {
|
SpectrumInputUpdate::InsertMarker { position } => {
|
||||||
let new_index = gradient.insert_stop(position);
|
let new_index = gradient.insert_stop(position, self.gradient_settings());
|
||||||
self.active_marker_index = Some(new_index as u32);
|
self.active_marker_index = Some(new_index as u32);
|
||||||
self.active_marker_is_midpoint = false;
|
self.active_marker_is_midpoint = false;
|
||||||
if let Some(color) = gradient.color.get(new_index).copied() {
|
if let Some(color) = gradient.color(new_index) {
|
||||||
self.adopt_color(color);
|
self.adopt_color(color);
|
||||||
self.snapshot_old();
|
self.snapshot_old();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SpectrumInputUpdate::InsertDuplicate { index, position } => {
|
SpectrumInputUpdate::InsertDuplicate { index, position } => {
|
||||||
let source = index as usize;
|
let source = index as usize;
|
||||||
let Some(insert_index) = gradient.duplicate_stop(source, position) else { return };
|
let Some(insert_index) = gradient.duplicate_stop(source, position, self.gradient_cyclic) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
// The dragged stop (the duplication source) stays active. Its index shifts up if the frozen copy landed at or before it.
|
// The dragged stop (the duplication source) stays active. Its index shifts up if the frozen copy landed at or before it.
|
||||||
let dragged_index = if insert_index <= source { source + 1 } else { source };
|
let dragged_index = if insert_index <= source { source + 1 } else { source };
|
||||||
self.active_marker_index = Some(dragged_index as u32);
|
self.active_marker_index = Some(dragged_index as u32);
|
||||||
@@ -349,7 +434,7 @@ impl ColorPickerMessageHandler {
|
|||||||
}
|
}
|
||||||
SpectrumInputUpdate::RemoveDuplicate { index } => {
|
SpectrumInputUpdate::RemoveDuplicate { index } => {
|
||||||
let anchor = index as usize;
|
let anchor = index as usize;
|
||||||
if anchor >= gradient.position.len() || gradient.position.len() <= 2 {
|
if anchor >= gradient.len() || gradient.len() <= 2 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Never remove the active (dragged) stop itself, this should only ever target the frozen copy.
|
// Never remove the active (dragged) stop itself, this should only ever target the frozen copy.
|
||||||
@@ -366,14 +451,14 @@ impl ColorPickerMessageHandler {
|
|||||||
}
|
}
|
||||||
SpectrumInputUpdate::DeleteMarker { index } => {
|
SpectrumInputUpdate::DeleteMarker { index } => {
|
||||||
// Enforce minimum stop count. The gradient editor needs at least 2 stops to remain meaningful.
|
// Enforce minimum stop count. The gradient editor needs at least 2 stops to remain meaningful.
|
||||||
if gradient.position.len() <= 2 || (index as usize) >= gradient.position.len() {
|
if gradient.len() <= 2 || (index as usize) >= gradient.len() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
gradient.remove(index as usize);
|
gradient.remove(index as usize);
|
||||||
let new_active = (index as usize).min(gradient.position.len() - 1);
|
let new_active = (index as usize).min(gradient.len() - 1);
|
||||||
self.active_marker_index = Some(new_active as u32);
|
self.active_marker_index = Some(new_active as u32);
|
||||||
self.active_marker_is_midpoint = false;
|
self.active_marker_is_midpoint = false;
|
||||||
if let Some(color) = gradient.color.get(new_active).copied() {
|
if let Some(color) = gradient.color(new_active) {
|
||||||
self.adopt_color(color);
|
self.adopt_color(color);
|
||||||
self.snapshot_old();
|
self.snapshot_old();
|
||||||
}
|
}
|
||||||
@@ -383,16 +468,17 @@ impl ColorPickerMessageHandler {
|
|||||||
}
|
}
|
||||||
SpectrumInputUpdate::ResetMarker { index } => {
|
SpectrumInputUpdate::ResetMarker { index } => {
|
||||||
let i = index as usize;
|
let i = index as usize;
|
||||||
let count = gradient.position.len();
|
let count = gradient.len();
|
||||||
if i >= count {
|
if i >= count {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Each stop's "natural" position is its evenly-spaced fraction along 0..1, e.g., for 5 stops: 0, 0.25, 0.5, 0.75, 1. Falls back to the midpoint between neighbors when the natural position would push the stop past another.
|
// Each stop's "natural" position is its evenly-spaced fraction along 0..1, e.g., for 5 stops: 0, 0.25, 0.5, 0.75, 1 (or fifths when cyclic, leaving the wrapped interval its share). Falls back to the midpoint between neighbors when the natural position would push the stop past another.
|
||||||
let left = if i == 0 { 0. } else { gradient.position[i - 1] };
|
let left = if i == 0 { 0. } else { gradient.position(i - 1, self.gradient_cyclic) };
|
||||||
let right = gradient.position.get(i + 1).copied().unwrap_or(1.);
|
let right = if i + 1 < count { gradient.position(i + 1, self.gradient_cyclic) } else { 1. };
|
||||||
let natural = if count <= 1 { 0. } else { i as f64 / (count - 1) as f64 };
|
let denominator = if self.gradient_cyclic { count } else { count - 1 };
|
||||||
|
let natural = if count <= 1 { 0. } else { i as f64 / denominator as f64 };
|
||||||
let new_position = if (left..=right).contains(&natural) { natural } else { (left + right) / 2. };
|
let new_position = if (left..=right).contains(&natural) { natural } else { (left + right) / 2. };
|
||||||
let new_index = gradient.move_stop(i, new_position);
|
let new_index = gradient.move_stop(i, new_position, self.gradient_cyclic);
|
||||||
if Some(index) == self.active_marker_index {
|
if Some(index) == self.active_marker_index {
|
||||||
self.active_marker_index = Some(new_index as u32);
|
self.active_marker_index = Some(new_index as u32);
|
||||||
}
|
}
|
||||||
@@ -400,11 +486,10 @@ impl ColorPickerMessageHandler {
|
|||||||
SpectrumInputUpdate::ActiveMarker { .. } => unreachable!("handled above"),
|
SpectrumInputUpdate::ActiveMarker { .. } => unreachable!("handled above"),
|
||||||
}
|
}
|
||||||
|
|
||||||
self.gradient = Some(gradient.clone());
|
|
||||||
let fill_choice = FillChoice::Gradient(gradient);
|
|
||||||
responses.add(FrontendMessage::ColorPickerColorChanged {
|
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||||
value: FillChoiceUI::from(&fill_choice),
|
value: FillChoice::Gradient(GradientRamp::from(&gradient).with_settings(self.gradient_settings())),
|
||||||
});
|
});
|
||||||
|
self.gradient = Some(gradient);
|
||||||
self.send_layouts(responses);
|
self.send_layouts(responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,9 +513,15 @@ impl ColorPickerMessageHandler {
|
|||||||
// Gradient editor (only present when the picker is in gradient mode)
|
// Gradient editor (only present when the picker is in gradient mode)
|
||||||
if let Some(gradient) = &self.gradient {
|
if let Some(gradient) = &self.gradient {
|
||||||
// For gradient editing, the markers' handle colors mirror their gradient stop colors
|
// For gradient editing, the markers' handle colors mirror their gradient stop colors
|
||||||
let markers = gradient.iter().map(|stop| SpectrumMarker::new(stop.position, stop.midpoint, stop.color)).collect();
|
let markers = (0..gradient.len())
|
||||||
|
.filter_map(|i| Some(SpectrumMarker::new(gradient.position(i, self.gradient_cyclic), gradient.midpoint(i), gradient.color(i)?)))
|
||||||
|
.collect();
|
||||||
let mut row_widgets = vec![
|
let mut row_widgets = vec![
|
||||||
SpectrumInput::new(GradientStopsUI::from(gradient))
|
SpectrumInput::new(GradientStops::from(gradient))
|
||||||
|
.track_space(self.gradient_space)
|
||||||
|
.track_cyclic(self.gradient_cyclic)
|
||||||
|
.track_hue_direction(self.gradient_hue_direction)
|
||||||
|
.track_interpolation(self.gradient_interpolation)
|
||||||
.markers(markers)
|
.markers(markers)
|
||||||
.active_marker_index(self.active_marker_index)
|
.active_marker_index(self.active_marker_index)
|
||||||
.active_marker_is_midpoint(self.active_marker_is_midpoint)
|
.active_marker_is_midpoint(self.active_marker_is_midpoint)
|
||||||
@@ -445,10 +536,10 @@ impl ColorPickerMessageHandler {
|
|||||||
|
|
||||||
if let Some(active) = self.active_marker_index {
|
if let Some(active) = self.active_marker_index {
|
||||||
let active_index = active as usize;
|
let active_index = active as usize;
|
||||||
let position_value = if self.active_marker_is_midpoint {
|
let position_value = match (self.active_marker_is_midpoint, active_index < gradient.len()) {
|
||||||
gradient.midpoint.get(active_index).copied().unwrap_or(0.)
|
(_, false) => 0.,
|
||||||
} else {
|
(true, true) => gradient.midpoint(active_index),
|
||||||
gradient.position.get(active_index).copied().unwrap_or(0.)
|
(false, true) => gradient.position(active_index, self.gradient_cyclic),
|
||||||
};
|
};
|
||||||
let is_midpoint = self.active_marker_is_midpoint;
|
let is_midpoint = self.active_marker_is_midpoint;
|
||||||
let captured_index = active;
|
let captured_index = active;
|
||||||
@@ -524,7 +615,11 @@ impl ColorPickerMessageHandler {
|
|||||||
|
|
||||||
// Hex
|
// Hex
|
||||||
groups.push(LayoutGroup::row(vec![
|
groups.push(LayoutGroup::row(vec![
|
||||||
TextLabel::new("Hex").tooltip_label("Hex Color Code").tooltip_description(HEX_DESCRIPTION).widget_instance(),
|
TextLabel::new("Hex")
|
||||||
|
.disabled(self.disabled)
|
||||||
|
.tooltip_label("Hex Color Code")
|
||||||
|
.tooltip_description(HEX_DESCRIPTION)
|
||||||
|
.widget_instance(),
|
||||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||||
TextInput::new(hex_value)
|
TextInput::new(hex_value)
|
||||||
.centered(true)
|
.centered(true)
|
||||||
@@ -537,7 +632,11 @@ impl ColorPickerMessageHandler {
|
|||||||
|
|
||||||
// RGB
|
// RGB
|
||||||
groups.push(LayoutGroup::row(vec![
|
groups.push(LayoutGroup::row(vec![
|
||||||
TextLabel::new("RGB").tooltip_label("Red/Green/Blue").tooltip_description("Integers 0–255.").widget_instance(),
|
TextLabel::new("RGB")
|
||||||
|
.disabled(self.disabled)
|
||||||
|
.tooltip_label("Red/Green/Blue")
|
||||||
|
.tooltip_description("Integers 0–255.")
|
||||||
|
.widget_instance(),
|
||||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||||
rgb_input(RgbChannel::Red, rgb_255.map(|(r, _, _)| r), "Red Channel", self.disabled),
|
rgb_input(RgbChannel::Red, rgb_255.map(|(r, _, _)| r), "Red Channel", self.disabled),
|
||||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||||
@@ -549,8 +648,9 @@ impl ColorPickerMessageHandler {
|
|||||||
// HSV
|
// HSV
|
||||||
groups.push(LayoutGroup::row(vec![
|
groups.push(LayoutGroup::row(vec![
|
||||||
TextLabel::new("HSV")
|
TextLabel::new("HSV")
|
||||||
|
.disabled(self.disabled)
|
||||||
.tooltip_label("Hue/Saturation/Value")
|
.tooltip_label("Hue/Saturation/Value")
|
||||||
.tooltip_description("Also known as Hue/Saturation/Brightness (HSB). Not to be confused with Hue/Saturation/Lightness (HSL), a different color model.")
|
.tooltip_description("Also known as Hue/Saturation/Brightness (HSB), but distinct from Hue/Saturation/Lightness (HSL), a different color model.")
|
||||||
.widget_instance(),
|
.widget_instance(),
|
||||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||||
hsv_input(
|
hsv_input(
|
||||||
@@ -586,7 +686,11 @@ impl ColorPickerMessageHandler {
|
|||||||
|
|
||||||
// Alpha
|
// Alpha
|
||||||
groups.push(LayoutGroup::row(vec![
|
groups.push(LayoutGroup::row(vec![
|
||||||
TextLabel::new("Alpha").tooltip_label("Alpha").tooltip_description(ALPHA_DESCRIPTION).widget_instance(),
|
TextLabel::new("Alpha")
|
||||||
|
.disabled(self.disabled)
|
||||||
|
.tooltip_label("Alpha")
|
||||||
|
.tooltip_description(ALPHA_DESCRIPTION)
|
||||||
|
.widget_instance(),
|
||||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||||
NumberInput::new(if self.is_none { None } else { Some(self.alpha * 100.) })
|
NumberInput::new(if self.is_none { None } else { Some(self.alpha * 100.) })
|
||||||
.disabled(self.disabled)
|
.disabled(self.disabled)
|
||||||
@@ -602,6 +706,98 @@ impl ColorPickerMessageHandler {
|
|||||||
.widget_instance(),
|
.widget_instance(),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
// Gradient spread, trailed by the cyclic wrap toggle (only present when the picker is in gradient mode)
|
||||||
|
if self.gradient.is_some() {
|
||||||
|
let entries = RadioEntryData::list_from_choice_type(|gradient_spread| ColorPickerMessage::SetGradientSpread { gradient_spread }.into());
|
||||||
|
|
||||||
|
groups.push(LayoutGroup::row(vec![
|
||||||
|
TextLabel::new("Ends")
|
||||||
|
.disabled(self.disabled)
|
||||||
|
.tooltip_label("Gradient Spread / Cyclic")
|
||||||
|
.tooltip_description(ENDS_DESCRIPTION)
|
||||||
|
.widget_instance(),
|
||||||
|
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||||
|
RadioInput::new(entries)
|
||||||
|
.narrow(true)
|
||||||
|
.selected_index(Some(self.gradient_spread as u32))
|
||||||
|
.disabled(self.disabled)
|
||||||
|
.widget_instance(),
|
||||||
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
|
CheckboxInput::new(self.gradient_cyclic)
|
||||||
|
.icon("Link")
|
||||||
|
.disabled(self.disabled)
|
||||||
|
.tooltip_label("Cyclic")
|
||||||
|
.tooltip_description(CYCLIC_DESCRIPTION)
|
||||||
|
.on_update(|checkbox_input: &CheckboxInput| {
|
||||||
|
ColorPickerMessage::SetGradientCyclic {
|
||||||
|
gradient_cyclic: checkbox_input.checked,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.widget_instance(),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gradient interpolation (only present when the picker is in gradient mode)
|
||||||
|
if self.gradient.is_some() {
|
||||||
|
let entries = MenuListEntry::sections_from_choice_type(|gradient_interpolation| ColorPickerMessage::SetGradientInterpolation { gradient_interpolation }.into());
|
||||||
|
|
||||||
|
groups.push(LayoutGroup::row(vec![
|
||||||
|
TextLabel::new("Intrp.")
|
||||||
|
.disabled(self.disabled)
|
||||||
|
.tooltip_label("Gradient Interpolation")
|
||||||
|
.tooltip_description(INTERPOLATION_DESCRIPTION)
|
||||||
|
.widget_instance(),
|
||||||
|
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||||
|
DropdownInput::new(entries)
|
||||||
|
.selected_index(Some(self.gradient_interpolation as u32))
|
||||||
|
.disabled(self.disabled)
|
||||||
|
.tooltip_label("Gradient Interpolation")
|
||||||
|
.tooltip_description(INTERPOLATION_DESCRIPTION)
|
||||||
|
.widget_instance(),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gradient color space (only present when the picker is in gradient mode)
|
||||||
|
if self.gradient.is_some() {
|
||||||
|
let entries = MenuListEntry::sections_from_choice_type(|gradient_space| ColorPickerMessage::SetGradientSpace { gradient_space }.into());
|
||||||
|
|
||||||
|
groups.push(LayoutGroup::row(vec![
|
||||||
|
TextLabel::new("Space")
|
||||||
|
.disabled(self.disabled)
|
||||||
|
.tooltip_label("Gradient Space")
|
||||||
|
.tooltip_description(SPACE_DESCRIPTION)
|
||||||
|
.widget_instance(),
|
||||||
|
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||||
|
DropdownInput::new(entries)
|
||||||
|
.selected_index(Some(self.gradient_space as u32))
|
||||||
|
.disabled(self.disabled)
|
||||||
|
.tooltip_label("Gradient Space")
|
||||||
|
.tooltip_description(SPACE_DESCRIPTION)
|
||||||
|
.widget_instance(),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gradient hue direction (only present when the chosen space is polar)
|
||||||
|
if self.gradient.is_some() && self.gradient_space.is_polar() {
|
||||||
|
let entries = MenuListEntry::sections_from_choice_type(|gradient_hue_direction| ColorPickerMessage::SetGradientHueDirection { gradient_hue_direction }.into());
|
||||||
|
|
||||||
|
groups.push(LayoutGroup::row(vec![
|
||||||
|
TextLabel::new("Arc")
|
||||||
|
.disabled(self.disabled)
|
||||||
|
.tooltip_label("Gradient Hue Direction")
|
||||||
|
.tooltip_description(HUE_DIRECTION_DESCRIPTION)
|
||||||
|
.widget_instance(),
|
||||||
|
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||||
|
DropdownInput::new(entries)
|
||||||
|
.selected_index(Some(self.gradient_hue_direction as u32))
|
||||||
|
.disabled(self.disabled)
|
||||||
|
.tooltip_label("Gradient Hue Direction")
|
||||||
|
.tooltip_description(HUE_DIRECTION_DESCRIPTION)
|
||||||
|
.widget_instance(),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
// Color presets (None / Black / White / pure colors / eyedropper)
|
// Color presets (None / Black / White / pure colors / eyedropper)
|
||||||
groups.push(LayoutGroup::row(vec![
|
groups.push(LayoutGroup::row(vec![
|
||||||
ColorPresetsInput::default()
|
ColorPresetsInput::default()
|
||||||
@@ -655,6 +851,11 @@ const HUE_DESCRIPTION: &str = "The shade along the spectrum of the rainbow.";
|
|||||||
const SATURATION_DESCRIPTION: &str = "The vividness from grayscale to full color.";
|
const SATURATION_DESCRIPTION: &str = "The vividness from grayscale to full color.";
|
||||||
const VALUE_DESCRIPTION: &str = "The brightness from black to full color.";
|
const VALUE_DESCRIPTION: &str = "The brightness from black to full color.";
|
||||||
const ALPHA_DESCRIPTION: &str = "The level of translucency, from transparent (0%) to opaque (100%).";
|
const ALPHA_DESCRIPTION: &str = "The level of translucency, from transparent (0%) to opaque (100%).";
|
||||||
|
const ENDS_DESCRIPTION: &str = "The method for how the gradient continues beyond its ends, and whether its stops cycle back around from last to first.";
|
||||||
|
const CYCLIC_DESCRIPTION: &str = "Treats the stops as a cycle, interpolating from the last stop back around to the first.";
|
||||||
|
const SPACE_DESCRIPTION: &str = "The color space where stops interpolate toward their neighbors.";
|
||||||
|
const HUE_DIRECTION_DESCRIPTION: &str = "Which way around the hue wheel the stops interpolate.";
|
||||||
|
const INTERPOLATION_DESCRIPTION: &str = "The path the stops interpolate along, deciding whether the gradient jumps, turns corners, or flows smoothly through them.";
|
||||||
|
|
||||||
/// The popover's background color as sRGB gamma-encoded channels (the `--color-2-mildblack` design token, `#222`).
|
/// The popover's background color as sRGB gamma-encoded channels (the `--color-2-mildblack` design token, `#222`).
|
||||||
/// Used by the comparison swatch's outline computation to brighten the inset border for colors close to this background.
|
/// Used by the comparison swatch's outline computation to brighten the inset border for colors close to this background.
|
||||||
|
|||||||
@@ -275,11 +275,7 @@ impl PreferencesDialogMessageHandler {
|
|||||||
|
|
||||||
let checkbox_id = CheckboxId::new();
|
let checkbox_id = CheckboxId::new();
|
||||||
let brush_tool_description = "
|
let brush_tool_description = "
|
||||||
Enable the Brush tool to support basic raster-based layer painting.\n\
|
Enable the Brush tool to support basic layer painting.\n\
|
||||||
\n\
|
|
||||||
This legacy experimental tool has performance and quality limitations and is slated for replacement in future versions of Graphite that will have a renewed focus on raster graphics editing.\n\
|
|
||||||
\n\
|
|
||||||
Content created with the Brush tool may not be compatible with future versions of Graphite.\n\
|
|
||||||
\n\
|
\n\
|
||||||
*Default: Off.*
|
*Default: Off.*
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -46,11 +46,15 @@ impl LayoutHolder for LicensesDialog {
|
|||||||
let year = &self.localized_commit_year;
|
let year = &self.localized_commit_year;
|
||||||
let description = format!(
|
let description = format!(
|
||||||
"
|
"
|
||||||
Graphite source code is copyright © {year} Graphite contrib-\nutors and is available under the Apache License 2.0. See\n\"Source Code License\" for details.\n\
|
Graphite source code is copyright © {year} Graphite contrib-\n\
|
||||||
|
utors and is available under both the MIT and Apache 2.0\n\
|
||||||
|
licenses. See \"Source Code License\" for details.\n\
|
||||||
\n\
|
\n\
|
||||||
The Graphite logo, icons, and visual identity are copyright ©\n{year} Graphite Labs, LLC. See \"Branding License\" for details.\n\
|
The Graphite logo, icons, and visual identity are copyright ©\n\
|
||||||
|
{year} Graphite Labs, LLC. See \"Branding License\" for details.\n\
|
||||||
\n\
|
\n\
|
||||||
Graphite is distributed with third-party open source code\ndependencies. See \"Dependency Licenses\" for details.
|
Graphite is distributed with third-party open source code\n\
|
||||||
|
dependencies. See \"Dependency Licenses\" for details.
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
let description = description.trim();
|
let description = description.trim();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::IconName;
|
use super::IconName;
|
||||||
use super::utility_types::{MouseCursorIcon, PersistedState};
|
use super::utility_types::{MouseCursorIcon, PersistedState};
|
||||||
use crate::messages::app_window::app_window_message_handler::AppWindowPlatform;
|
use crate::messages::app_window::app_window_message_handler::AppWindowPlatform;
|
||||||
use crate::messages::frontend::utility_types::{DocumentInfo, EyedropperPreviewImage, RasterizedImage};
|
use crate::messages::frontend::utility_types::{DocumentInfo, EyedropperPreviewImage, FileFilter, RasterizedImage};
|
||||||
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
|
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
|
||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::portfolio::document::node_graph::utility_types::{
|
use crate::messages::portfolio::document::node_graph::utility_types::{
|
||||||
@@ -14,7 +14,7 @@ use crate::messages::prelude::*;
|
|||||||
use crate::messages::tool::tool_messages::eyedropper_tool::PrimarySecondary;
|
use crate::messages::tool::tool_messages::eyedropper_tool::PrimarySecondary;
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
use graphene_std::color::SRGBA8;
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::vector::style::FillChoiceUI;
|
use graphene_std::vector::style::FillChoice;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
@@ -87,16 +87,24 @@ pub enum FrontendMessage {
|
|||||||
commit_date: String,
|
commit_date: String,
|
||||||
},
|
},
|
||||||
TriggerDisplayThirdPartyLicensesDialog,
|
TriggerDisplayThirdPartyLicensesDialog,
|
||||||
|
TriggerOpen {
|
||||||
|
filters: Vec<FileFilter>,
|
||||||
|
},
|
||||||
|
TriggerImport {
|
||||||
|
filters: Vec<FileFilter>,
|
||||||
|
},
|
||||||
TriggerSaveDocument {
|
TriggerSaveDocument {
|
||||||
document_id: DocumentId,
|
document_id: DocumentId,
|
||||||
name: String,
|
name: String,
|
||||||
path: Option<PathBuf>,
|
path: Option<PathBuf>,
|
||||||
folder: Option<PathBuf>,
|
folder: Option<PathBuf>,
|
||||||
|
filters: Vec<FileFilter>,
|
||||||
content: serde_bytes::ByteBuf,
|
content: serde_bytes::ByteBuf,
|
||||||
},
|
},
|
||||||
TriggerSaveFile {
|
TriggerSaveFile {
|
||||||
name: String,
|
name: String,
|
||||||
folder: Option<PathBuf>,
|
folder: Option<PathBuf>,
|
||||||
|
filters: Vec<FileFilter>,
|
||||||
content: serde_bytes::ByteBuf,
|
content: serde_bytes::ByteBuf,
|
||||||
},
|
},
|
||||||
TriggerExportImage {
|
TriggerExportImage {
|
||||||
@@ -128,8 +136,6 @@ pub enum FrontendMessage {
|
|||||||
},
|
},
|
||||||
TriggerOpenLaunchDocuments,
|
TriggerOpenLaunchDocuments,
|
||||||
TriggerLoadPreferences,
|
TriggerLoadPreferences,
|
||||||
TriggerOpen,
|
|
||||||
TriggerImport,
|
|
||||||
TriggerSavePreferences {
|
TriggerSavePreferences {
|
||||||
#[cfg_attr(feature = "wasm", tsify(type = "unknown"))]
|
#[cfg_attr(feature = "wasm", tsify(type = "unknown"))]
|
||||||
preferences: PreferencesMessageHandler,
|
preferences: PreferencesMessageHandler,
|
||||||
@@ -165,7 +171,7 @@ pub enum FrontendMessage {
|
|||||||
},
|
},
|
||||||
/// The Rust color picker handler picked a new color/gradient. The frontend `<ColorPicker />` forwards this as its `colorOrGradient` event.
|
/// The Rust color picker handler picked a new color/gradient. The frontend `<ColorPicker />` forwards this as its `colorOrGradient` event.
|
||||||
ColorPickerColorChanged {
|
ColorPickerColorChanged {
|
||||||
value: FillChoiceUI,
|
value: FillChoice<SRGBA8>,
|
||||||
},
|
},
|
||||||
/// The Rust color picker handler is starting an undo transaction. The frontend `<ColorPicker />` forwards this as its `startHistoryTransaction` event.
|
/// The Rust color picker handler is starting an undo transaction. The frontend `<ColorPicker />` forwards this as its `startHistoryTransaction` event.
|
||||||
ColorPickerStartHistoryTransaction,
|
ColorPickerStartHistoryTransaction,
|
||||||
@@ -344,6 +350,10 @@ pub enum FrontendMessage {
|
|||||||
position: (f64, f64),
|
position: (f64, f64),
|
||||||
},
|
},
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
WindowUpdateDirectInput {
|
||||||
|
enabled: bool,
|
||||||
|
},
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
WindowClose,
|
WindowClose,
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
WindowMinimize,
|
WindowMinimize,
|
||||||
|
|||||||
@@ -64,6 +64,26 @@ impl FileType {
|
|||||||
FileType::Svg => "image/svg+xml",
|
FileType::Svg => "image/svg+xml",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn extension(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
FileType::Png => "png",
|
||||||
|
FileType::Jpg => "jpg",
|
||||||
|
FileType::Svg => "svg",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file_filter(self) -> FileFilter {
|
||||||
|
let name = match self {
|
||||||
|
FileType::Png => "PNG Image",
|
||||||
|
FileType::Jpg => "JPEG Image",
|
||||||
|
FileType::Svg => "SVG Image",
|
||||||
|
};
|
||||||
|
FileFilter {
|
||||||
|
name: name.into(),
|
||||||
|
extensions: vec![self.extension().into()],
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
@@ -91,3 +111,10 @@ pub struct RasterizedImage {
|
|||||||
pub height: u32,
|
pub height: u32,
|
||||||
pub pixels: serde_bytes::ByteBuf,
|
pub pixels: serde_bytes::ByteBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct FileFilter {
|
||||||
|
pub name: String,
|
||||||
|
pub extensions: Vec<String>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
use crate::messages::input_mapper::utility_types::keyboard::Key;
|
||||||
use crate::messages::input_mapper::utility_types::input_mouse::MouseButton;
|
use crate::messages::input_mapper::utility_types::pointer::MouseButton;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
|
||||||
#[impl_message(Message, KeyMappingMessage, Lookup)]
|
#[impl_message(Message, KeyMappingMessage, Lookup)]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::utility_types::input_keyboard::KeysGroup;
|
use super::utility_types::keyboard::KeysGroup;
|
||||||
use super::utility_types::misc::Mapping;
|
use super::utility_types::misc::Mapping;
|
||||||
use crate::application::Editor;
|
use crate::application::Editor;
|
||||||
use crate::messages::input_mapper::utility_types::input_keyboard::{self, Key};
|
use crate::messages::input_mapper::utility_types::keyboard::{self, Key};
|
||||||
use crate::messages::input_mapper::utility_types::misc::MappingEntry;
|
use crate::messages::input_mapper::utility_types::misc::MappingEntry;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ impl InputMapperMessageHandler {
|
|||||||
.map(|i| {
|
.map(|i| {
|
||||||
// TODO: Use a safe solution eventually
|
// TODO: Use a safe solution eventually
|
||||||
assert!(
|
assert!(
|
||||||
i < input_keyboard::NUMBER_OF_KEYS,
|
i < keyboard::NUMBER_OF_KEYS,
|
||||||
"Attempting to convert a Key with enum index {i}, which is larger than the number of Key enums",
|
"Attempting to convert a Key with enum index {i}, which is larger than the number of Key enums",
|
||||||
);
|
);
|
||||||
(i as u8).try_into().unwrap()
|
(i as u8).try_into().unwrap()
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use crate::application::Editor;
|
use crate::application::Editor;
|
||||||
use crate::consts::{BIG_NUDGE_AMOUNT, BRUSH_SIZE_CHANGE_KEYBOARD, NUDGE_AMOUNT};
|
use crate::consts::{BIG_NUDGE_AMOUNT, BRUSH_SIZE_CHANGE_KEYBOARD, NUDGE_AMOUNT};
|
||||||
use crate::messages::input_mapper::key_mapping::MappingVariant;
|
use crate::messages::input_mapper::key_mapping::MappingVariant;
|
||||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeyStates};
|
use crate::messages::input_mapper::utility_types::keyboard::{Key, KeyStates};
|
||||||
use crate::messages::input_mapper::utility_types::input_mouse::MouseButton;
|
|
||||||
use crate::messages::input_mapper::utility_types::macros::*;
|
use crate::messages::input_mapper::utility_types::macros::*;
|
||||||
use crate::messages::input_mapper::utility_types::misc::MappingEntry;
|
use crate::messages::input_mapper::utility_types::misc::MappingEntry;
|
||||||
use crate::messages::input_mapper::utility_types::misc::{KeyMappingEntries, Mapping};
|
use crate::messages::input_mapper::utility_types::misc::{KeyMappingEntries, Mapping};
|
||||||
|
use crate::messages::input_mapper::utility_types::pointer::MouseButton;
|
||||||
use crate::messages::portfolio::document::node_graph::utility_types::Direction;
|
use crate::messages::portfolio::document::node_graph::utility_types::Direction;
|
||||||
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
|
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::messages::input_mapper::input_mapper_message_handler::InputMapperMessageContext;
|
use crate::messages::input_mapper::input_mapper_message_handler::InputMapperMessageContext;
|
||||||
use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
|
use crate::messages::input_mapper::utility_types::keyboard::KeysGroup;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
|
||||||
#[derive(ExtractField)]
|
#[derive(ExtractField)]
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ macro_rules! action_shortcut {
|
|||||||
macro_rules! action_shortcut_manual {
|
macro_rules! action_shortcut_manual {
|
||||||
($($keys:expr),*) => {
|
($($keys:expr),*) => {
|
||||||
Some(crate::messages::input_mapper::utility_types::misc::ActionShortcut::Shortcut(
|
Some(crate::messages::input_mapper::utility_types::misc::ActionShortcut::Shortcut(
|
||||||
crate::messages::input_mapper::utility_types::input_keyboard::LabeledShortcut(vec![$($keys.into()),*]).into(),
|
crate::messages::input_mapper::utility_types::keyboard::LabeledShortcut(vec![$($keys.into()),*]).into(),
|
||||||
))
|
))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::input_keyboard::{KeysGroup, LabeledShortcut, all_required_modifiers_pressed};
|
use super::keyboard::{KeysGroup, LabeledShortcut, all_required_modifiers_pressed};
|
||||||
use crate::messages::input_mapper::key_mapping::MappingVariant;
|
use crate::messages::input_mapper::key_mapping::MappingVariant;
|
||||||
use crate::messages::input_mapper::utility_types::input_keyboard::{KeyStates, NUMBER_OF_KEYS};
|
use crate::messages::input_mapper::utility_types::keyboard::{KeyStates, NUMBER_OF_KEYS};
|
||||||
use crate::messages::input_mapper::utility_types::input_mouse::NUMBER_OF_MOUSE_BUTTONS;
|
use crate::messages::input_mapper::utility_types::pointer::NUMBER_OF_MOUSE_BUTTONS;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use core::time::Duration;
|
use core::time::Duration;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod input_keyboard;
|
pub mod keyboard;
|
||||||
pub mod input_mouse;
|
pub mod pointer;
|
||||||
|
|
||||||
pub mod macros;
|
pub mod macros;
|
||||||
pub mod misc;
|
pub mod misc;
|
||||||
|
|||||||
+27
-9
@@ -50,15 +50,21 @@ impl ScrollDelta {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Document the difference between this and EditorMouseState
|
// TODO: Document the difference between this and EditorPointerState
|
||||||
#[derive(Debug, Copy, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Copy, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct MouseState {
|
pub struct PointerState {
|
||||||
pub position: ViewportPosition,
|
pub position: ViewportPosition,
|
||||||
pub mouse_keys: MouseKeys,
|
pub mouse_keys: MouseKeys,
|
||||||
pub scroll_delta: ScrollDelta,
|
pub scroll_delta: ScrollDelta,
|
||||||
|
pub time: Option<f64>,
|
||||||
|
pub pressure: Option<f64>,
|
||||||
|
pub tilt: Option<DVec2>,
|
||||||
|
pub twist: Option<f64>,
|
||||||
|
pub wheel: Option<f64>,
|
||||||
|
pub eraser: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MouseState {
|
impl PointerState {
|
||||||
pub fn finish_transaction(&self, drag_start: DVec2, responses: &mut VecDeque<Message>) {
|
pub fn finish_transaction(&self, drag_start: DVec2, responses: &mut VecDeque<Message>) {
|
||||||
let drag_too_small = drag_start.distance(self.position) <= DRAG_THRESHOLD;
|
let drag_too_small = drag_start.distance(self.position) <= DRAG_THRESHOLD;
|
||||||
let response = if drag_too_small { DocumentMessage::AbortTransaction } else { DocumentMessage::EndTransaction };
|
let response = if drag_too_small { DocumentMessage::AbortTransaction } else { DocumentMessage::EndTransaction };
|
||||||
@@ -66,15 +72,21 @@ impl MouseState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Document the difference between this and MouseState
|
// TODO: Document the difference between this and PointerState
|
||||||
#[derive(Debug, Copy, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Copy, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct EditorMouseState {
|
pub struct EditorPointerState {
|
||||||
pub editor_position: EditorPosition,
|
pub editor_position: EditorPosition,
|
||||||
pub mouse_keys: MouseKeys,
|
pub mouse_keys: MouseKeys,
|
||||||
pub scroll_delta: ScrollDelta,
|
pub scroll_delta: ScrollDelta,
|
||||||
|
pub time: Option<f64>,
|
||||||
|
pub pressure: Option<f64>,
|
||||||
|
pub tilt: Option<DVec2>,
|
||||||
|
pub twist: Option<f64>,
|
||||||
|
pub wheel: Option<f64>,
|
||||||
|
pub eraser: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EditorMouseState {
|
impl EditorPointerState {
|
||||||
pub fn from_keys_and_editor_position(keys: u8, editor_position: EditorPosition) -> Self {
|
pub fn from_keys_and_editor_position(keys: u8, editor_position: EditorPosition) -> Self {
|
||||||
// TODO: Some graphic tablets send key codes not mentioned in the spec. In the future we would like to support these as well.
|
// TODO: Some graphic tablets send key codes not mentioned in the spec. In the future we would like to support these as well.
|
||||||
let mouse_keys = MouseKeys::from_bits_truncate(keys);
|
let mouse_keys = MouseKeys::from_bits_truncate(keys);
|
||||||
@@ -82,15 +94,21 @@ impl EditorMouseState {
|
|||||||
Self {
|
Self {
|
||||||
editor_position,
|
editor_position,
|
||||||
mouse_keys,
|
mouse_keys,
|
||||||
scroll_delta: ScrollDelta::default(),
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_mouse_state(&self, viewport: &ViewportMessageHandler) -> MouseState {
|
pub fn to_pointer_state(&self, viewport: &ViewportMessageHandler) -> PointerState {
|
||||||
MouseState {
|
PointerState {
|
||||||
position: (viewport.logical(self.editor_position) - viewport.offset()).into(),
|
position: (viewport.logical(self.editor_position) - viewport.offset()).into(),
|
||||||
mouse_keys: self.mouse_keys,
|
mouse_keys: self.mouse_keys,
|
||||||
scroll_delta: self.scroll_delta,
|
scroll_delta: self.scroll_delta,
|
||||||
|
time: self.time,
|
||||||
|
pressure: self.pressure,
|
||||||
|
tilt: self.tilt,
|
||||||
|
twist: self.twist,
|
||||||
|
wheel: self.wheel,
|
||||||
|
eraser: self.eraser,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys};
|
use crate::messages::input_mapper::utility_types::keyboard::{Key, ModifierKeys};
|
||||||
use crate::messages::input_mapper::utility_types::input_mouse::EditorMouseState;
|
use crate::messages::input_mapper::utility_types::pointer::EditorPointerState;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
|
||||||
#[impl_message(Message, InputPreprocessor)]
|
#[impl_message(Message, InputPreprocessor)]
|
||||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum InputPreprocessorMessage {
|
pub enum InputPreprocessorMessage {
|
||||||
DoubleClick { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
DoubleClick { editor_mouse_state: EditorPointerState, modifier_keys: ModifierKeys },
|
||||||
KeyDown { key: Key, key_repeat: bool, modifier_keys: ModifierKeys },
|
KeyDown { key: Key, key_repeat: bool, modifier_keys: ModifierKeys },
|
||||||
KeyUp { key: Key, key_repeat: bool, modifier_keys: ModifierKeys },
|
KeyUp { key: Key, key_repeat: bool, modifier_keys: ModifierKeys },
|
||||||
PointerDown { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
PointerDown { editor_mouse_state: EditorPointerState, modifier_keys: ModifierKeys },
|
||||||
PointerMove { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
PointerMove { editor_mouse_state: EditorPointerState, modifier_keys: ModifierKeys },
|
||||||
PointerUp { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
PointerUp { editor_mouse_state: EditorPointerState, modifier_keys: ModifierKeys },
|
||||||
PointerShake { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
PointerShake { editor_mouse_state: EditorPointerState, modifier_keys: ModifierKeys },
|
||||||
CurrentTime { timestamp: u64 },
|
CurrentTime { timestamp: u64 },
|
||||||
WheelScroll { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
WheelScroll { editor_mouse_state: EditorPointerState, modifier_keys: ModifierKeys },
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::application::Editor;
|
use crate::application::Editor;
|
||||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeyStates, ModifierKeys};
|
use crate::messages::input_mapper::utility_types::keyboard::{Key, KeyStates, ModifierKeys};
|
||||||
use crate::messages::input_mapper::utility_types::input_mouse::{MouseButton, MouseKeys, MouseState};
|
|
||||||
use crate::messages::input_mapper::utility_types::misc::FrameTimeInfo;
|
use crate::messages::input_mapper::utility_types::misc::FrameTimeInfo;
|
||||||
|
use crate::messages::input_mapper::utility_types::pointer::{MouseButton, MouseKeys, PointerState};
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -15,7 +15,8 @@ pub struct InputPreprocessorMessageHandler {
|
|||||||
pub frame_time: FrameTimeInfo,
|
pub frame_time: FrameTimeInfo,
|
||||||
pub time: u64,
|
pub time: u64,
|
||||||
pub keyboard: KeyStates,
|
pub keyboard: KeyStates,
|
||||||
pub mouse: MouseState,
|
pub mouse: PointerState,
|
||||||
|
pointer_down_time: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[message_handler_data]
|
#[message_handler_data]
|
||||||
@@ -27,10 +28,10 @@ impl<'a> MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContex
|
|||||||
InputPreprocessorMessage::DoubleClick { editor_mouse_state, modifier_keys } => {
|
InputPreprocessorMessage::DoubleClick { editor_mouse_state, modifier_keys } => {
|
||||||
self.update_states_of_modifier_keys(modifier_keys, responses);
|
self.update_states_of_modifier_keys(modifier_keys, responses);
|
||||||
|
|
||||||
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
|
let pointer_state = editor_mouse_state.to_pointer_state(viewport);
|
||||||
self.mouse.position = mouse_state.position;
|
self.mouse.position = pointer_state.position;
|
||||||
|
|
||||||
for key in mouse_state.mouse_keys {
|
for key in pointer_state.mouse_keys {
|
||||||
responses.add(InputMapperMessage::DoubleClick(match key {
|
responses.add(InputMapperMessage::DoubleClick(match key {
|
||||||
MouseKeys::LEFT => MouseButton::Left,
|
MouseKeys::LEFT => MouseButton::Left,
|
||||||
MouseKeys::RIGHT => MouseButton::Right,
|
MouseKeys::RIGHT => MouseButton::Right,
|
||||||
@@ -60,35 +61,35 @@ impl<'a> MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContex
|
|||||||
InputPreprocessorMessage::PointerDown { editor_mouse_state, modifier_keys } => {
|
InputPreprocessorMessage::PointerDown { editor_mouse_state, modifier_keys } => {
|
||||||
self.update_states_of_modifier_keys(modifier_keys, responses);
|
self.update_states_of_modifier_keys(modifier_keys, responses);
|
||||||
|
|
||||||
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
|
let pointer_state = editor_mouse_state.to_pointer_state(viewport);
|
||||||
self.mouse.position = mouse_state.position;
|
self.mouse.position = pointer_state.position;
|
||||||
|
|
||||||
self.translate_mouse_event(mouse_state, true, responses);
|
self.translate_mouse_event(pointer_state, true, responses);
|
||||||
}
|
}
|
||||||
InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys } => {
|
InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys } => {
|
||||||
self.update_states_of_modifier_keys(modifier_keys, responses);
|
self.update_states_of_modifier_keys(modifier_keys, responses);
|
||||||
|
|
||||||
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
|
let pointer_state = editor_mouse_state.to_pointer_state(viewport);
|
||||||
self.mouse.position = mouse_state.position;
|
self.mouse.position = pointer_state.position;
|
||||||
|
|
||||||
responses.add(InputMapperMessage::PointerMove);
|
responses.add(InputMapperMessage::PointerMove);
|
||||||
|
|
||||||
// While any pointer button is already down, additional button down events are not reported, but they are sent as `pointermove` events
|
// While any pointer button is already down, additional button down events are not reported, but they are sent as `pointermove` events
|
||||||
self.translate_mouse_event(mouse_state, false, responses);
|
self.translate_mouse_event(pointer_state, false, responses);
|
||||||
}
|
}
|
||||||
InputPreprocessorMessage::PointerUp { editor_mouse_state, modifier_keys } => {
|
InputPreprocessorMessage::PointerUp { editor_mouse_state, modifier_keys } => {
|
||||||
self.update_states_of_modifier_keys(modifier_keys, responses);
|
self.update_states_of_modifier_keys(modifier_keys, responses);
|
||||||
|
|
||||||
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
|
let pointer_state = editor_mouse_state.to_pointer_state(viewport);
|
||||||
self.mouse.position = mouse_state.position;
|
self.mouse.position = pointer_state.position;
|
||||||
|
|
||||||
self.translate_mouse_event(mouse_state, false, responses);
|
self.translate_mouse_event(pointer_state, false, responses);
|
||||||
}
|
}
|
||||||
InputPreprocessorMessage::PointerShake { editor_mouse_state, modifier_keys } => {
|
InputPreprocessorMessage::PointerShake { editor_mouse_state, modifier_keys } => {
|
||||||
self.update_states_of_modifier_keys(modifier_keys, responses);
|
self.update_states_of_modifier_keys(modifier_keys, responses);
|
||||||
|
|
||||||
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
|
let pointer_state = editor_mouse_state.to_pointer_state(viewport);
|
||||||
self.mouse.position = mouse_state.position;
|
self.mouse.position = pointer_state.position;
|
||||||
|
|
||||||
responses.add(InputMapperMessage::PointerShake);
|
responses.add(InputMapperMessage::PointerShake);
|
||||||
}
|
}
|
||||||
@@ -100,9 +101,9 @@ impl<'a> MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContex
|
|||||||
InputPreprocessorMessage::WheelScroll { editor_mouse_state, modifier_keys } => {
|
InputPreprocessorMessage::WheelScroll { editor_mouse_state, modifier_keys } => {
|
||||||
self.update_states_of_modifier_keys(modifier_keys, responses);
|
self.update_states_of_modifier_keys(modifier_keys, responses);
|
||||||
|
|
||||||
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
|
let pointer_state = editor_mouse_state.to_pointer_state(viewport);
|
||||||
self.mouse.position = mouse_state.position;
|
self.mouse.position = pointer_state.position;
|
||||||
self.mouse.scroll_delta = mouse_state.scroll_delta;
|
self.mouse.scroll_delta = pointer_state.scroll_delta;
|
||||||
|
|
||||||
responses.add(InputMapperMessage::WheelScroll);
|
responses.add(InputMapperMessage::WheelScroll);
|
||||||
}
|
}
|
||||||
@@ -116,7 +117,7 @@ impl<'a> MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContex
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl InputPreprocessorMessageHandler {
|
impl InputPreprocessorMessageHandler {
|
||||||
fn translate_mouse_event(&mut self, mut new_state: MouseState, allow_first_button_down: bool, responses: &mut VecDeque<Message>) {
|
fn translate_mouse_event(&mut self, mut new_state: PointerState, allow_first_button_down: bool, responses: &mut VecDeque<Message>) {
|
||||||
let click_mappings = [
|
let click_mappings = [
|
||||||
(MouseKeys::LEFT, Key::MouseLeft),
|
(MouseKeys::LEFT, Key::MouseLeft),
|
||||||
(MouseKeys::RIGHT, Key::MouseRight),
|
(MouseKeys::RIGHT, Key::MouseRight),
|
||||||
@@ -144,6 +145,13 @@ impl InputPreprocessorMessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let raw_time = new_state.time.unwrap_or(self.time as f64);
|
||||||
|
if self.mouse.mouse_keys == MouseKeys::empty() {
|
||||||
|
self.pointer_down_time = raw_time;
|
||||||
|
}
|
||||||
|
let interacting = (self.mouse.mouse_keys | new_state.mouse_keys) != MouseKeys::empty();
|
||||||
|
new_state.time = interacting.then_some(raw_time - self.pointer_down_time);
|
||||||
|
|
||||||
self.mouse = new_state;
|
self.mouse = new_state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,18 +193,17 @@ impl InputPreprocessorMessageHandler {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys};
|
use crate::messages::input_mapper::utility_types::keyboard::{Key, ModifierKeys};
|
||||||
use crate::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, MouseKeys, ScrollDelta};
|
use crate::messages::input_mapper::utility_types::pointer::EditorPointerState;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn process_action_mouse_move_handle_modifier_keys() {
|
fn process_action_mouse_move_handle_modifier_keys() {
|
||||||
let mut input_preprocessor = InputPreprocessorMessageHandler::default();
|
let mut input_preprocessor = InputPreprocessorMessageHandler::default();
|
||||||
|
|
||||||
let editor_mouse_state = EditorMouseState {
|
let editor_mouse_state = EditorPointerState {
|
||||||
editor_position: (4., 809.).into(),
|
editor_position: (4., 809.).into(),
|
||||||
mouse_keys: MouseKeys::default(),
|
..Default::default()
|
||||||
scroll_delta: ScrollDelta::default(),
|
|
||||||
};
|
};
|
||||||
let modifier_keys = ModifierKeys::ALT;
|
let modifier_keys = ModifierKeys::ALT;
|
||||||
let message = InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys };
|
let message = InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys };
|
||||||
@@ -216,7 +223,7 @@ mod test {
|
|||||||
fn process_action_mouse_down_handle_modifier_keys() {
|
fn process_action_mouse_down_handle_modifier_keys() {
|
||||||
let mut input_preprocessor = InputPreprocessorMessageHandler::default();
|
let mut input_preprocessor = InputPreprocessorMessageHandler::default();
|
||||||
|
|
||||||
let editor_mouse_state = EditorMouseState::default();
|
let editor_mouse_state = EditorPointerState::default();
|
||||||
let modifier_keys = ModifierKeys::CONTROL;
|
let modifier_keys = ModifierKeys::CONTROL;
|
||||||
let message = InputPreprocessorMessage::PointerDown { editor_mouse_state, modifier_keys };
|
let message = InputPreprocessorMessage::PointerDown { editor_mouse_state, modifier_keys };
|
||||||
|
|
||||||
@@ -235,7 +242,7 @@ mod test {
|
|||||||
fn process_action_mouse_up_handle_modifier_keys() {
|
fn process_action_mouse_up_handle_modifier_keys() {
|
||||||
let mut input_preprocessor = InputPreprocessorMessageHandler::default();
|
let mut input_preprocessor = InputPreprocessorMessageHandler::default();
|
||||||
|
|
||||||
let editor_mouse_state = EditorMouseState::default();
|
let editor_mouse_state = EditorPointerState::default();
|
||||||
let modifier_keys = ModifierKeys::SHIFT;
|
let modifier_keys = ModifierKeys::SHIFT;
|
||||||
let message = InputPreprocessorMessage::PointerUp { editor_mouse_state, modifier_keys };
|
let message = InputPreprocessorMessage::PointerUp { editor_mouse_state, modifier_keys };
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
|
use crate::messages::input_mapper::utility_types::keyboard::KeysGroup;
|
||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
|
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use graphene_std::color::SRGBA8;
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::vector::style::FillChoiceUI;
|
use graphene_std::vector::style::FillChoice;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@@ -117,23 +117,26 @@ impl LayoutMessageHandler {
|
|||||||
}
|
}
|
||||||
LayoutGroup::Table(WidgetTable { rows, .. }) => {
|
LayoutGroup::Table(WidgetTable { rows, .. }) => {
|
||||||
for (row_index, row) in rows.iter().enumerate() {
|
for (row_index, row) in rows.iter().enumerate() {
|
||||||
for (value_index, value) in row.iter().enumerate() {
|
for (cell_index, cell) in row.iter().enumerate() {
|
||||||
// Return if this is the correct ID
|
for (value_index, value) in cell.iter().enumerate() {
|
||||||
if value.widget_id == widget_id {
|
// Return if this is the correct ID
|
||||||
widget_path.push(row_index);
|
if value.widget_id == widget_id {
|
||||||
widget_path.push(value_index);
|
widget_path.push(row_index);
|
||||||
return Some((value, widget_path));
|
widget_path.push(cell_index);
|
||||||
}
|
widget_path.push(value_index);
|
||||||
|
return Some((value, widget_path));
|
||||||
|
}
|
||||||
|
|
||||||
if let Widget::PopoverButton(popover) = &*value.widget {
|
if let Widget::PopoverButton(popover) = &*value.widget {
|
||||||
stack.extend(
|
stack.extend(
|
||||||
popover
|
popover
|
||||||
.popover_layout
|
.popover_layout
|
||||||
.0
|
.0
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(child, val)| ([widget_path.as_slice(), &[row_index, value_index, child]].concat(), val)),
|
.map(|(child, val)| ([widget_path.as_slice(), &[row_index, cell_index, value_index, child]].concat(), val)),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,12 +197,16 @@ impl LayoutMessageHandler {
|
|||||||
let callback_message = match action {
|
let callback_message = match action {
|
||||||
WidgetValueAction::Commit => (color_button.on_commit.callback)(&()),
|
WidgetValueAction::Commit => (color_button.on_commit.callback)(&()),
|
||||||
WidgetValueAction::Update => {
|
WidgetValueAction::Update => {
|
||||||
let Ok(fill_choice_ui) = serde_json::from_value::<FillChoiceUI>(value) else {
|
let Ok(fill_choice) = serde_json::from_value::<FillChoice<SRGBA8>>(value) else {
|
||||||
warn!("ColorInput update was not able to be parsed as FillChoiceUI: {color_button:?}");
|
warn!("ColorInput update was not able to be parsed as FillChoice<SRGBA8>: {color_button:?}");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
color_button.value = fill_choice_ui;
|
// The stored copy has to keep mirroring what the frontend was last sent, so the rebuilt layout still
|
||||||
(color_button.on_update.callback)(color_button)
|
// diffs against it and syncs picks that leave the swatch unchanged; the callback borrows the new one
|
||||||
|
let previous_value = std::mem::replace(&mut color_button.value, fill_choice);
|
||||||
|
let update_message = (color_button.on_update.callback)(color_button);
|
||||||
|
color_button.value = previous_value;
|
||||||
|
update_message
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -528,9 +535,28 @@ fn populate_computed_display_fields(layout: &mut Layout) {
|
|||||||
color_input.chosen_gradient = color_input.value.to_css_background_image();
|
color_input.chosen_gradient = color_input.value.to_css_background_image();
|
||||||
}
|
}
|
||||||
Widget::SpectrumInput(spectrum_input) => {
|
Widget::SpectrumInput(spectrum_input) => {
|
||||||
spectrum_input.track_css = spectrum_input.track.to_css_linear_gradient();
|
// The track strip spans exactly 0 to 1, which no spread affects, so the widget carries no spread of its own
|
||||||
spectrum_input.track_start_css = spectrum_input.track.color.first().map(|color| color.to_css_hex()).unwrap_or_else(|| "black".to_string());
|
let settings = graphene_std::vector::style::GradientSettings {
|
||||||
spectrum_input.track_end_css = spectrum_input.track.color.last().map(|color| color.to_css_hex()).unwrap_or_else(|| "black".to_string());
|
spread: Default::default(),
|
||||||
|
cyclic: spectrum_input.track_cyclic,
|
||||||
|
space: spectrum_input.track_space,
|
||||||
|
hue_direction: spectrum_input.track_hue_direction,
|
||||||
|
interpolation: spectrum_input.track_interpolation,
|
||||||
|
};
|
||||||
|
let track_gradient = graphene_std::vector::style::Gradient::from(&spectrum_input.track);
|
||||||
|
spectrum_input.track_samples = track_gradient
|
||||||
|
.interpolated_samples_or_black(settings)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(position, color, _)| SpectrumSample::new(position, color))
|
||||||
|
.collect();
|
||||||
|
// The end caps sample the track's boundary colors, which a cyclic wrap makes the wrapped interval's boundary-crossing color rather than the outermost stops'
|
||||||
|
let track_evaluator = track_gradient.evaluator(settings);
|
||||||
|
let cap = |t: f64| {
|
||||||
|
let color = track_evaluator.evaluate(t);
|
||||||
|
SRGBA8::from(color).to_css_hex()
|
||||||
|
};
|
||||||
|
spectrum_input.track_start_css = cap(0.);
|
||||||
|
spectrum_input.track_end_css = cap(1.);
|
||||||
}
|
}
|
||||||
Widget::ColorComparisonInput(comparison) => {
|
Widget::ColorComparisonInput(comparison) => {
|
||||||
let contrasting = |color: Option<SRGBA8>| color.map_or(SRGBA8::BLACK, |color| color.contrasting_text_color()).to_css_hex();
|
let contrasting = |color: Option<SRGBA8>| color.map_or(SRGBA8::BLACK, |color| color.contrasting_text_color()).to_css_hex();
|
||||||
@@ -550,7 +576,6 @@ fn evaluate_and_validate_number_input(expression: &str, number_input: &NumberInp
|
|||||||
let value = math_parser::evaluate(expression)
|
let value = math_parser::evaluate(expression)
|
||||||
.inspect_err(|err| error!("Math parser error on \"{expression}\": {err}"))
|
.inspect_err(|err| error!("Math parser error on \"{expression}\": {err}"))
|
||||||
.ok()?
|
.ok()?
|
||||||
.0
|
|
||||||
.inspect_err(|err| error!("Math evaluate error on \"{expression}\": {err}"))
|
.inspect_err(|err| error!("Math evaluate error on \"{expression}\": {err}"))
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use super::widgets::button_widgets::*;
|
|||||||
use super::widgets::input_widgets::*;
|
use super::widgets::input_widgets::*;
|
||||||
use super::widgets::label_widgets::*;
|
use super::widgets::label_widgets::*;
|
||||||
use crate::application::generate_uuid;
|
use crate::application::generate_uuid;
|
||||||
use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
|
use crate::messages::input_mapper::utility_types::keyboard::KeysGroup;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
@@ -273,7 +273,7 @@ impl<'a> Iterator for WidgetIter<'a> {
|
|||||||
self.next()
|
self.next()
|
||||||
}
|
}
|
||||||
Some(LayoutGroup::Table(WidgetTable { rows, .. })) => {
|
Some(LayoutGroup::Table(WidgetTable { rows, .. })) => {
|
||||||
self.table.extend(rows.iter().flatten().rev());
|
self.table.extend(rows.iter().flatten().flatten().rev());
|
||||||
self.next()
|
self.next()
|
||||||
}
|
}
|
||||||
Some(LayoutGroup::Section(WidgetSection { layout, .. })) => {
|
Some(LayoutGroup::Section(WidgetSection { layout, .. })) => {
|
||||||
@@ -325,7 +325,7 @@ impl<'a> Iterator for WidgetIterMut<'a> {
|
|||||||
self.next()
|
self.next()
|
||||||
}
|
}
|
||||||
Some(LayoutGroup::Table(WidgetTable { rows, .. })) => {
|
Some(LayoutGroup::Table(WidgetTable { rows, .. })) => {
|
||||||
self.table.extend(rows.iter_mut().flatten().rev());
|
self.table.extend(rows.iter_mut().flatten().flatten().rev());
|
||||||
self.next()
|
self.next()
|
||||||
}
|
}
|
||||||
Some(LayoutGroup::Section(WidgetSection { layout, .. })) => {
|
Some(LayoutGroup::Section(WidgetSection { layout, .. })) => {
|
||||||
@@ -365,8 +365,9 @@ pub struct WidgetRow {
|
|||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct WidgetTable {
|
pub struct WidgetTable {
|
||||||
|
/// Rows of cells, each cell holding one or more widgets laid out inline.
|
||||||
#[serde(rename = "tableWidgets")]
|
#[serde(rename = "tableWidgets")]
|
||||||
pub rows: Vec<Vec<WidgetInstance>>,
|
pub rows: Vec<Vec<Vec<WidgetInstance>>>,
|
||||||
pub unstyled: bool,
|
pub unstyled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,7 +413,14 @@ impl LayoutGroup {
|
|||||||
Self::Column(WidgetColumn { widgets })
|
Self::Column(WidgetColumn { widgets })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A table with one widget per cell.
|
||||||
pub fn table(rows: Vec<Vec<WidgetInstance>>, unstyled: bool) -> Self {
|
pub fn table(rows: Vec<Vec<WidgetInstance>>, unstyled: bool) -> Self {
|
||||||
|
let rows = rows.into_iter().map(|row| row.into_iter().map(|widget| vec![widget]).collect()).collect();
|
||||||
|
Self::Table(WidgetTable { rows, unstyled })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A table whose cells may each hold multiple widgets laid out inline.
|
||||||
|
pub fn table_of_cells(rows: Vec<Vec<Vec<WidgetInstance>>>, unstyled: bool) -> Self {
|
||||||
Self::Table(WidgetTable { rows, unstyled })
|
Self::Table(WidgetTable { rows, unstyled })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,13 +608,17 @@ impl Diffable for LayoutGroup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Table(WidgetTable { rows, .. }) => {
|
Self::Table(WidgetTable { rows, .. }) => {
|
||||||
for (row_idx, row) in rows.iter().enumerate() {
|
for (row_index, row) in rows.iter().enumerate() {
|
||||||
for (col_idx, widget) in row.iter().enumerate() {
|
for (column_index, cell) in row.iter().enumerate() {
|
||||||
widget_path.push(row_idx);
|
for (widget_index, widget) in cell.iter().enumerate() {
|
||||||
widget_path.push(col_idx);
|
widget_path.push(row_index);
|
||||||
widget.collect_checkbox_ids(layout_target, widget_path, checkbox_map);
|
widget_path.push(column_index);
|
||||||
widget_path.pop();
|
widget_path.push(widget_index);
|
||||||
widget_path.pop();
|
widget.collect_checkbox_ids(layout_target, widget_path, checkbox_map);
|
||||||
|
widget_path.pop();
|
||||||
|
widget_path.pop();
|
||||||
|
widget_path.pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -626,13 +638,17 @@ impl Diffable for LayoutGroup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Table(WidgetTable { rows, .. }) => {
|
Self::Table(WidgetTable { rows, .. }) => {
|
||||||
for (row_idx, row) in rows.iter_mut().enumerate() {
|
for (row_index, row) in rows.iter_mut().enumerate() {
|
||||||
for (col_idx, widget) in row.iter_mut().enumerate() {
|
for (column_index, cell) in row.iter_mut().enumerate() {
|
||||||
widget_path.push(row_idx);
|
for (widget_index, widget) in cell.iter_mut().enumerate() {
|
||||||
widget_path.push(col_idx);
|
widget_path.push(row_index);
|
||||||
widget.replace_widget_ids(layout_target, widget_path, checkbox_map);
|
widget_path.push(column_index);
|
||||||
widget_path.pop();
|
widget_path.push(widget_index);
|
||||||
widget_path.pop();
|
widget.replace_widget_ids(layout_target, widget_path, checkbox_map);
|
||||||
|
widget_path.pop();
|
||||||
|
widget_path.pop();
|
||||||
|
widget_path.pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ use crate::messages::layout::utility_types::widget_prelude::*;
|
|||||||
use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType;
|
use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType;
|
||||||
use crate::messages::tool::tool_messages::tool_prelude::WidgetCallback;
|
use crate::messages::tool::tool_messages::tool_prelude::WidgetCallback;
|
||||||
use derivative::*;
|
use derivative::*;
|
||||||
use graphene_std::vector::style::FillChoiceUI;
|
use graphene_std::color::SRGBA8;
|
||||||
|
use graphene_std::vector::style::FillChoice;
|
||||||
use graphite_proc_macros::WidgetBuilder;
|
use graphite_proc_macros::WidgetBuilder;
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
@@ -191,9 +192,9 @@ pub struct ImageButton {
|
|||||||
pub struct ColorInput {
|
pub struct ColorInput {
|
||||||
// Content
|
// Content
|
||||||
#[widget_builder(constructor)]
|
#[widget_builder(constructor)]
|
||||||
pub value: FillChoiceUI,
|
pub value: FillChoice<SRGBA8>,
|
||||||
/// CSS `linear-gradient(...)` (or solid-color stand-in) for the swatch's `background-image`. Auto-populated from `value` at layout-send time.
|
/// CSS `linear-gradient(...)` (or solid-color stand-in) for the swatch's `background-image`. Auto-populated from `value` at layout-send time.
|
||||||
/// `None` when `value` is `FillChoiceUI::None`, in which case the frontend uses its "none" fallback styling.
|
/// `None` when `value` is `FillChoice::<SRGBA8>::None`, in which case the frontend uses its "none" fallback styling.
|
||||||
#[serde(rename = "chosenGradient")]
|
#[serde(rename = "chosenGradient")]
|
||||||
#[widget_builder(skip)]
|
#[widget_builder(skip)]
|
||||||
pub chosen_gradient: Option<String>,
|
pub chosen_gradient: Option<String>,
|
||||||
@@ -232,6 +233,24 @@ pub struct ColorInput {
|
|||||||
pub on_commit: WidgetCallback<()>,
|
pub on_commit: WidgetCallback<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shortens a breadcrumb label to fit the trail, replacing the tail with an ellipsis.
|
||||||
|
/// Quotes delimit a string value, so a quoted label budgets only the text between them.
|
||||||
|
pub fn truncate_breadcrumb_label(label: &str) -> String {
|
||||||
|
const MAX_CHARACTERS: usize = 40;
|
||||||
|
|
||||||
|
let quoted = label.len() >= 2 && label.starts_with('"') && label.ends_with('"');
|
||||||
|
let content = if quoted { &label[1..label.len() - 1] } else { label };
|
||||||
|
|
||||||
|
if content.chars().count() <= MAX_CHARACTERS {
|
||||||
|
return label.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut truncated: String = content.chars().take(MAX_CHARACTERS - 1).collect();
|
||||||
|
truncated.push('…');
|
||||||
|
|
||||||
|
if quoted { format!("\"{truncated}\"") } else { truncated }
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder)]
|
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder)]
|
||||||
#[derivative(Debug, PartialEq)]
|
#[derivative(Debug, PartialEq)]
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
use crate::messages::frontend::IconName;
|
use crate::messages::frontend::IconName;
|
||||||
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
|
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
|
||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
|
use crate::messages::message::Message;
|
||||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
|
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
|
||||||
use derivative::*;
|
use derivative::*;
|
||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
use graphene_std::color::SRGBA8;
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::transform::ReferencePoint;
|
use graphene_std::transform::ReferencePoint;
|
||||||
use graphene_std::vector::style::{FillChoiceUI, GradientStopsUI};
|
use graphene_std::vector::style::{FillChoice, GradientHueDirection, GradientInterpolation, GradientSpace, GradientStops};
|
||||||
use graphite_proc_macros::WidgetBuilder;
|
use graphite_proc_macros::WidgetBuilder;
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
@@ -150,6 +151,35 @@ impl std::hash::Hash for MenuListEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MenuListEntry {
|
||||||
|
/// One entry per variant of a choice type enum, keeping the choice type's section groupings, shown as the variant's label alongside its icon when it has one.
|
||||||
|
pub fn sections_from_choice_type<E>(to_message: impl Fn(E) -> Message + Clone + Send + Sync + 'static) -> MenuListEntrySections
|
||||||
|
where
|
||||||
|
E: graphene_std::choice_type::ChoiceTypeStatic + 'static,
|
||||||
|
{
|
||||||
|
E::list()
|
||||||
|
.iter()
|
||||||
|
.map(|section| {
|
||||||
|
section
|
||||||
|
.iter()
|
||||||
|
.map(|(variant, metadata)| {
|
||||||
|
let to_message = to_message.clone();
|
||||||
|
let variant = *variant;
|
||||||
|
|
||||||
|
let entry = MenuListEntry::new(metadata.name)
|
||||||
|
.label(metadata.label)
|
||||||
|
.tooltip_label(metadata.label)
|
||||||
|
.tooltip_description(metadata.description.unwrap_or_default())
|
||||||
|
.on_update(move |_| to_message(variant));
|
||||||
|
|
||||||
|
if let Some(icon) = metadata.icon { entry.icon(icon) } else { entry }
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder)]
|
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder)]
|
||||||
#[derivative(Debug, PartialEq, Default)]
|
#[derivative(Debug, PartialEq, Default)]
|
||||||
@@ -344,6 +374,30 @@ pub struct RadioEntryData {
|
|||||||
pub on_commit: WidgetCallback<()>,
|
pub on_commit: WidgetCallback<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl RadioEntryData {
|
||||||
|
/// One entry per variant of a choice type enum, shown as the variant's icon when it has one and its text label otherwise.
|
||||||
|
pub fn list_from_choice_type<E>(to_message: impl Fn(E) -> Message + Clone + Send + Sync + 'static) -> Vec<Self>
|
||||||
|
where
|
||||||
|
E: graphene_std::choice_type::ChoiceTypeStatic + 'static,
|
||||||
|
{
|
||||||
|
E::list()
|
||||||
|
.iter()
|
||||||
|
.flat_map(|section| section.iter())
|
||||||
|
.map(|(variant, metadata)| {
|
||||||
|
let to_message = to_message.clone();
|
||||||
|
let variant = *variant;
|
||||||
|
|
||||||
|
let entry = RadioEntryData::new(metadata.name)
|
||||||
|
.tooltip_label(metadata.label)
|
||||||
|
.tooltip_description(metadata.description.unwrap_or_default())
|
||||||
|
.on_update(move |_| to_message(variant));
|
||||||
|
|
||||||
|
if let Some(icon) = metadata.icon { entry.icon(icon) } else { entry.label(metadata.label) }
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder)]
|
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder)]
|
||||||
#[derivative(Debug, PartialEq, Default)]
|
#[derivative(Debug, PartialEq, Default)]
|
||||||
@@ -520,7 +574,7 @@ pub struct ColorPresetsInput {
|
|||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum ColorPresetsInputUpdate {
|
pub enum ColorPresetsInputUpdate {
|
||||||
Preset(FillChoiceUI),
|
Preset(FillChoice<SRGBA8>),
|
||||||
EyedropperColorCode(String),
|
EyedropperColorCode(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,16 +585,28 @@ pub struct SpectrumInput {
|
|||||||
// Content
|
// Content
|
||||||
/// The colored gradient drawn behind the markers (display-only, caller-owned).
|
/// The colored gradient drawn behind the markers (display-only, caller-owned).
|
||||||
#[widget_builder(constructor)]
|
#[widget_builder(constructor)]
|
||||||
pub track: GradientStopsUI,
|
pub track: GradientStops<SRGBA8>,
|
||||||
/// CSS `linear-gradient(...)` string for the track strip's `background-image`. Auto-populated from `track` at layout-send time.
|
/// The color space the track's stops interpolate in, used to bake `track_samples`. Not sent to the frontend.
|
||||||
#[serde(rename = "trackCSS")]
|
#[serde(skip)]
|
||||||
|
pub track_space: GradientSpace,
|
||||||
|
/// Whether the track's stops wrap as a cycle, used to bake `track_samples` and by the frontend to draw the wrapped interval's midpoint diamond.
|
||||||
|
#[serde(rename = "trackCyclic")]
|
||||||
|
pub track_cyclic: bool,
|
||||||
|
/// The hue direction the track's stops interpolate with in a polar space, used to bake `track_samples`. Not sent to the frontend.
|
||||||
|
#[serde(skip)]
|
||||||
|
pub track_hue_direction: GradientHueDirection,
|
||||||
|
/// The path the track's stops interpolate along, used to bake `track_samples` and by the frontend to suppress the midpoint diamonds when stepped.
|
||||||
|
#[serde(rename = "trackInterpolation")]
|
||||||
|
pub track_interpolation: GradientInterpolation,
|
||||||
|
/// Straight-alpha samples the frontend draws as the stops of an SVG gradient filling the track strip. Auto-populated from `track` at layout-send time.
|
||||||
|
#[serde(rename = "trackSamples")]
|
||||||
#[widget_builder(skip)]
|
#[widget_builder(skip)]
|
||||||
pub track_css: String,
|
pub track_samples: Vec<SpectrumSample>,
|
||||||
/// Hex string for the track strip's leftmost solid-color end-cap. Auto-populated from `track`'s first stop.
|
/// Hex string for the track strip's leftmost solid-color end-cap. Auto-populated by evaluating `track` at position 0.
|
||||||
#[serde(rename = "trackStartCSS")]
|
#[serde(rename = "trackStartCSS")]
|
||||||
#[widget_builder(skip)]
|
#[widget_builder(skip)]
|
||||||
pub track_start_css: String,
|
pub track_start_css: String,
|
||||||
/// Hex string for the track strip's rightmost solid-color end-cap. Auto-populated from `track`'s last stop.
|
/// Hex string for the track strip's rightmost solid-color end-cap. Auto-populated by evaluating `track` at position 1.
|
||||||
#[serde(rename = "trackEndCSS")]
|
#[serde(rename = "trackEndCSS")]
|
||||||
#[widget_builder(skip)]
|
#[widget_builder(skip)]
|
||||||
pub track_end_css: String,
|
pub track_end_css: String,
|
||||||
@@ -579,22 +645,46 @@ pub struct SpectrumInput {
|
|||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct SpectrumMarker {
|
pub struct SpectrumMarker {
|
||||||
/// Position (0..1) of the marker along the spectrum track.
|
/// Position of the marker along the spectrum track, normally from 0 to 1. A shifted or stretched non-cyclic ramp can
|
||||||
|
/// place it outside that range, where the track draws only the markers falling within its visible span.
|
||||||
position: f64,
|
position: f64,
|
||||||
/// Position (0..1) of the midpoint between this marker and the next, used only if `show_midpoints` is true. The last marker's value is ignored.
|
/// Position (0..1) of the midpoint between this marker and the next, used only if `show_midpoints` is true.
|
||||||
|
/// The last marker's value controls the wrapped interval when `track_cyclic` is set, and is otherwise ignored.
|
||||||
midpoint: f64,
|
midpoint: f64,
|
||||||
/// CSS color string for the marker handle's fill. Set via `SpectrumMarker::new` from a linear [`Color`].
|
/// CSS color string for the marker handle's fill. Set via `SpectrumMarker::new` from a linear [`Color`],
|
||||||
|
/// discarding any transparency so the handle always shows the RGB that steers the interpolation.
|
||||||
#[serde(rename = "handleColorCSS")]
|
#[serde(rename = "handleColorCSS")]
|
||||||
handle_color_css: String,
|
handle_color_css: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SpectrumMarker {
|
impl SpectrumMarker {
|
||||||
pub fn new(position: f64, midpoint: f64, handle_color: Color) -> Self {
|
pub fn new(position: f64, midpoint: f64, handle_color: Color) -> Self {
|
||||||
let handle_color_css = SRGBA8::from(handle_color).to_css_hex();
|
let handle_color_css = format!("#{}", SRGBA8::from(handle_color).to_rgb_hex());
|
||||||
Self { position, midpoint, handle_color_css }
|
Self { position, midpoint, handle_color_css }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct SpectrumSample {
|
||||||
|
/// Position (0..1) of the sample along the spectrum track, drawn as the SVG stop's `offset`.
|
||||||
|
position: f64,
|
||||||
|
/// `#rrggbb` hex of the sample's color, drawn as the SVG stop's `stop-color`.
|
||||||
|
color: String,
|
||||||
|
/// Straight alpha (0..1) of the sample, drawn as the SVG stop's `stop-opacity`.
|
||||||
|
alpha: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpectrumSample {
|
||||||
|
pub fn new(position: f64, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
position,
|
||||||
|
color: format!("#{}", SRGBA8::from(color).to_rgb_hex()),
|
||||||
|
alpha: color.a(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum SpectrumInputUpdate {
|
pub enum SpectrumInputUpdate {
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ pub struct TextLabel {
|
|||||||
pub italic: bool,
|
pub italic: bool,
|
||||||
pub monospace: bool,
|
pub monospace: bool,
|
||||||
pub multiline: bool,
|
pub multiline: bool,
|
||||||
|
pub enquote: bool,
|
||||||
|
pub selectable: bool,
|
||||||
#[serde(rename = "centerAlign")]
|
#[serde(rename = "centerAlign")]
|
||||||
pub center_align: bool,
|
pub center_align: bool,
|
||||||
#[serde(rename = "tableAlign")]
|
#[serde(rename = "tableAlign")]
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ pub enum PathStep {
|
|||||||
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum VectorTableTab {
|
pub enum VectorTableTab {
|
||||||
#[default]
|
#[default]
|
||||||
Properties,
|
|
||||||
Points,
|
Points,
|
||||||
Segments,
|
Segments,
|
||||||
Regions,
|
Handles,
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -17,9 +17,9 @@ use super::utility_types::network_interface::storage_metadata::{StorageMetadataV
|
|||||||
#[derivative(Clone, Debug, Default)]
|
#[derivative(Clone, Debug, Default)]
|
||||||
pub struct DocumentHistory {
|
pub struct DocumentHistory {
|
||||||
/// Stack of document network snapshots for previous history states.
|
/// Stack of document network snapshots for previous history states.
|
||||||
lagacy_undo_stack: VecDeque<NodeNetworkInterface>,
|
legacy_undo_stack: VecDeque<NodeNetworkInterface>,
|
||||||
/// Stack of document network snapshots for future history states.
|
/// Stack of document network snapshots for future history states.
|
||||||
lagacy_redo_stack: VecDeque<NodeNetworkInterface>,
|
legacy_redo_stack: VecDeque<NodeNetworkInterface>,
|
||||||
/// The `Gdd` working copy: owns the CRDT `Session` and mirrors edits to disk. `None` until the mount
|
/// The `Gdd` working copy: owns the CRDT `Session` and mirrors edits to disk. `None` until the mount
|
||||||
/// future built by `load_document` resolves.
|
/// future built by `load_document` resolves.
|
||||||
#[derivative(Debug = "ignore")]
|
#[derivative(Debug = "ignore")]
|
||||||
@@ -31,38 +31,38 @@ impl DocumentHistory {
|
|||||||
|
|
||||||
/// Push a snapshot onto the undo stack, evicting the oldest entry past the history cap.
|
/// Push a snapshot onto the undo stack, evicting the oldest entry past the history cap.
|
||||||
pub fn push_undo(&mut self, snapshot: NodeNetworkInterface) {
|
pub fn push_undo(&mut self, snapshot: NodeNetworkInterface) {
|
||||||
Self::push_capped(&mut self.lagacy_undo_stack, snapshot);
|
Self::push_capped(&mut self.legacy_undo_stack, snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push a snapshot onto the redo stack, evicting the oldest entry past the history cap.
|
/// Push a snapshot onto the redo stack, evicting the oldest entry past the history cap.
|
||||||
pub fn push_redo(&mut self, snapshot: NodeNetworkInterface) {
|
pub fn push_redo(&mut self, snapshot: NodeNetworkInterface) {
|
||||||
Self::push_capped(&mut self.lagacy_redo_stack, snapshot);
|
Self::push_capped(&mut self.legacy_redo_stack, snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pop the most recent undo snapshot, or `None` when the stack is empty.
|
/// Pop the most recent undo snapshot, or `None` when the stack is empty.
|
||||||
pub fn pop_undo(&mut self) -> Option<NodeNetworkInterface> {
|
pub fn pop_undo(&mut self) -> Option<NodeNetworkInterface> {
|
||||||
self.lagacy_undo_stack.pop_back()
|
self.legacy_undo_stack.pop_back()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pop the most recent redo snapshot, or `None` when the stack is empty.
|
/// Pop the most recent redo snapshot, or `None` when the stack is empty.
|
||||||
pub fn pop_redo(&mut self) -> Option<NodeNetworkInterface> {
|
pub fn pop_redo(&mut self) -> Option<NodeNetworkInterface> {
|
||||||
self.lagacy_redo_stack.pop_back()
|
self.legacy_redo_stack.pop_back()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drop the most recently pushed undo snapshot (used to cancel a transaction that ended up unmodified).
|
/// Drop the most recently pushed undo snapshot (used to cancel a transaction that ended up unmodified).
|
||||||
pub fn discard_last_undo(&mut self) {
|
pub fn discard_last_undo(&mut self) {
|
||||||
self.lagacy_undo_stack.pop_back();
|
self.legacy_undo_stack.pop_back();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear the redo stack, called when a fresh edit invalidates the redo future.
|
/// Clear the redo stack, called when a fresh edit invalidates the redo future.
|
||||||
pub fn clear_redo(&mut self) {
|
pub fn clear_redo(&mut self) {
|
||||||
self.lagacy_redo_stack.clear();
|
self.legacy_redo_stack.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add the resources referenced by every snapshot in both history stacks into `resources`, so
|
/// Add the resources referenced by every snapshot in both history stacks into `resources`, so
|
||||||
/// history-only resources stay alive for legacy undo/redo.
|
/// history-only resources stay alive for legacy undo/redo.
|
||||||
pub fn collect_used_resources(&self, resources: &mut HashSet<ResourceId>) {
|
pub fn collect_used_resources(&self, resources: &mut HashSet<ResourceId>) {
|
||||||
for interface in self.lagacy_undo_stack.iter().chain(&self.lagacy_redo_stack) {
|
for interface in self.legacy_undo_stack.iter().chain(&self.legacy_redo_stack) {
|
||||||
interface.collect_used_resources(resources);
|
interface.collect_used_resources(resources);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::path::PathBuf;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use super::utility_types::misc::{GroupFolderType, SnappingState};
|
use super::utility_types::misc::{GroupFolderType, SnappingState};
|
||||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
use crate::messages::input_mapper::utility_types::keyboard::Key;
|
||||||
use crate::messages::portfolio::document::data_panel::DataPanelMessage;
|
use crate::messages::portfolio::document::data_panel::DataPanelMessage;
|
||||||
use crate::messages::portfolio::document::overlays::utility_types::{OverlayContext, OverlaysType};
|
use crate::messages::portfolio::document::overlays::utility_types::{OverlayContext, OverlaysType};
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
@@ -245,7 +245,7 @@ pub enum DocumentMessage {
|
|||||||
vector_data: HashMap<NodeId, Arc<Vector>>,
|
vector_data: HashMap<NodeId, Arc<Vector>>,
|
||||||
},
|
},
|
||||||
// `Message` is only serialized at `editor_wrapper.rs`, and only inputs from JS pass through it.
|
// `Message` is only serialized at `editor_wrapper.rs`, and only inputs from JS pass through it.
|
||||||
// `UpdateFillAttributes` and `UpdateStrokeAttributes` are produced inside `editor.handle_message` by `node_graph_executor.rs` and consumed in the same dispatch loop, so it never reaches that serialization point.
|
// `UpdateFillAttributes`/`UpdateStrokeAttributes` are produced inside `editor.handle_message` by `node_graph_executor.rs` and consumed in the same dispatch loop, so they never reach that serialization point.
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
UpdateFillAttributes {
|
UpdateFillAttributes {
|
||||||
fill_attributes: HashMap<NodeId, Arc<List<Graphic<'static>>>>,
|
fill_attributes: HashMap<NodeId, Arc<List<Graphic<'static>>>>,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use crate::consts::{
|
|||||||
ASYMPTOTIC_EFFECT, BLEND_COUNT_PER_LAYER, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, GDD_FILE_EXTENSION, LAYER_INDENT_OFFSET, NODE_CHAIN_WIDTH, SCALE_EFFECT, SCROLLBAR_SPACING,
|
ASYMPTOTIC_EFFECT, BLEND_COUNT_PER_LAYER, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, GDD_FILE_EXTENSION, LAYER_INDENT_OFFSET, NODE_CHAIN_WIDTH, SCALE_EFFECT, SCROLLBAR_SPACING,
|
||||||
VIEWPORT_ROTATE_SNAP_INTERVAL,
|
VIEWPORT_ROTATE_SNAP_INTERVAL,
|
||||||
};
|
};
|
||||||
|
use crate::messages::frontend::utility_types::FileFilter;
|
||||||
use crate::messages::input_mapper::utility_types::macros::action_shortcut;
|
use crate::messages::input_mapper::utility_types::macros::action_shortcut;
|
||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::portfolio::document::data_panel::{DataPanelMessageContext, DataPanelMessageHandler};
|
use crate::messages::portfolio::document::data_panel::{DataPanelMessageContext, DataPanelMessageHandler};
|
||||||
@@ -34,19 +35,19 @@ use crate::node_graph_executor::NodeGraphExecutor;
|
|||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graph_craft::application_io::resource::ResourceId;
|
use graph_craft::application_io::resource::ResourceId;
|
||||||
use graph_craft::application_io::wgpu_available;
|
use graph_craft::application_io::wgpu_available;
|
||||||
use graph_craft::descriptor;
|
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
|
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
|
||||||
use graphene_std::graphic::is_paint_present;
|
use graph_craft::list;
|
||||||
|
use graphene_std::Cover;
|
||||||
use graphene_std::math::quad::Quad;
|
use graphene_std::math::quad::Quad;
|
||||||
use graphene_std::path_bool_nodes::boolean_intersect;
|
use graphene_std::path_bool_nodes::boolean_intersect;
|
||||||
use graphene_std::raster::BlendMode;
|
use graphene_std::raster::BlendMode;
|
||||||
use graphene_std::subpath::Subpath;
|
use graphene_std::vector::algorithms::bezpath_algorithms::bezpath_is_inside_bezpath;
|
||||||
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
|
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
|
||||||
|
use graphene_std::vector::graphic_types;
|
||||||
use graphene_std::vector::misc::dvec2_to_point;
|
use graphene_std::vector::misc::dvec2_to_point;
|
||||||
use graphene_std::vector::style::RenderMode;
|
use graphene_std::vector::style::RenderMode;
|
||||||
use graphene_std::vector::{PointId, graphic_types};
|
use kurbo::{Affine, BezPath, Line, PathSeg, Shape};
|
||||||
use kurbo::{Affine, BezPath, Line, PathSeg};
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -103,6 +104,7 @@ pub struct DocumentMessageHandler {
|
|||||||
pub properties_panel_collapsed_sections: Vec<NodeId>,
|
pub properties_panel_collapsed_sections: Vec<NodeId>,
|
||||||
/// The full Git commit hash of the Graphite repository that was used to build the editor.
|
/// The full Git commit hash of the Graphite repository that was used to build the editor.
|
||||||
/// We save this to provide a hint about which version of the editor was used to create the document.
|
/// We save this to provide a hint about which version of the editor was used to create the document.
|
||||||
|
#[serde(skip_deserializing, default)]
|
||||||
pub commit_hash: String,
|
pub commit_hash: String,
|
||||||
/// The current pan, tilt, and zoom state of the viewport's view of the document canvas.
|
/// The current pan, tilt, and zoom state of the viewport's view of the document canvas.
|
||||||
pub document_ptz: PTZ,
|
pub document_ptz: PTZ,
|
||||||
@@ -125,7 +127,7 @@ pub struct DocumentMessageHandler {
|
|||||||
/// network path, the node itself, and its original relative gradient. The deferred migration removes each entry as its bake lands.
|
/// network path, the node itself, and its original relative gradient. The deferred migration removes each entry as its bake lands.
|
||||||
/// Transient migration state, but persisted in the saved document so unfinished bakes retry on the next open instead of losing placement.
|
/// Transient migration state, but persisted in the saved document so unfinished bakes retry on the next open instead of losing placement.
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
pub(crate) pending_gradient_bbox_bake: Vec<(Vec<NodeId>, NodeId, graphic_types::migrations::legacy::Gradient)>,
|
pub(crate) pending_gradient_bbox_bake: Vec<(Vec<NodeId>, NodeId, graphic_types::migrations::legacy::LegacyGradient)>,
|
||||||
|
|
||||||
// =============================================
|
// =============================================
|
||||||
// Fields omitted from the saved document format
|
// Fields omitted from the saved document format
|
||||||
@@ -298,7 +300,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
graph_operation_message_handler.process_message(message, responses, context);
|
graph_operation_message_handler.process_message(message, responses, context);
|
||||||
}
|
}
|
||||||
DocumentMessage::Resource(message) => {
|
DocumentMessage::Resource(message) => {
|
||||||
let context = ResourceMessageContext { document_id, fonts };
|
let context = ResourceMessageContext { document_id, fonts, resource_storage };
|
||||||
self.resources.process_message(message, responses, context);
|
self.resources.process_message(message, responses, context);
|
||||||
}
|
}
|
||||||
DocumentMessage::AlignSelectedLayers { axis, aggregate } => {
|
DocumentMessage::AlignSelectedLayers { axis, aggregate } => {
|
||||||
@@ -915,7 +917,14 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
}
|
}
|
||||||
responses.add(SelectToolMessage::Abort);
|
responses.add(SelectToolMessage::Abort);
|
||||||
responses.add(DocumentMessage::DocumentHistoryForward);
|
responses.add(DocumentMessage::DocumentHistoryForward);
|
||||||
responses.add(ToolMessage::Redo);
|
|
||||||
|
// The Pen tool advances its in-progress drawing to match the re-applied document once the graph has re-evaluated
|
||||||
|
if *current_tool == ToolType::Pen {
|
||||||
|
responses.add(DeferMessage::AfterGraphRun {
|
||||||
|
messages: vec![PenToolMessage::Redo.into()],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
responses.add(OverlaysMessage::Draw);
|
responses.add(OverlaysMessage::Draw);
|
||||||
responses.add(EventMessage::SelectionChanged);
|
responses.add(EventMessage::SelectionChanged);
|
||||||
}
|
}
|
||||||
@@ -1065,7 +1074,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
(true, Some(storage)) => storage
|
(true, Some(storage)) => storage
|
||||||
.export_to_bytes(
|
.export_to_bytes(
|
||||||
document_format::ExportFormat::Xz,
|
document_format::ExportFormat::Xz,
|
||||||
document_format::ExportOptions::default(),
|
document_format::ExportOptions {
|
||||||
|
include_history: false,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
export_load_handle.as_ref(),
|
export_load_handle.as_ref(),
|
||||||
Some(&legacy_document),
|
Some(&legacy_document),
|
||||||
)
|
)
|
||||||
@@ -1085,6 +1097,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
name,
|
name,
|
||||||
path,
|
path,
|
||||||
folder,
|
folder,
|
||||||
|
filters: vec![FileFilter {
|
||||||
|
name: "Graphite Document".into(),
|
||||||
|
extensions: vec![extension.into()],
|
||||||
|
}],
|
||||||
content: content.into(),
|
content: content.into(),
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -1502,9 +1518,9 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
.collect();
|
.collect();
|
||||||
self.network_interface.update_vector_data(layer_vector_data);
|
self.network_interface.update_vector_data(layer_vector_data);
|
||||||
}
|
}
|
||||||
DocumentMessage::UpdateFillAttributes { fill_attributes } => {
|
DocumentMessage::UpdateAppearanceAttributes { appearance_attributes } => {
|
||||||
// Convert NodeId keys to LayerNodeIdentifier keys, filtering to only layers
|
// Convert NodeId keys to LayerNodeIdentifier keys, filtering to only layers
|
||||||
let layer_fill_attributes = fill_attributes
|
let layer_appearance_attributes = appearance_attributes
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id))
|
.filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id))
|
||||||
.filter_map(|(node_id, attrs)| {
|
.filter_map(|(node_id, attrs)| {
|
||||||
@@ -1514,30 +1530,25 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
self.network_interface.update_fill_attributes(layer_fill_attributes);
|
self.network_interface.update_appearance_attributes(layer_appearance_attributes);
|
||||||
}
|
|
||||||
DocumentMessage::UpdateStrokeAttributes { stroke_attributes } => {
|
|
||||||
// Convert NodeId keys to LayerNodeIdentifier keys, filtering to only layers
|
|
||||||
let layer_stroke_attributes = stroke_attributes
|
|
||||||
.into_iter()
|
|
||||||
.filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id))
|
|
||||||
.filter_map(|(node_id, attrs)| {
|
|
||||||
self.network_interface.is_layer(&node_id, &[]).then(|| {
|
|
||||||
let layer = LayerNodeIdentifier::new(node_id, &self.network_interface);
|
|
||||||
(layer, attrs)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
self.network_interface.update_stroke_attributes(layer_stroke_attributes);
|
|
||||||
}
|
}
|
||||||
DocumentMessage::Undo => {
|
DocumentMessage::Undo => {
|
||||||
if self.network_interface.transaction_status() != TransactionStatus::Finished {
|
if self.network_interface.transaction_status() != TransactionStatus::Finished {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
responses.add(ToolMessage::PreUndo);
|
|
||||||
responses.add(DocumentMessage::DocumentHistoryBackward);
|
// The Pen tool keeps drawing across an undo, so instead of aborting beforehand it rewinds to match the reverted document once the graph has re-evaluated
|
||||||
|
if *current_tool == ToolType::Pen {
|
||||||
|
responses.add(DocumentMessage::DocumentHistoryBackward);
|
||||||
|
responses.add(DeferMessage::AfterGraphRun {
|
||||||
|
messages: vec![PenToolMessage::Undo.into()],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
responses.add(EventMessage::ToolAbort);
|
||||||
|
responses.add(DocumentMessage::DocumentHistoryBackward);
|
||||||
|
}
|
||||||
|
|
||||||
responses.add(OverlaysMessage::Draw);
|
responses.add(OverlaysMessage::Draw);
|
||||||
responses.add(ToolMessage::Undo);
|
|
||||||
responses.add(EventMessage::SelectionChanged);
|
responses.add(EventMessage::SelectionChanged);
|
||||||
}
|
}
|
||||||
DocumentMessage::UngroupSelectedLayers => {
|
DocumentMessage::UngroupSelectedLayers => {
|
||||||
@@ -1712,7 +1723,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
}
|
}
|
||||||
DocumentMessage::ZoomCanvasToFitAll => {
|
DocumentMessage::ZoomCanvasToFitAll => {
|
||||||
let bounds = if self.graph_view_overlay_open {
|
let bounds = if self.graph_view_overlay_open {
|
||||||
self.network_interface.all_nodes_bounding_box(&self.breadcrumb_network_path).cloned()
|
self.network_interface.all_nodes_bounding_box(&self.breadcrumb_network_path)
|
||||||
} else {
|
} else {
|
||||||
self.network_interface.document_bounds_document_space(true)
|
self.network_interface.document_bounds_document_space(true)
|
||||||
};
|
};
|
||||||
@@ -1844,16 +1855,16 @@ impl DocumentMessageHandler {
|
|||||||
self.intersect_quad(viewport_quad, viewport).filter(|layer| !self.network_interface.is_artboard(&layer.to_node(), &[]))
|
self.intersect_quad(viewport_quad, viewport).filter(|layer| !self.network_interface.is_artboard(&layer.to_node(), &[]))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs an intersection test with all layers and a viewport space subpath
|
/// Runs an intersection test with all layers and a viewport space polygon path
|
||||||
pub fn intersect_polygon<'a>(&'a self, mut viewport_polygon: Subpath<PointId>, viewport: &ViewportMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + use<'a> {
|
pub fn intersect_polygon<'a>(&'a self, mut viewport_polygon: BezPath, viewport: &ViewportMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + use<'a> {
|
||||||
let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz);
|
let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz);
|
||||||
viewport_polygon.apply_transform(document_to_viewport.inverse());
|
viewport_polygon.apply_affine(Affine::new(document_to_viewport.inverse().to_cols_array()));
|
||||||
|
|
||||||
ClickXRayIter::new(&self.network_interface, XRayTarget::Polygon(viewport_polygon))
|
ClickXRayIter::new(&self.network_interface, XRayTarget::Path(viewport_polygon))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs an intersection test with all layers and a viewport space subpath; ignoring artboards
|
/// Runs an intersection test with all layers and a viewport space polygon path; ignoring artboards
|
||||||
pub fn intersect_polygon_no_artboards<'a>(&'a self, viewport_polygon: Subpath<PointId>, viewport: &ViewportMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + use<'a> {
|
pub fn intersect_polygon_no_artboards<'a>(&'a self, viewport_polygon: BezPath, viewport: &ViewportMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + use<'a> {
|
||||||
self.intersect_polygon(viewport_polygon, viewport)
|
self.intersect_polygon(viewport_polygon, viewport)
|
||||||
.filter(|layer| !self.network_interface.is_artboard(&layer.to_node(), &[]))
|
.filter(|layer| !self.network_interface.is_artboard(&layer.to_node(), &[]))
|
||||||
}
|
}
|
||||||
@@ -1881,29 +1892,24 @@ impl DocumentMessageHandler {
|
|||||||
layer_left >= quad_left && layer_right <= quad_right && layer_top <= quad_top && layer_bottom >= quad_bottom
|
layer_left >= quad_left && layer_right <= quad_right && layer_top <= quad_top && layer_bottom >= quad_bottom
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_layer_fully_inside_polygon(&self, layer: &LayerNodeIdentifier, viewport: &ViewportMessageHandler, mut viewport_polygon: Subpath<PointId>) -> bool {
|
pub fn is_layer_fully_inside_polygon(&self, layer: &LayerNodeIdentifier, viewport: &ViewportMessageHandler, mut viewport_polygon: BezPath) -> bool {
|
||||||
let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz);
|
let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz);
|
||||||
viewport_polygon.apply_transform(document_to_viewport.inverse());
|
viewport_polygon.apply_affine(Affine::new(document_to_viewport.inverse().to_cols_array()));
|
||||||
|
|
||||||
let layer_click_targets = self.network_interface.document_metadata().click_targets(*layer);
|
let layer_click_targets = self.network_interface.document_metadata().click_targets(*layer);
|
||||||
let layer_transform = self.network_interface.document_metadata().transform_to_document(*layer);
|
let layer_transform = self.network_interface.document_metadata().transform_to_document(*layer);
|
||||||
|
|
||||||
layer_click_targets.is_some_and(|targets| {
|
layer_click_targets.is_some_and(|targets| {
|
||||||
targets.iter().all(|target| match target.target_type() {
|
targets.iter().all(|target| match target.target_type() {
|
||||||
ClickTargetType::Subpath(subpath) => {
|
ClickTargetType::Path(path) => {
|
||||||
let mut subpath = subpath.clone();
|
let mut path = path.clone();
|
||||||
subpath.apply_transform(layer_transform);
|
path.apply_affine(Affine::new(layer_transform.to_cols_array()));
|
||||||
subpath.is_inside_subpath(&viewport_polygon, None, None)
|
bezpath_is_inside_bezpath(&path, &viewport_polygon, None, None)
|
||||||
}
|
}
|
||||||
ClickTargetType::CompoundPath(subpaths) => subpaths.iter().all(|subpath| {
|
|
||||||
let mut subpath = subpath.clone();
|
|
||||||
subpath.apply_transform(layer_transform);
|
|
||||||
subpath.is_inside_subpath(&viewport_polygon, None, None)
|
|
||||||
}),
|
|
||||||
ClickTargetType::FreePoint(point) => {
|
ClickTargetType::FreePoint(point) => {
|
||||||
let mut point = *point;
|
let mut point = *point;
|
||||||
point.apply_transform(layer_transform);
|
point.apply_transform(layer_transform);
|
||||||
viewport_polygon.contains_point(point.position)
|
viewport_polygon.contains(dvec2_to_point(point.position))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -2414,22 +2420,26 @@ impl DocumentMessageHandler {
|
|||||||
self.drive_storage_undo_redo(document_id, resource_storage, legacy_applied, true, responses);
|
self.drive_storage_undo_redo(document_id, resource_storage, legacy_applied, true, responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn undo(&mut self, viewport: &ViewportMessageHandler, responses: &mut VecDeque<Message>) -> Option<NodeNetworkInterface> {
|
/// Installs a history snapshot as the active network interface, carrying over the current view state and structure load, and returns the replaced interface.
|
||||||
// If there is no history return and don't broadcast SelectionChanged
|
fn install_history_snapshot(&mut self, mut network_interface: NodeNetworkInterface, viewport: &ViewportMessageHandler) -> NodeNetworkInterface {
|
||||||
let mut network_interface = self.history.pop_undo()?;
|
|
||||||
|
|
||||||
// Set the previous network navigation metadata to the current navigation metadata
|
// Set the previous network navigation metadata to the current navigation metadata
|
||||||
network_interface.copy_all_navigation_metadata(&self.network_interface);
|
network_interface.copy_all_navigation_metadata(&self.network_interface);
|
||||||
std::mem::swap(&mut network_interface.resolved_types, &mut self.network_interface.resolved_types);
|
std::mem::swap(&mut network_interface.resolved_types, &mut self.network_interface.resolved_types);
|
||||||
|
|
||||||
//Update the metadata transform based on document PTZ
|
// Update the metadata transform based on document PTZ
|
||||||
let transform = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz);
|
let transform = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz);
|
||||||
network_interface.set_document_to_viewport_transform(transform);
|
network_interface.set_document_to_viewport_transform(transform);
|
||||||
|
|
||||||
// Ensure document structure is loaded so that updating the selected nodes has the correct metadata
|
// Ensure document structure is loaded so that updating the selected nodes has the correct metadata
|
||||||
network_interface.load_structure();
|
network_interface.load_structure();
|
||||||
|
|
||||||
let previous_network = std::mem::replace(&mut self.network_interface, network_interface);
|
std::mem::replace(&mut self.network_interface, network_interface)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn undo(&mut self, viewport: &ViewportMessageHandler, responses: &mut VecDeque<Message>) -> Option<NodeNetworkInterface> {
|
||||||
|
// If there is no history return and don't broadcast SelectionChanged
|
||||||
|
let network_interface = self.history.pop_undo()?;
|
||||||
|
let previous_network = self.install_history_snapshot(network_interface, viewport);
|
||||||
|
|
||||||
// Push the UpdateOpenDocumentsList message to the bus in order to update the save status of the open documents
|
// Push the UpdateOpenDocumentsList message to the bus in order to update the save status of the open documents
|
||||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||||
@@ -2454,17 +2464,9 @@ impl DocumentMessageHandler {
|
|||||||
|
|
||||||
pub fn redo(&mut self, viewport: &ViewportMessageHandler, responses: &mut VecDeque<Message>) -> Option<NodeNetworkInterface> {
|
pub fn redo(&mut self, viewport: &ViewportMessageHandler, responses: &mut VecDeque<Message>) -> Option<NodeNetworkInterface> {
|
||||||
// If there is no history return and don't broadcast SelectionChanged
|
// If there is no history return and don't broadcast SelectionChanged
|
||||||
let mut network_interface = self.history.pop_redo()?;
|
let network_interface = self.history.pop_redo()?;
|
||||||
|
let previous_network = self.install_history_snapshot(network_interface, viewport);
|
||||||
|
|
||||||
// Set the previous network navigation metadata to the current navigation metadata
|
|
||||||
network_interface.copy_all_navigation_metadata(&self.network_interface);
|
|
||||||
std::mem::swap(&mut network_interface.resolved_types, &mut self.network_interface.resolved_types);
|
|
||||||
|
|
||||||
//Update the metadata transform based on document PTZ
|
|
||||||
let transform = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz);
|
|
||||||
network_interface.set_document_to_viewport_transform(transform);
|
|
||||||
|
|
||||||
let previous_network = std::mem::replace(&mut self.network_interface, network_interface);
|
|
||||||
// Push the UpdateOpenDocumentsList message to the bus in order to update the save status of the open documents
|
// Push the UpdateOpenDocumentsList message to the bus in order to update the save status of the open documents
|
||||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||||
responses.add(NodeGraphMessage::SelectedNodesUpdated);
|
responses.add(NodeGraphMessage::SelectedNodesUpdated);
|
||||||
@@ -2617,7 +2619,11 @@ impl DocumentMessageHandler {
|
|||||||
|
|
||||||
// If there's already a boolean operation on the selected layer, update it with the new operation
|
// If there's already a boolean operation on the selected layer, update it with the new operation
|
||||||
if let (Some(upstream_boolean_op), Some(only_selected_layer)) = (upstream_boolean_op, only_selected_layer) {
|
if let (Some(upstream_boolean_op), Some(only_selected_layer)) = (upstream_boolean_op, only_selected_layer) {
|
||||||
network_interface.set_input(&InputConnector::node(upstream_boolean_op, 1), NodeInput::value(TaggedValue::BooleanOperation(operation), false), &[]);
|
network_interface.set_input(
|
||||||
|
&InputConnector::node(upstream_boolean_op, graphene_std::path_bool_nodes::boolean_operation::OperationInput),
|
||||||
|
NodeInput::value(TaggedValue::BooleanOperation(operation), false),
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
|
||||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
|
|
||||||
@@ -2748,7 +2754,7 @@ impl DocumentMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// For each selected layer, splits its fill and stroke into two stacked layers connected
|
/// For each selected layer, splits its fill and stroke into two stacked layers connected
|
||||||
/// to a shared `Solidify Stroke` node via two `Index Elements` nodes (indices 0 and 1).
|
/// to a shared `Solidify Stroke` node via two `Item at Index` nodes (indices 0 and 1).
|
||||||
/// Layers with only a stroke get just a `Solidify Stroke` added.
|
/// Layers with only a stroke get just a `Solidify Stroke` added.
|
||||||
/// Layers with only a fill, or neither, are left untouched.
|
/// Layers with only a fill, or neither, are left untouched.
|
||||||
fn handle_expand_fill_stroke_on_selected_layers(&mut self, responses: &mut VecDeque<Message>) {
|
fn handle_expand_fill_stroke_on_selected_layers(&mut self, responses: &mut VecDeque<Message>) {
|
||||||
@@ -2758,25 +2764,24 @@ impl DocumentMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let solidify_stroke_definition = document_node_definitions::resolve_proto_node_type(graphene_std::vector::solidify_stroke::IDENTIFIER).expect("Solidify Stroke node should exist");
|
let solidify_stroke_definition = document_node_definitions::resolve_proto_node_type(graphene_std::vector::solidify_stroke::IDENTIFIER).expect("Solidify Stroke node should exist");
|
||||||
let index_elements_definition = document_node_definitions::resolve_proto_node_type(graphene_std::graphic::index_elements::IDENTIFIER).expect("Index Elements node should exist");
|
let item_at_index_definition = document_node_definitions::resolve_proto_node_type(graphene_std::graphic::item_at_index::IDENTIFIER).expect("Item at Index node should exist");
|
||||||
|
|
||||||
let mut resulting_layers: Vec<NodeId> = Vec::new();
|
let mut resulting_layers: Vec<NodeId> = Vec::new();
|
||||||
|
|
||||||
for layer in selected_layers {
|
for layer in selected_layers {
|
||||||
let Some(vector_data) = self.network_interface.document_metadata().layer_vector_data.get(&layer) else {
|
if !self.network_interface.document_metadata().layer_vector_data.contains_key(&layer) {
|
||||||
resulting_layers.push(layer.to_node());
|
resulting_layers.push(layer.to_node());
|
||||||
continue;
|
continue;
|
||||||
};
|
}
|
||||||
let stroke = vector_data.stroke.as_ref();
|
|
||||||
|
|
||||||
let fill_graphic_list = self.network_interface.document_metadata().layer_fill_attributes.get(&layer);
|
let appearance = self.network_interface.document_metadata().layer_appearance_attributes.get(&layer);
|
||||||
let stroke_graphic_list = self.network_interface.document_metadata().layer_stroke_attributes.get(&layer);
|
|
||||||
|
|
||||||
let has_fill = fill_graphic_list.is_some_and(|list| is_paint_present(list));
|
let has_fill = appearance.is_some_and(|appearance| appearance.has_painted_cover(Cover::Fill));
|
||||||
// `Vector.stroke` captures stroke geometry, even with weight 0 or transparent paint.
|
// A visible stroke needs both renderable geometry (non-zero weight) and paint that draws something
|
||||||
// So stroke visibility must be checked from `ATTR_STROKE`, the paint source of truth.
|
let has_stroke = appearance.is_some_and(|appearance| {
|
||||||
let stroke_visible = stroke_graphic_list.is_some_and(|list| list.element(0).is_some_and(|g| !g.is_fully_transparent()));
|
appearance.first_coverage_of(Cover::Stroke).is_some_and(|coverage| coverage.stroke_params().has_renderable_stroke())
|
||||||
let has_stroke = stroke.as_ref().is_some_and(|s| s.has_renderable_stroke()) && stroke_visible;
|
&& appearance.first_paint_of(Cover::Stroke).is_some_and(|paint| !paint.is_guaranteed_fully_transparent())
|
||||||
|
});
|
||||||
|
|
||||||
// No stroke means there's nothing to solidify. Fill-only layers are already in the desired form, so skip.
|
// No stroke means there's nothing to solidify. Fill-only layers are already in the desired form, so skip.
|
||||||
if !has_stroke {
|
if !has_stroke {
|
||||||
@@ -2791,7 +2796,7 @@ impl DocumentMessageHandler {
|
|||||||
if has_fill && has_stroke {
|
if has_fill && has_stroke {
|
||||||
let (existing_index, new_index) = (0_f64, 1_f64);
|
let (existing_index, new_index) = (0_f64, 1_f64);
|
||||||
|
|
||||||
let existing_index_template = index_elements_definition.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(existing_index), false))]);
|
let existing_index_template = item_at_index_definition.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(existing_index), false))]);
|
||||||
let existing_index_id = NodeId::new();
|
let existing_index_id = NodeId::new();
|
||||||
self.network_interface.insert_node(existing_index_id, existing_index_template, &[]);
|
self.network_interface.insert_node(existing_index_id, existing_index_template, &[]);
|
||||||
self.network_interface.move_node_to_chain_start(&existing_index_id, layer, &[], false);
|
self.network_interface.move_node_to_chain_start(&existing_index_id, layer, &[], false);
|
||||||
@@ -2813,12 +2818,13 @@ impl DocumentMessageHandler {
|
|||||||
self.network_interface.set_display_name(&new_layer_id, original_name, &[]);
|
self.network_interface.set_display_name(&new_layer_id, original_name, &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_index_template = index_elements_definition.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(new_index), false))]);
|
let new_index_template = item_at_index_definition.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(new_index), false))]);
|
||||||
let new_index_id = NodeId::new();
|
let new_index_id = NodeId::new();
|
||||||
self.network_interface.insert_node(new_index_id, new_index_template, &[]);
|
self.network_interface.insert_node(new_index_id, new_index_template, &[]);
|
||||||
self.network_interface.move_node_to_chain_start(&new_index_id, new_layer, &[], false);
|
self.network_interface.move_node_to_chain_start(&new_index_id, new_layer, &[], false);
|
||||||
|
|
||||||
self.network_interface.create_wire(&OutputConnector::node(solidify_id, 0), &InputConnector::node(new_index_id, 0), &[]);
|
self.network_interface
|
||||||
|
.create_wire(&OutputConnector::primary_output(solidify_id), &InputConnector::primary_input(new_index_id), &[]);
|
||||||
|
|
||||||
resulting_layers.push(layer.to_node());
|
resulting_layers.push(layer.to_node());
|
||||||
resulting_layers.push(new_layer.to_node());
|
resulting_layers.push(new_layer.to_node());
|
||||||
@@ -3408,6 +3414,11 @@ impl DocumentMessageHandler {
|
|||||||
let selected_nodes = self.network_interface.selected_nodes();
|
let selected_nodes = self.network_interface.selected_nodes();
|
||||||
let selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&self.network_interface);
|
let selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&self.network_interface);
|
||||||
|
|
||||||
|
// A layer whose chain cannot carry blending nodes has nowhere to put the value, so it disqualifies the whole selection
|
||||||
|
let all_layers_support_blending = selected_nodes
|
||||||
|
.selected_layers_except_artboards(&self.network_interface)
|
||||||
|
.all(|layer| self.network_interface.layer_hosts_blending_nodes(&layer.to_node(), &[]));
|
||||||
|
|
||||||
// Look up the current opacity and blend mode of the selected layers (if any), and split the iterator into the first tuple and the rest.
|
// Look up the current opacity and blend mode of the selected layers (if any), and split the iterator into the first tuple and the rest.
|
||||||
let mut blending_options = selected_layers_except_artboards.map(|layer| {
|
let mut blending_options = selected_layers_except_artboards.map(|layer| {
|
||||||
(
|
(
|
||||||
@@ -3419,8 +3430,8 @@ impl DocumentMessageHandler {
|
|||||||
let first_blending_options = blending_options.next();
|
let first_blending_options = blending_options.next();
|
||||||
let result_blending_options = blending_options;
|
let result_blending_options = blending_options;
|
||||||
|
|
||||||
// If there are no selected layers, disable the opacity and blend mode widgets.
|
// If there are no selected layers, or any of them cannot host the nodes, disable the opacity and blend mode widgets.
|
||||||
let disabled = first_blending_options.is_none();
|
let disabled = first_blending_options.is_none() || !all_layers_support_blending;
|
||||||
|
|
||||||
// Amongst the selected layers, check if the opacities and blend modes are identical across all layers.
|
// Amongst the selected layers, check if the opacities and blend modes are identical across all layers.
|
||||||
// The result is setting `option` and `blend_mode` to Some value if all their values are identical, or None if they are not.
|
// The result is setting `option` and `blend_mode` to Some value if all their values are identical, or None if they are not.
|
||||||
@@ -3583,7 +3594,7 @@ impl DocumentMessageHandler {
|
|||||||
// Showing only compatible types for the layer based on the output type of the node upstream from its horizontal input
|
// Showing only compatible types for the layer based on the output type of the node upstream from its horizontal input
|
||||||
let compatible_type = selected_layer.and_then(|layer| {
|
let compatible_type = selected_layer.and_then(|layer| {
|
||||||
self.network_interface
|
self.network_interface
|
||||||
.upstream_output_connector(&InputConnector::node(layer.to_node(), 1), &[])
|
.upstream_output_connector(&InputConnector::layer_secondary_input(layer.to_node()), &[])
|
||||||
.and_then(|upstream_output| self.network_interface.output_type(&upstream_output, &[]).add_node_string())
|
.and_then(|upstream_output| self.network_interface.output_type(&upstream_output, &[]).add_node_string())
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3789,7 +3800,7 @@ impl DocumentMessageHandler {
|
|||||||
/// Create a network interface with a single export
|
/// Create a network interface with a single export
|
||||||
fn default_document_network_interface() -> NodeNetworkInterface {
|
fn default_document_network_interface() -> NodeNetworkInterface {
|
||||||
let mut network_interface = NodeNetworkInterface::default();
|
let mut network_interface = NodeNetworkInterface::default();
|
||||||
network_interface.add_export(TaggedValue::TypeDefault(descriptor!(graphene_std::list::List<graphene_std::Artboard>)), -1, "", &[]);
|
network_interface.add_export(TaggedValue::TypeDefault(list!(graphene_std::Artboard)), -1, "", &[]);
|
||||||
network_interface
|
network_interface
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3799,7 +3810,6 @@ enum XRayTarget {
|
|||||||
Point(DVec2),
|
Point(DVec2),
|
||||||
Quad(Quad),
|
Quad(Quad),
|
||||||
Path(BezPath),
|
Path(BezPath),
|
||||||
Polygon(Subpath<PointId>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The result for the [`ClickXRayIter`] on the layer
|
/// The result for the [`ClickXRayIter`] on the layer
|
||||||
@@ -3822,13 +3832,7 @@ fn quad_to_kurbo(quad: Quad) -> BezPath {
|
|||||||
|
|
||||||
fn click_targets_to_kurbo<'a>(click_targets: impl Iterator<Item = &'a ClickTarget>, transform: DAffine2) -> BezPath {
|
fn click_targets_to_kurbo<'a>(click_targets: impl Iterator<Item = &'a ClickTarget>, transform: DAffine2) -> BezPath {
|
||||||
let segments = click_targets
|
let segments = click_targets
|
||||||
.filter_map(|target| {
|
.filter_map(|target| if let ClickTargetType::Path(path) = target.target_type() { Some(path.segments()) } else { None })
|
||||||
if let ClickTargetType::Subpath(subpath) = target.target_type() {
|
|
||||||
Some(subpath.iter())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(|bezier| Affine::new(transform.to_cols_array()) * bezier);
|
.map(|bezier| Affine::new(transform.to_cols_array()) * bezier);
|
||||||
BezPath::from_path_segments(segments)
|
BezPath::from_path_segments(segments)
|
||||||
@@ -3899,10 +3903,6 @@ impl<'a> ClickXRayIter<'a> {
|
|||||||
}
|
}
|
||||||
XRayTarget::Quad(quad) => self.check_layer_area_target(click_targets, clip, layer, quad_to_kurbo(*quad), transform),
|
XRayTarget::Quad(quad) => self.check_layer_area_target(click_targets, clip, layer, quad_to_kurbo(*quad), transform),
|
||||||
XRayTarget::Path(path) => self.check_layer_area_target(click_targets, clip, layer, path.clone(), transform),
|
XRayTarget::Path(path) => self.check_layer_area_target(click_targets, clip, layer, path.clone(), transform),
|
||||||
XRayTarget::Polygon(polygon) => {
|
|
||||||
let polygon = BezPath::from_path_segments(polygon.iter_closed());
|
|
||||||
self.check_layer_area_target(click_targets, clip, layer, polygon, transform)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4007,7 +4007,7 @@ mod document_message_handler_tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn pending_gradient_bakes_round_trip_through_serialization() {
|
fn pending_gradient_bakes_round_trip_through_serialization() {
|
||||||
let document = DocumentMessageHandler {
|
let document = DocumentMessageHandler {
|
||||||
pending_gradient_bbox_bake: vec![(vec![NodeId(7)], NodeId(42), graphic_types::migrations::legacy::Gradient::default())],
|
pending_gradient_bbox_bake: vec![(vec![NodeId(7)], NodeId(42), graphic_types::migrations::legacy::LegacyGradient::default())],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -4318,4 +4318,40 @@ mod document_message_handler_tests {
|
|||||||
Dist: {distance} (should be < 1)"
|
Dist: {distance} (should be < 1)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Grouping choreography transiently disconnects the stack wire, and the stored default for that connector
|
||||||
|
// must stay an empty list rather than any value which materializes as a one-element phantom in the stack
|
||||||
|
#[tokio::test]
|
||||||
|
async fn grouping_adds_no_phantom_element_to_the_stack() {
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||||
|
|
||||||
|
editor
|
||||||
|
.handle_message(DocumentMessage::GroupSelectedLayers {
|
||||||
|
group_folder_type: GroupFolderType::Layer,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let instrumented = editor.eval_graph().await.unwrap();
|
||||||
|
|
||||||
|
// The emptiness guards keep these assertions honest: a wrong `Output` type on `grab_all_input` yields no records at all, which would otherwise pass without checking anything
|
||||||
|
let base_lengths: Vec<usize> = instrumented
|
||||||
|
.grab_all_input::<graphene_std::graphic::extend::BaseInput, graphene_std::list::List<graphene_std::Graphic>>(&editor.runtime)
|
||||||
|
.map(|base| base.len())
|
||||||
|
.collect();
|
||||||
|
assert!(!base_lengths.is_empty(), "Instrumentation should have recorded at least one stack base");
|
||||||
|
assert!(base_lengths.iter().all(|&len| len == 0), "Every stack base should be empty, found lengths {base_lengths:?}");
|
||||||
|
|
||||||
|
let news: Vec<graphene_std::list::List<graphene_std::Graphic>> = instrumented
|
||||||
|
.grab_all_input::<graphene_std::graphic::extend::NewInput, graphene_std::list::List<graphene_std::Graphic>>(&editor.runtime)
|
||||||
|
.collect();
|
||||||
|
assert!(!news.is_empty(), "Instrumentation should have recorded at least one stacked element list");
|
||||||
|
let phantom_count = news
|
||||||
|
.iter()
|
||||||
|
.flat_map(|new| new.iter_element_values())
|
||||||
|
.filter(|graphic| matches!(graphic, graphene_std::Graphic::None(_)))
|
||||||
|
.count();
|
||||||
|
assert_eq!(phantom_count, 0, "No stacked element should be a phantom None graphic");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ use crate::messages::prelude::*;
|
|||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
use graphene_std::brush::brush_stroke::BrushStroke;
|
|
||||||
use graphene_std::raster::BlendMode;
|
use graphene_std::raster::BlendMode;
|
||||||
use graphene_std::raster_types::Image;
|
use graphene_std::raster_types::Image;
|
||||||
use graphene_std::subpath::Subpath;
|
|
||||||
use graphene_std::text::{Font, TypesettingConfig};
|
use graphene_std::text::{Font, TypesettingConfig};
|
||||||
use graphene_std::vector::style::{GradientSpreadMethod, GradientType, Stroke};
|
use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace, GradientSpread, PaintOrder, Stroke};
|
||||||
use graphene_std::vector::{GradientStops, PointId, VectorModificationType};
|
use graphene_std::vector::{Gradient, VectorModificationType};
|
||||||
|
|
||||||
#[impl_message(Message, DocumentMessage, GraphOperation)]
|
#[impl_message(Message, DocumentMessage, GraphOperation)]
|
||||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
@@ -20,11 +18,16 @@ pub enum GraphOperationMessage {
|
|||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
color: Option<Color>,
|
color: Option<Color>,
|
||||||
},
|
},
|
||||||
|
ColorValueSet {
|
||||||
|
layer: LayerNodeIdentifier,
|
||||||
|
color: Color,
|
||||||
|
},
|
||||||
FillGradientSet {
|
FillGradientSet {
|
||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
gradient: GradientStops,
|
#[serde(skip)]
|
||||||
gradient_type: GradientType,
|
gradient: Gradient,
|
||||||
spread_method: GradientSpreadMethod,
|
gradient_form: GradientForm,
|
||||||
|
gradient_settings: GradientSettings,
|
||||||
transform: DAffine2,
|
transform: DAffine2,
|
||||||
},
|
},
|
||||||
BlendingFillSet {
|
BlendingFillSet {
|
||||||
@@ -33,19 +36,44 @@ pub enum GraphOperationMessage {
|
|||||||
},
|
},
|
||||||
GradientStopsSet {
|
GradientStopsSet {
|
||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
stops: GradientStops,
|
#[serde(skip)]
|
||||||
|
stops: Gradient,
|
||||||
|
},
|
||||||
|
GradientPositionsSet {
|
||||||
|
layer: LayerNodeIdentifier,
|
||||||
|
positions: Vec<f64>,
|
||||||
|
},
|
||||||
|
GradientMidpointsSet {
|
||||||
|
layer: LayerNodeIdentifier,
|
||||||
|
midpoints: Vec<f64>,
|
||||||
},
|
},
|
||||||
GradientTransformSet {
|
GradientTransformSet {
|
||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
transform: DAffine2,
|
transform: DAffine2,
|
||||||
},
|
},
|
||||||
GradientTypeSet {
|
GradientFormSet {
|
||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
gradient_type: GradientType,
|
gradient_form: GradientForm,
|
||||||
},
|
},
|
||||||
GradientSpreadMethodSet {
|
GradientSpreadSet {
|
||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
spread_method: GradientSpreadMethod,
|
gradient_spread: GradientSpread,
|
||||||
|
},
|
||||||
|
GradientSpaceSet {
|
||||||
|
layer: LayerNodeIdentifier,
|
||||||
|
gradient_space: GradientSpace,
|
||||||
|
},
|
||||||
|
GradientCyclicSet {
|
||||||
|
layer: LayerNodeIdentifier,
|
||||||
|
gradient_cyclic: bool,
|
||||||
|
},
|
||||||
|
GradientHueDirectionSet {
|
||||||
|
layer: LayerNodeIdentifier,
|
||||||
|
gradient_hue_direction: GradientHueDirection,
|
||||||
|
},
|
||||||
|
GradientInterpolationSet {
|
||||||
|
layer: LayerNodeIdentifier,
|
||||||
|
gradient_interpolation: GradientInterpolation,
|
||||||
},
|
},
|
||||||
OpacitySet {
|
OpacitySet {
|
||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
@@ -63,6 +91,10 @@ pub enum GraphOperationMessage {
|
|||||||
color: Option<Color>,
|
color: Option<Color>,
|
||||||
stroke: Stroke,
|
stroke: Stroke,
|
||||||
},
|
},
|
||||||
|
StrokeOrderSet {
|
||||||
|
layer: LayerNodeIdentifier,
|
||||||
|
paint_order: PaintOrder,
|
||||||
|
},
|
||||||
TransformChange {
|
TransformChange {
|
||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
transform: DAffine2,
|
transform: DAffine2,
|
||||||
@@ -79,9 +111,23 @@ pub enum GraphOperationMessage {
|
|||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
modification_type: VectorModificationType,
|
modification_type: VectorModificationType,
|
||||||
},
|
},
|
||||||
Brush {
|
NewBrushGroupLayer {
|
||||||
|
id: NodeId,
|
||||||
|
strokes_node_id: NodeId,
|
||||||
|
parent: LayerNodeIdentifier,
|
||||||
|
insert_index: usize,
|
||||||
|
color: Color,
|
||||||
|
diameter: f64,
|
||||||
|
hardness: f64,
|
||||||
|
flow: f64,
|
||||||
|
},
|
||||||
|
NewBrushStrokesNode {
|
||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
strokes: Vec<BrushStroke>,
|
strokes_node_id: NodeId,
|
||||||
|
color: Color,
|
||||||
|
diameter: f64,
|
||||||
|
hardness: f64,
|
||||||
|
flow: f64,
|
||||||
},
|
},
|
||||||
SetUpstreamToChain {
|
SetUpstreamToChain {
|
||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
@@ -128,12 +174,6 @@ pub enum GraphOperationMessage {
|
|||||||
parent: LayerNodeIdentifier,
|
parent: LayerNodeIdentifier,
|
||||||
insert_index: usize,
|
insert_index: usize,
|
||||||
},
|
},
|
||||||
NewVectorLayer {
|
|
||||||
id: NodeId,
|
|
||||||
subpaths: Vec<Subpath<PointId>>,
|
|
||||||
parent: LayerNodeIdentifier,
|
|
||||||
insert_index: usize,
|
|
||||||
},
|
|
||||||
NewTextLayer {
|
NewTextLayer {
|
||||||
id: NodeId,
|
id: NodeId,
|
||||||
text: String,
|
text: String,
|
||||||
|
|||||||
+495
-93
@@ -1,19 +1,20 @@
|
|||||||
use super::transform_utils;
|
use super::transform_utils;
|
||||||
use super::utility_types::ModifyInputsContext;
|
use super::utility_types::{ModifyInputsContext, set_stroke_paint_order};
|
||||||
use crate::consts::{LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP};
|
use crate::consts::{LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP};
|
||||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||||
|
use crate::messages::portfolio::document::node_graph::document_node_definitions::{BLEND_PATH_INPUT_INDEX, DefinitionIdentifier, resolve_proto_node_type};
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector};
|
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector};
|
||||||
use crate::messages::portfolio::document::utility_types::nodes::CollapsedLayers;
|
use crate::messages::portfolio::document::utility_types::nodes::CollapsedLayers;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode;
|
use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode;
|
||||||
use glam::{DAffine2, DVec2, IVec2};
|
use glam::{DAffine2, DVec2, IVec2};
|
||||||
use graph_craft::descriptor;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{NodeId, NodeInput};
|
use graph_craft::document::{NodeId, NodeInput};
|
||||||
use graphene_std::list::List;
|
use graph_craft::list;
|
||||||
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
|
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
|
||||||
use graphene_std::text::{Font, TypesettingConfig};
|
use graphene_std::text::{Font, TypesettingConfig};
|
||||||
use graphene_std::vector::style::{GradientSpreadMethod, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
use graphene_std::vector::style::{Gradient, GradientForm, GradientSettings, GradientSpace, GradientSpread, GradientStop, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
use graphene_std::{Artboard, Color};
|
use graphene_std::{Artboard, Color};
|
||||||
|
|
||||||
#[derive(ExtractField)]
|
#[derive(ExtractField)]
|
||||||
@@ -39,15 +40,20 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
modify_inputs.fill_color_set(color);
|
modify_inputs.fill_color_set(color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
GraphOperationMessage::ColorValueSet { layer, color } => {
|
||||||
|
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||||
|
modify_inputs.color_value_set(color);
|
||||||
|
}
|
||||||
|
}
|
||||||
GraphOperationMessage::FillGradientSet {
|
GraphOperationMessage::FillGradientSet {
|
||||||
layer,
|
layer,
|
||||||
gradient,
|
gradient,
|
||||||
gradient_type,
|
gradient_form,
|
||||||
spread_method,
|
gradient_settings,
|
||||||
transform,
|
transform,
|
||||||
} => {
|
} => {
|
||||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||||
modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform);
|
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_settings, transform);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GraphOperationMessage::BlendingFillSet { layer, fill } => {
|
GraphOperationMessage::BlendingFillSet { layer, fill } => {
|
||||||
@@ -60,19 +66,49 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
modify_inputs.gradient_stops_set(stops);
|
modify_inputs.gradient_stops_set(stops);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
GraphOperationMessage::GradientPositionsSet { layer, positions } => {
|
||||||
|
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||||
|
modify_inputs.gradient_positions_set(positions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GraphOperationMessage::GradientMidpointsSet { layer, midpoints } => {
|
||||||
|
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||||
|
modify_inputs.gradient_midpoints_set(midpoints);
|
||||||
|
}
|
||||||
|
}
|
||||||
GraphOperationMessage::GradientTransformSet { layer, transform } => {
|
GraphOperationMessage::GradientTransformSet { layer, transform } => {
|
||||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||||
modify_inputs.gradient_transform_set(transform);
|
modify_inputs.gradient_transform_set(transform);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GraphOperationMessage::GradientTypeSet { layer, gradient_type } => {
|
GraphOperationMessage::GradientFormSet { layer, gradient_form } => {
|
||||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||||
modify_inputs.gradient_type_set(gradient_type);
|
modify_inputs.gradient_form_set(gradient_form);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GraphOperationMessage::GradientSpreadMethodSet { layer, spread_method } => {
|
GraphOperationMessage::GradientSpreadSet { layer, gradient_spread } => {
|
||||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||||
modify_inputs.gradient_spread_method_set(spread_method);
|
modify_inputs.gradient_spread_set(gradient_spread);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GraphOperationMessage::GradientSpaceSet { layer, gradient_space } => {
|
||||||
|
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||||
|
modify_inputs.gradient_space_set(gradient_space);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GraphOperationMessage::GradientCyclicSet { layer, gradient_cyclic } => {
|
||||||
|
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||||
|
modify_inputs.gradient_cyclic_set(gradient_cyclic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GraphOperationMessage::GradientHueDirectionSet { layer, gradient_hue_direction } => {
|
||||||
|
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||||
|
modify_inputs.gradient_hue_direction_set(gradient_hue_direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GraphOperationMessage::GradientInterpolationSet { layer, gradient_interpolation } => {
|
||||||
|
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||||
|
modify_inputs.gradient_interpolation_set(gradient_interpolation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GraphOperationMessage::OpacitySet { layer, opacity } => {
|
GraphOperationMessage::OpacitySet { layer, opacity } => {
|
||||||
@@ -96,6 +132,17 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
modify_inputs.stroke_set(color, stroke);
|
modify_inputs.stroke_set(color, stroke);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
GraphOperationMessage::StrokeOrderSet { layer, paint_order } => {
|
||||||
|
let stroke_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER);
|
||||||
|
let Some(stroke_node_id) = ModifyInputsContext::locate_node_in_layer_chain(&stroke_reference, layer, network_interface) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if set_stroke_paint_order(network_interface, &[], stroke_node_id, paint_order) {
|
||||||
|
responses.add(PropertiesPanelMessage::Refresh);
|
||||||
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
|
responses.add(NodeGraphMessage::SendGraph);
|
||||||
|
}
|
||||||
|
}
|
||||||
GraphOperationMessage::TransformChange {
|
GraphOperationMessage::TransformChange {
|
||||||
layer,
|
layer,
|
||||||
transform,
|
transform,
|
||||||
@@ -126,13 +173,38 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
modify_inputs.vector_modify(modification_type);
|
modify_inputs.vector_modify(modification_type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GraphOperationMessage::Brush { layer, strokes } => {
|
GraphOperationMessage::NewBrushGroupLayer {
|
||||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
id,
|
||||||
modify_inputs.brush_modify(strokes);
|
strokes_node_id,
|
||||||
}
|
parent,
|
||||||
|
insert_index,
|
||||||
|
color,
|
||||||
|
diameter,
|
||||||
|
hardness,
|
||||||
|
flow,
|
||||||
|
} => {
|
||||||
|
let layer = ModifyInputsContext::new(network_interface, responses).create_layer(id);
|
||||||
|
insert_brush_strokes_chain(network_interface, layer, strokes_node_id, color, diameter, hardness, flow);
|
||||||
|
|
||||||
|
responses.add(NodeGraphMessage::MoveLayerToStack { layer, parent, insert_index });
|
||||||
|
responses.add(GraphOperationMessage::SetUpstreamToChain { layer });
|
||||||
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
|
}
|
||||||
|
GraphOperationMessage::NewBrushStrokesNode {
|
||||||
|
layer,
|
||||||
|
strokes_node_id,
|
||||||
|
color,
|
||||||
|
diameter,
|
||||||
|
hardness,
|
||||||
|
flow,
|
||||||
|
} => {
|
||||||
|
insert_brush_strokes_chain(network_interface, layer, strokes_node_id, color, diameter, hardness, flow);
|
||||||
|
|
||||||
|
responses.add(GraphOperationMessage::SetUpstreamToChain { layer });
|
||||||
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
}
|
}
|
||||||
GraphOperationMessage::SetUpstreamToChain { layer } => {
|
GraphOperationMessage::SetUpstreamToChain { layer } => {
|
||||||
let Some(OutputConnector::Node { node_id: first_chain_node, .. }) = network_interface.upstream_output_connector(&InputConnector::node(layer.to_node(), 1), &[]) else {
|
let Some(OutputConnector::Node { node_id: first_chain_node, .. }) = network_interface.upstream_output_connector(&InputConnector::layer_secondary_input(layer.to_node()), &[]) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -180,16 +252,16 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set the bottom input of the artboard back to artboard
|
// Set the bottom input of the artboard back to artboard
|
||||||
let bottom_input = NodeInput::type_default(descriptor!(List<Artboard>), true);
|
let bottom_input = NodeInput::type_default(list!(Artboard), true);
|
||||||
network_interface.set_input(&InputConnector::node(artboard_layer.to_node(), 0), bottom_input, &[]);
|
network_interface.set_input(&InputConnector::primary_input(artboard_layer.to_node()), bottom_input, &[]);
|
||||||
} else {
|
} else {
|
||||||
// We have some non layers (e.g. just a rectangle node). We disconnect the bottom input and connect it to the left input.
|
// We have some non layers (e.g. just a rectangle node). We disconnect the bottom input and connect it to the left input.
|
||||||
network_interface.disconnect_input(&InputConnector::node(artboard_layer.to_node(), 0), &[]);
|
network_interface.disconnect_input(&InputConnector::primary_input(artboard_layer.to_node()), &[]);
|
||||||
network_interface.set_input(&InputConnector::node(artboard_layer.to_node(), 1), primary_input, &[]);
|
network_interface.set_input(&InputConnector::layer_secondary_input(artboard_layer.to_node()), primary_input, &[]);
|
||||||
|
|
||||||
// Set the bottom input of the artboard back to artboard
|
// Set the bottom input of the artboard back to artboard
|
||||||
let bottom_input = NodeInput::type_default(descriptor!(List<Artboard>), true);
|
let bottom_input = NodeInput::type_default(list!(Artboard), true);
|
||||||
network_interface.set_input(&InputConnector::node(artboard_layer.to_node(), 0), bottom_input, &[]);
|
network_interface.set_input(&InputConnector::primary_input(artboard_layer.to_node()), bottom_input, &[]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
responses.add_front(NodeGraphMessage::SelectedNodesSet { nodes: vec![id] });
|
responses.add_front(NodeGraphMessage::SelectedNodesSet { nodes: vec![id] });
|
||||||
@@ -212,11 +284,14 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
|
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
|
||||||
let layer = modify_inputs.create_layer(id);
|
let layer = modify_inputs.create_layer(id);
|
||||||
|
|
||||||
// Insert the main chain node (Blend or Morph) depending on whether a blend count is provided
|
// Insert the main chain node (Blend or Morph) depending on whether a blend count is provided, referencing
|
||||||
let (chain_node_id, layer_alias, path_alias) = if let Some(count) = blend_count {
|
// its control path input by the Blend template's named position or the Morph proto node's parameter symbol
|
||||||
(modify_inputs.insert_blend_data(layer, count as f64), "Blend", "Blend Path")
|
let (path_input_connector, layer_alias, path_alias) = if let Some(count) = blend_count {
|
||||||
|
let blend_node_id = modify_inputs.insert_blend_data(layer, count as f64);
|
||||||
|
(InputConnector::node_at_index(blend_node_id, BLEND_PATH_INPUT_INDEX), "Blend", "Blend Path")
|
||||||
} else {
|
} else {
|
||||||
(modify_inputs.insert_morph_data(layer), "Morph", "Morph Path")
|
let morph_node_id = modify_inputs.insert_morph_data(layer);
|
||||||
|
(InputConnector::node(morph_node_id, graphene_std::vector::morph::PathInput), "Morph", "Morph Path")
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the control path layer (Path → Auto-Tangents → Origins to Polyline)
|
// Create the control path layer (Path → Auto-Tangents → Origins to Polyline)
|
||||||
@@ -226,9 +301,9 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
network_interface.move_layer_to_stack(control_path_layer, parent, insert_index, &[]);
|
network_interface.move_layer_to_stack(control_path_layer, parent, insert_index, &[]);
|
||||||
network_interface.move_layer_to_stack(layer, parent, insert_index + 1, &[]);
|
network_interface.move_layer_to_stack(layer, parent, insert_index + 1, &[]);
|
||||||
|
|
||||||
// Connect the Path node's output to the chain node's path parameter input (input 4 for both Morph and Blend).
|
// Connect the Path node's output to the chain node's control path input.
|
||||||
// Done after move_layer_to_stack so chain nodes have correct positions when converted to absolute.
|
// Done after move_layer_to_stack so chain nodes have correct positions when converted to absolute.
|
||||||
network_interface.set_input(&InputConnector::node(chain_node_id, 4), NodeInput::node(path_node_id, 0), &[]);
|
network_interface.set_input(&path_input_connector, NodeInput::node(path_node_id, 0), &[]);
|
||||||
|
|
||||||
responses.add(NodeGraphMessage::SetDisplayNameImpl {
|
responses.add(NodeGraphMessage::SetDisplayNameImpl {
|
||||||
node_id: id,
|
node_id: id,
|
||||||
@@ -246,29 +321,29 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
control_path_id,
|
control_path_id,
|
||||||
} => {
|
} => {
|
||||||
// Find the chain node (Blend or Morph, first in chain of the layer)
|
// Find the chain node (Blend or Morph, first in chain of the layer)
|
||||||
let Some(OutputConnector::Node { node_id: chain_node, .. }) = network_interface.upstream_output_connector(&InputConnector::node(interpolation_layer_id, 1), &[]) else {
|
let Some(OutputConnector::Node { node_id: chain_node, .. }) = network_interface.upstream_output_connector(&InputConnector::layer_secondary_input(interpolation_layer_id), &[]) else {
|
||||||
log::error!("Could not find chain node for layer {interpolation_layer_id}");
|
log::error!("Could not find chain node for layer {interpolation_layer_id}");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get what feeds into the chain node's primary input (the children stack)
|
// Get what feeds into the chain node's primary input (the children stack)
|
||||||
let Some(OutputConnector::Node { node_id: children_id, output_index }) = network_interface.upstream_output_connector(&InputConnector::node(chain_node, 0), &[]) else {
|
let Some(OutputConnector::Node { node_id: children_id, output_index }) = network_interface.upstream_output_connector(&InputConnector::primary_input(chain_node), &[]) else {
|
||||||
log::error!("Could not find children stack feeding chain node {chain_node}");
|
log::error!("Could not find children stack feeding chain node {chain_node}");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Find the deepest node in the control path layer's chain (Origins to Polyline)
|
// Find the deepest node in the control path layer's chain (Origins to Polyline)
|
||||||
let mut deepest_chain_node = None;
|
let mut deepest_chain_node = None;
|
||||||
let mut current_connector = InputConnector::node(control_path_id, 1);
|
let mut current_connector = InputConnector::layer_secondary_input(control_path_id);
|
||||||
while let Some(OutputConnector::Node { node_id, .. }) = network_interface.upstream_output_connector(¤t_connector, &[]) {
|
while let Some(OutputConnector::Node { node_id, .. }) = network_interface.upstream_output_connector(¤t_connector, &[]) {
|
||||||
deepest_chain_node = Some(node_id);
|
deepest_chain_node = Some(node_id);
|
||||||
current_connector = InputConnector::node(node_id, 0);
|
current_connector = InputConnector::primary_input(node_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect children to the deepest chain node's input 0 (or the layer's input 1 if no chain)
|
// Connect children to the deepest chain node's input 0 (or the layer's input 1 if no chain)
|
||||||
let target_connector = match deepest_chain_node {
|
let target_connector = match deepest_chain_node {
|
||||||
Some(node_id) => InputConnector::node(node_id, 0),
|
Some(node_id) => InputConnector::primary_input(node_id),
|
||||||
None => InputConnector::node(control_path_id, 1),
|
None => InputConnector::layer_secondary_input(control_path_id),
|
||||||
};
|
};
|
||||||
network_interface.set_input(&target_connector, NodeInput::node(children_id, output_index), &[]);
|
network_interface.set_input(&target_connector, NodeInput::node(children_id, output_index), &[]);
|
||||||
|
|
||||||
@@ -299,7 +374,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
responses.add(NodeGraphMessage::AddNodes { nodes, new_ids });
|
responses.add(NodeGraphMessage::AddNodes { nodes, new_ids });
|
||||||
|
|
||||||
responses.add(NodeGraphMessage::SetInput {
|
responses.add(NodeGraphMessage::SetInput {
|
||||||
input_connector: InputConnector::node(layer.to_node(), 1),
|
input_connector: InputConnector::layer_secondary_input(layer.to_node()),
|
||||||
input: NodeInput::node(first_new_node_id, 0),
|
input: NodeInput::node(first_new_node_id, 0),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -310,14 +385,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
GraphOperationMessage::NewColorFillLayer { node_id, color, parent, insert_index } => {
|
GraphOperationMessage::NewColorFillLayer { node_id, color, parent, insert_index } => {
|
||||||
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
|
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
|
||||||
let layer = modify_inputs.create_layer(node_id);
|
let layer = modify_inputs.create_layer(node_id);
|
||||||
modify_inputs.insert_color_value(color, layer);
|
modify_inputs.insert_color_value(color, layer, InputConnector::layer_secondary_input(layer.to_node()));
|
||||||
network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
|
|
||||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
|
||||||
}
|
|
||||||
GraphOperationMessage::NewVectorLayer { id, subpaths, parent, insert_index } => {
|
|
||||||
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
|
|
||||||
let layer = modify_inputs.create_layer(id);
|
|
||||||
modify_inputs.insert_vector(subpaths, layer, true, true, true);
|
|
||||||
network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
|
network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
|
||||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
}
|
}
|
||||||
@@ -368,7 +436,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
input_node: NodeInput::node(document_node.inputs[1].as_node().unwrap_or_default(), 0),
|
input_node: NodeInput::node(document_node.inputs[1].as_node().unwrap_or_default(), 0),
|
||||||
output_nodes: network_interface
|
output_nodes: network_interface
|
||||||
.outward_wires(&[])
|
.outward_wires(&[])
|
||||||
.and_then(|outward_wires| outward_wires.get(&OutputConnector::node(artboard.to_node(), 0)))
|
.and_then(|outward_wires| outward_wires.get(&OutputConnector::primary_output(artboard.to_node())))
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
merge_node: node_id,
|
merge_node: node_id,
|
||||||
@@ -394,7 +462,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
for artboard in &artboard_data {
|
for artboard in &artboard_data {
|
||||||
// Modify downstream connections
|
// Modify downstream connections
|
||||||
responses.add(NodeGraphMessage::SetInput {
|
responses.add(NodeGraphMessage::SetInput {
|
||||||
input_connector: InputConnector::node(artboard.1.merge_node, 1),
|
input_connector: InputConnector::layer_secondary_input(artboard.1.merge_node),
|
||||||
input: NodeInput::node(artboard.1.input_node.as_node().unwrap_or_default(), 0),
|
input: NodeInput::node(artboard.1.input_node.as_node().unwrap_or_default(), 0),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -402,7 +470,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
for outward_wire in &artboard.1.output_nodes {
|
for outward_wire in &artboard.1.output_nodes {
|
||||||
let input = NodeInput::node(artboard_data[artboard.0].merge_node, 0);
|
let input = NodeInput::node(artboard_data[artboard.0].merge_node, 0);
|
||||||
let input_connector = match artboard_data.get(&outward_wire.node_id().unwrap_or_default()) {
|
let input_connector = match artboard_data.get(&outward_wire.node_id().unwrap_or_default()) {
|
||||||
Some(artboard_info) => InputConnector::node(artboard_info.merge_node, outward_wire.input_index()),
|
Some(artboard_info) => InputConnector::node_at_index(artboard_info.merge_node, outward_wire.input_index()),
|
||||||
_ => *outward_wire,
|
_ => *outward_wire,
|
||||||
};
|
};
|
||||||
responses.add(NodeGraphMessage::SetInput { input_connector, input });
|
responses.add(NodeGraphMessage::SetInput { input_connector, input });
|
||||||
@@ -462,18 +530,14 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
};
|
};
|
||||||
placement_transform.translation = placement_transform.translation.round();
|
placement_transform.translation = placement_transform.translation.round();
|
||||||
|
|
||||||
let graphite_gradient_stops = extract_graphite_gradient_stops(&svg);
|
let gradient_info = SvgGradientInfo {
|
||||||
|
graphite_stops: extract_graphite_gradient_stops(&svg),
|
||||||
|
spaces: extract_gradient_spaces(&svg),
|
||||||
|
};
|
||||||
|
|
||||||
// Pass identity so each leaf layer receives only its SVG-native transform from `abs_transform`.
|
// Pass identity so each leaf layer receives only its SVG-native transform from `abs_transform`.
|
||||||
// The placement offset is then applied once to the root group layer below.
|
// The placement offset is then applied once to the root group layer below.
|
||||||
import_usvg_node(
|
import_usvg_node(&mut modify_inputs, &usvg::Node::Group(Box::new(tree.root().clone())), id, parent, insert_index, &gradient_info);
|
||||||
&mut modify_inputs,
|
|
||||||
&usvg::Node::Group(Box::new(tree.root().clone())),
|
|
||||||
id,
|
|
||||||
parent,
|
|
||||||
insert_index,
|
|
||||||
&graphite_gradient_stops,
|
|
||||||
);
|
|
||||||
|
|
||||||
// After import, `layer_node` is set to the root group. Apply the placement transform to it
|
// After import, `layer_node` is set to the root group. Apply the placement transform to it
|
||||||
// (skipped automatically when identity, so file-open with content at origin creates no Transform node).
|
// (skipped automatically when identity, so file-open with content at origin creates no Transform node).
|
||||||
@@ -505,11 +569,136 @@ fn usvg_transform(c: usvg::Transform) -> DAffine2 {
|
|||||||
|
|
||||||
const GRAPHITE_NAMESPACE: &str = "https://graphite.art";
|
const GRAPHITE_NAMESPACE: &str = "https://graphite.art";
|
||||||
|
|
||||||
|
/// Gradient information pre-parsed from the raw SVG XML, carrying what usvg's simplified tree drops.
|
||||||
|
struct SvgGradientInfo {
|
||||||
|
/// Real stops, keyed by gradient element `id`, for gradients Graphite exported with midpoint curve data.
|
||||||
|
graphite_stops: HashMap<String, Gradient>,
|
||||||
|
/// Gradient spaces, keyed by gradient element `id`, resolved from the `color-interpolation` property.
|
||||||
|
spaces: HashMap<String, GradientSpace>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pre-parses the raw SVG XML to resolve each gradient's inherited `color-interpolation` property, which usvg's
|
||||||
|
/// tree does not carry. Only `linearRGB` selects the linear space; `auto` and `sRGB` (browsers treat the
|
||||||
|
/// user-agent-defined `auto` as `sRGB`) mean gamma, as does any unrecognized value.
|
||||||
|
fn extract_gradient_spaces(svg: &str) -> HashMap<String, GradientSpace> {
|
||||||
|
let mut result = HashMap::new();
|
||||||
|
|
||||||
|
// Quick check: gradients in an SVG that never mentions `color-interpolation` all take the sRGB default
|
||||||
|
if !svg.contains("color-interpolation") {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let doc = match usvg::roxmltree::Document::parse(svg) {
|
||||||
|
Ok(doc) => doc,
|
||||||
|
Err(_) => return result,
|
||||||
|
};
|
||||||
|
|
||||||
|
// The document's `<style>` blocks apply to every element, so parse them once up front
|
||||||
|
let mut stylesheet = simplecss::StyleSheet::new();
|
||||||
|
for style_element in doc.descendants().filter(|node| node.tag_name().name() == "style") {
|
||||||
|
if !matches!(style_element.attribute("type"), None | Some("") | Some("text/css")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for text in style_element.children().filter(|child| child.is_text()).filter_map(|child| child.text()) {
|
||||||
|
stylesheet.parse_more(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for node in doc.descendants() {
|
||||||
|
match node.tag_name().name() {
|
||||||
|
"linearGradient" | "radialGradient" => {}
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(gradient_id) = node.attribute("id")
|
||||||
|
&& let Some(gradient_space) = resolve_color_interpolation(node, &stylesheet)
|
||||||
|
{
|
||||||
|
result.insert(gradient_id.to_string(), gradient_space);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `color-interpolation` in effect for an element: the nearest self-or-ancestor declaration, taking each
|
||||||
|
/// element's own winning declaration per [`declared_color_interpolation`]'s cascade order.
|
||||||
|
fn resolve_color_interpolation(element: usvg::roxmltree::Node, stylesheet: &simplecss::StyleSheet) -> Option<GradientSpace> {
|
||||||
|
let mut next = Some(element);
|
||||||
|
|
||||||
|
while let Some(element) = next {
|
||||||
|
match declared_color_interpolation(element, stylesheet) {
|
||||||
|
Some("linearRGB") => return Some(GradientSpace::RgbLinear),
|
||||||
|
// `inherit` defers to the ancestors like an undeclared element
|
||||||
|
Some("inherit") | None => {}
|
||||||
|
Some(_) => return Some(GradientSpace::RgbGamma),
|
||||||
|
}
|
||||||
|
|
||||||
|
next = element.parent_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The winning `color-interpolation` declaration on a single element per the CSS cascade: `!important` declarations
|
||||||
|
/// beat normal ones, the inline `style` beats the `<style>` rules (already specificity-sorted, so their last match
|
||||||
|
/// wins), and the presentation attribute yields to them all. Later declarations win priority ties.
|
||||||
|
fn declared_color_interpolation<'a>(element: usvg::roxmltree::Node<'a, '_>, stylesheet: &simplecss::StyleSheet<'a>) -> Option<&'a str> {
|
||||||
|
let mut winner: Option<(u8, &'a str)> = None;
|
||||||
|
let mut consider = |priority: u8, value: &'a str| {
|
||||||
|
if winner.is_none_or(|(existing, _)| priority >= existing) {
|
||||||
|
winner = Some((priority, value));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(value) = element.attribute("color-interpolation") {
|
||||||
|
consider(0, value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
for rule in stylesheet.rules.iter().filter(|rule| rule.selector.matches(&CssElement(element))) {
|
||||||
|
for declaration in rule.declarations.iter().filter(|declaration| declaration.name == "color-interpolation") {
|
||||||
|
consider(if declaration.important { 3 } else { 1 }, declaration.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(style) = element.attribute("style") {
|
||||||
|
for declaration in simplecss::DeclarationTokenizer::from(style).filter(|declaration| declaration.name == "color-interpolation") {
|
||||||
|
consider(if declaration.important { 4 } else { 2 }, declaration.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
winner.map(|(_, value)| value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adapts a roxmltree element to simplecss's selector-matching interface.
|
||||||
|
struct CssElement<'a, 'input>(usvg::roxmltree::Node<'a, 'input>);
|
||||||
|
|
||||||
|
impl simplecss::Element for CssElement<'_, '_> {
|
||||||
|
fn parent_element(&self) -> Option<Self> {
|
||||||
|
self.0.parent_element().map(CssElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prev_sibling_element(&self) -> Option<Self> {
|
||||||
|
self.0.prev_sibling_element().map(CssElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_local_name(&self, local_name: &str) -> bool {
|
||||||
|
self.0.tag_name().name() == local_name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attribute_matches(&self, local_name: &str, operator: simplecss::AttributeOperator) -> bool {
|
||||||
|
self.0.attribute(local_name).is_some_and(|value| operator.matches(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pseudo_class_matches(&self, class: simplecss::PseudoClass) -> bool {
|
||||||
|
matches!(class, simplecss::PseudoClass::FirstChild) && self.0.prev_sibling_element().is_none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Pre-parses the raw SVG XML to extract gradient stops that have `graphite:midpoint` attributes.
|
/// Pre-parses the raw SVG XML to extract gradient stops that have `graphite:midpoint` attributes.
|
||||||
/// Graphite exports gradients with midpoint curve data by writing interpolated approximation stops
|
/// Graphite exports gradients with midpoint curve data by writing interpolated approximation stops
|
||||||
/// alongside the real stops. Real stops are tagged with `graphite:midpoint` attributes.
|
/// alongside the real stops. Real stops are tagged with `graphite:midpoint` attributes.
|
||||||
/// Returns a map from gradient element `id` to `GradientStops` containing only the real stops.
|
/// Returns a map from gradient element `id` to `Gradient` containing only the real stops.
|
||||||
fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops> {
|
fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, Gradient> {
|
||||||
let mut result = HashMap::new();
|
let mut result = HashMap::new();
|
||||||
|
|
||||||
// Quick check: if the SVG doesn't reference `graphite:midpoint` at all, skip parsing
|
// Quick check: if the SVG doesn't reference `graphite:midpoint` at all, skip parsing
|
||||||
@@ -555,7 +744,7 @@ fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if has_any_midpoint && !real_stops.is_empty() {
|
if has_any_midpoint && !real_stops.is_empty() {
|
||||||
result.insert(gradient_id, GradientStops::new(real_stops));
|
result.insert(gradient_id, Gradient::new(real_stops));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -570,7 +759,7 @@ fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option<Color> {
|
|||||||
let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.;
|
let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.;
|
||||||
let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.;
|
let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.;
|
||||||
let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.;
|
let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.;
|
||||||
Some(Color::from_rgbaf32_unchecked(r, g, b, opacity))
|
Some(Color::from_gamma_srgb_channels(r, g, b, opacity))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Import a usvg node as the root of an SVG import operation.
|
/// Import a usvg node as the root of an SVG import operation.
|
||||||
@@ -579,14 +768,7 @@ fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option<Color> {
|
|||||||
/// interact with any existing layers in the parent stack. All descendant layers use a lightweight
|
/// interact with any existing layers in the parent stack. All descendant layers use a lightweight
|
||||||
/// O(n) import path that skips collision detection and instead calculates positions directly from
|
/// O(n) import path that skips collision detection and instead calculates positions directly from
|
||||||
/// the known tree structure.
|
/// the known tree structure.
|
||||||
fn import_usvg_node(
|
fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, id: NodeId, parent: LayerNodeIdentifier, insert_index: usize, gradient_info: &SvgGradientInfo) {
|
||||||
modify_inputs: &mut ModifyInputsContext,
|
|
||||||
node: &usvg::Node,
|
|
||||||
id: NodeId,
|
|
||||||
parent: LayerNodeIdentifier,
|
|
||||||
insert_index: usize,
|
|
||||||
graphite_gradient_stops: &HashMap<String, GradientStops>,
|
|
||||||
) {
|
|
||||||
let layer = modify_inputs.create_layer(id);
|
let layer = modify_inputs.create_layer(id);
|
||||||
|
|
||||||
modify_inputs.network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
|
modify_inputs.network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
|
||||||
@@ -606,7 +788,7 @@ fn import_usvg_node(
|
|||||||
modify_inputs.import = true;
|
modify_inputs.import = true;
|
||||||
|
|
||||||
for child in group.children() {
|
for child in group.children() {
|
||||||
let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, graphite_gradient_stops, &mut group_extents_map);
|
let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, gradient_info, &mut group_extents_map);
|
||||||
child_extents_svg_order.push(extent);
|
child_extents_svg_order.push(extent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,7 +807,7 @@ fn import_usvg_node(
|
|||||||
modify_inputs.network_interface.unload_all_nodes_bounding_box(&[]);
|
modify_inputs.network_interface.unload_all_nodes_bounding_box(&[]);
|
||||||
}
|
}
|
||||||
usvg::Node::Path(path) => {
|
usvg::Node::Path(path) => {
|
||||||
import_usvg_path(modify_inputs, node, path, layer, graphite_gradient_stops);
|
import_usvg_path(modify_inputs, node, path, layer, gradient_info);
|
||||||
}
|
}
|
||||||
usvg::Node::Image(_image) => {
|
usvg::Node::Image(_image) => {
|
||||||
warn!("Skip image");
|
warn!("Skip image");
|
||||||
@@ -649,7 +831,7 @@ fn import_usvg_node_inner(
|
|||||||
id: NodeId,
|
id: NodeId,
|
||||||
parent: LayerNodeIdentifier,
|
parent: LayerNodeIdentifier,
|
||||||
insert_index: usize,
|
insert_index: usize,
|
||||||
graphite_gradient_stops: &HashMap<String, GradientStops>,
|
gradient_info: &SvgGradientInfo,
|
||||||
group_extents_map: &mut HashMap<LayerNodeIdentifier, Vec<u32>>,
|
group_extents_map: &mut HashMap<LayerNodeIdentifier, Vec<u32>>,
|
||||||
) -> u32 {
|
) -> u32 {
|
||||||
let layer = modify_inputs.create_layer(id);
|
let layer = modify_inputs.create_layer(id);
|
||||||
@@ -660,7 +842,7 @@ fn import_usvg_node_inner(
|
|||||||
usvg::Node::Group(group) => {
|
usvg::Node::Group(group) => {
|
||||||
let mut child_extents: Vec<u32> = Vec::new();
|
let mut child_extents: Vec<u32> = Vec::new();
|
||||||
for child in group.children() {
|
for child in group.children() {
|
||||||
let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, graphite_gradient_stops, group_extents_map);
|
let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, gradient_info, group_extents_map);
|
||||||
child_extents.push(extent);
|
child_extents.push(extent);
|
||||||
}
|
}
|
||||||
modify_inputs.layer_node = Some(layer);
|
modify_inputs.layer_node = Some(layer);
|
||||||
@@ -675,7 +857,7 @@ fn import_usvg_node_inner(
|
|||||||
total_extent
|
total_extent
|
||||||
}
|
}
|
||||||
usvg::Node::Path(path) => {
|
usvg::Node::Path(path) => {
|
||||||
import_usvg_path(modify_inputs, node, path, layer, graphite_gradient_stops);
|
import_usvg_path(modify_inputs, node, path, layer, gradient_info);
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
usvg::Node::Image(_image) => {
|
usvg::Node::Image(_image) => {
|
||||||
@@ -691,22 +873,38 @@ fn import_usvg_node_inner(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn insert_brush_strokes_chain(network_interface: &mut NodeNetworkInterface, layer: LayerNodeIdentifier, strokes_node_id: NodeId, color: Color, diameter: f64, hardness: f64, flow: f64) {
|
||||||
|
let Some(strokes_node_type) = resolve_proto_node_type(graphene_std::brush::brush_strokes::IDENTIFIER) else {
|
||||||
|
log::error!("Brush strokes node does not exist");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let strokes_node = strokes_node_type.node_template_input_override([
|
||||||
|
Some(NodeInput::value(TaggedValue::Strokes(Vec::new()), false)),
|
||||||
|
Some(NodeInput::value(TaggedValue::Color(color), false)),
|
||||||
|
Some(NodeInput::value(TaggedValue::F64(diameter), false)),
|
||||||
|
Some(NodeInput::value(TaggedValue::F64(hardness), false)),
|
||||||
|
Some(NodeInput::value(TaggedValue::F64(flow), false)),
|
||||||
|
]);
|
||||||
|
network_interface.insert_node(strokes_node_id, strokes_node, &[]);
|
||||||
|
network_interface.set_input(&InputConnector::node_at_index(layer.to_node(), 1), NodeInput::node(strokes_node_id, 0), &[]);
|
||||||
|
}
|
||||||
|
|
||||||
/// Helper to apply path data (vector geometry, fill, stroke, transform) to a layer.
|
/// Helper to apply path data (vector geometry, fill, stroke, transform) to a layer.
|
||||||
fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, path: &usvg::Path, layer: LayerNodeIdentifier, graphite_gradient_stops: &HashMap<String, GradientStops>) {
|
fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, path: &usvg::Path, layer: LayerNodeIdentifier, gradient_info: &SvgGradientInfo) {
|
||||||
let subpaths = convert_usvg_path(path);
|
let bezpath = convert_usvg_path(path);
|
||||||
|
|
||||||
// Skip creating a Transform node entirely when the SVG-native transform is identity.
|
// Skip creating a Transform node entirely when the SVG-native transform is identity.
|
||||||
let node_transform = usvg_transform(node.abs_transform());
|
let node_transform = usvg_transform(node.abs_transform());
|
||||||
let has_transform = node_transform != DAffine2::IDENTITY;
|
let has_transform = node_transform != DAffine2::IDENTITY;
|
||||||
|
|
||||||
modify_inputs.insert_vector(subpaths, layer, has_transform, path.fill().is_some(), path.stroke().is_some());
|
modify_inputs.insert_vector(bezpath, layer, has_transform, path.fill().is_some(), path.stroke().is_some());
|
||||||
|
|
||||||
if has_transform && let Some(transform_node_id) = modify_inputs.existing_proto_node_id(graphene_std::transform_nodes::transform::IDENTIFIER, false) {
|
if has_transform && let Some(transform_node_id) = modify_inputs.existing_proto_node_id(graphene_std::transform_nodes::transform::IDENTIFIER, false) {
|
||||||
transform_utils::update_transform(modify_inputs.network_interface, &transform_node_id, node_transform);
|
transform_utils::update_transform(modify_inputs.network_interface, &transform_node_id, node_transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(fill) = path.fill() {
|
if let Some(fill) = path.fill() {
|
||||||
apply_usvg_fill(fill, modify_inputs, graphite_gradient_stops);
|
apply_usvg_fill(fill, modify_inputs, gradient_info);
|
||||||
}
|
}
|
||||||
if let Some(stroke) = path.stroke() {
|
if let Some(stroke) = path.stroke() {
|
||||||
apply_usvg_stroke(stroke, modify_inputs, node_transform);
|
apply_usvg_stroke(stroke, modify_inputs, node_transform);
|
||||||
@@ -792,22 +990,21 @@ fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsCont
|
|||||||
},
|
},
|
||||||
join_miter_limit: stroke.miterlimit().get() as f64,
|
join_miter_limit: stroke.miterlimit().get() as f64,
|
||||||
align: StrokeAlign::Center,
|
align: StrokeAlign::Center,
|
||||||
paint_order: PaintOrder::StrokeAbove,
|
|
||||||
transform,
|
transform,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_spread_method(spread_method: usvg::SpreadMethod) -> GradientSpreadMethod {
|
fn convert_gradient_spread(spread_method: usvg::SpreadMethod) -> GradientSpread {
|
||||||
match spread_method {
|
match spread_method {
|
||||||
usvg::SpreadMethod::Pad => GradientSpreadMethod::Pad,
|
usvg::SpreadMethod::Pad => GradientSpread::Pad,
|
||||||
usvg::SpreadMethod::Reflect => GradientSpreadMethod::Reflect,
|
usvg::SpreadMethod::Reflect => GradientSpread::Reflect,
|
||||||
usvg::SpreadMethod::Repeat => GradientSpreadMethod::Repeat,
|
usvg::SpreadMethod::Repeat => GradientSpread::Repeat,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, graphite_gradient_stops: &HashMap<String, GradientStops>) {
|
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, gradient_info: &SvgGradientInfo) {
|
||||||
match &fill.paint() {
|
match &fill.paint() {
|
||||||
usvg::Paint::Color(color) => modify_inputs.fill_color_set(Some(usvg_color(*color, fill.opacity().get()))),
|
usvg::Paint::Color(color) => modify_inputs.fill_color_set(Some(usvg_color(*color, fill.opacity().get()))),
|
||||||
usvg::Paint::LinearGradient(linear) => {
|
usvg::Paint::LinearGradient(linear) => {
|
||||||
@@ -817,9 +1014,9 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
|
|||||||
let direction = end - start;
|
let direction = end - start;
|
||||||
let transform = DAffine2::from_cols(direction, direction.perp(), start);
|
let transform = DAffine2::from_cols(direction, direction.perp(), start);
|
||||||
|
|
||||||
let gradient_type = GradientType::Linear;
|
let gradient_form = GradientForm::Linear;
|
||||||
|
|
||||||
let gradient = match graphite_gradient_stops.get(linear.id()) {
|
let gradient = match gradient_info.graphite_stops.get(linear.id()) {
|
||||||
Some(graphite_stops) => graphite_stops.clone(),
|
Some(graphite_stops) => graphite_stops.clone(),
|
||||||
None => {
|
None => {
|
||||||
let stops = linear.stops().iter().map(|stop| GradientStop {
|
let stops = linear.stops().iter().map(|stop| GradientStop {
|
||||||
@@ -827,11 +1024,16 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
|
|||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
color: usvg_color(stop.color(), stop.opacity().get()),
|
color: usvg_color(stop.color(), stop.opacity().get()),
|
||||||
});
|
});
|
||||||
GradientStops::new(stops)
|
Gradient::new(stops)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let spread_method = convert_spread_method(linear.spread_method());
|
// SVG interpolates between stops in gamma sRGB unless `color-interpolation` opts into linearRGB, carried explicitly rather than as the linear default
|
||||||
modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform);
|
let settings = GradientSettings {
|
||||||
|
spread: convert_gradient_spread(linear.spread_method()),
|
||||||
|
space: gradient_info.spaces.get(linear.id()).copied().unwrap_or(GradientSpace::RgbGamma),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
modify_inputs.fill_gradient_set(gradient, gradient_form, settings, transform);
|
||||||
}
|
}
|
||||||
usvg::Paint::RadialGradient(radial) => {
|
usvg::Paint::RadialGradient(radial) => {
|
||||||
let gradient_transform = usvg_transform(radial.transform());
|
let gradient_transform = usvg_transform(radial.transform());
|
||||||
@@ -841,9 +1043,9 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
|
|||||||
let direction = end - start;
|
let direction = end - start;
|
||||||
let transform = DAffine2::from_cols(direction, direction.perp(), start);
|
let transform = DAffine2::from_cols(direction, direction.perp(), start);
|
||||||
|
|
||||||
let gradient_type = GradientType::Radial;
|
let gradient_form = GradientForm::Radial;
|
||||||
|
|
||||||
let gradient = match graphite_gradient_stops.get(radial.id()) {
|
let gradient = match gradient_info.graphite_stops.get(radial.id()) {
|
||||||
Some(graphite_stops) => graphite_stops.clone(),
|
Some(graphite_stops) => graphite_stops.clone(),
|
||||||
None => {
|
None => {
|
||||||
let stops = radial.stops().iter().map(|stop| GradientStop {
|
let stops = radial.stops().iter().map(|stop| GradientStop {
|
||||||
@@ -851,13 +1053,213 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
|
|||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
color: usvg_color(stop.color(), stop.opacity().get()),
|
color: usvg_color(stop.color(), stop.opacity().get()),
|
||||||
});
|
});
|
||||||
GradientStops::new(stops)
|
Gradient::new(stops)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let spread_method = convert_spread_method(radial.spread_method());
|
let settings = GradientSettings {
|
||||||
|
spread: convert_gradient_spread(radial.spread_method()),
|
||||||
modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform);
|
space: gradient_info.spaces.get(radial.id()).copied().unwrap_or(GradientSpace::RgbGamma),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
modify_inputs.fill_gradient_set(gradient, gradient_form, settings, transform);
|
||||||
}
|
}
|
||||||
usvg::Paint::Pattern(_) => warn!("SVG patterns are not currently supported"),
|
usvg::Paint::Pattern(_) => warn!("SVG patterns are not currently supported"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn stroke_order_set_reorders_the_fill_and_stroke_nodes() {
|
||||||
|
use crate::messages::tool::common_functionality::graph_modification_utils::get_stroke_paint_order;
|
||||||
|
use crate::test_utils::test_prelude::*;
|
||||||
|
use graphene_std::vector::style::PaintOrder;
|
||||||
|
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||||
|
|
||||||
|
let document = editor.active_document();
|
||||||
|
let layer = document.metadata().all_layers().next().unwrap();
|
||||||
|
let paint_order = get_stroke_paint_order(layer, &document.network_interface);
|
||||||
|
assert_eq!(paint_order, PaintOrder::StrokeAbove, "a fresh shape should stroke above its fill");
|
||||||
|
|
||||||
|
editor
|
||||||
|
.handle_message(GraphOperationMessage::StrokeOrderSet {
|
||||||
|
layer,
|
||||||
|
paint_order: PaintOrder::StrokeBelow,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let paint_order = get_stroke_paint_order(layer, &editor.active_document().network_interface);
|
||||||
|
assert_eq!(paint_order, PaintOrder::StrokeBelow, "the rewrite should move the stroke downstream of the fill");
|
||||||
|
|
||||||
|
editor
|
||||||
|
.handle_message(GraphOperationMessage::StrokeOrderSet {
|
||||||
|
layer,
|
||||||
|
paint_order: PaintOrder::StrokeAbove,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let paint_order = get_stroke_paint_order(layer, &editor.active_document().network_interface);
|
||||||
|
assert_eq!(paint_order, PaintOrder::StrokeAbove, "the rewrite should move the stroke back upstream of the fill");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_node_inserted_between_the_fill_and_stroke_makes_the_order_swap_inapplicable() {
|
||||||
|
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_proto_node_type;
|
||||||
|
use crate::messages::tool::common_functionality::graph_modification_utils::stroke_paint_order_applicable;
|
||||||
|
use crate::test_utils::test_prelude::*;
|
||||||
|
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||||
|
|
||||||
|
let layer = editor.active_document().metadata().all_layers().next().unwrap();
|
||||||
|
assert!(
|
||||||
|
stroke_paint_order_applicable(layer, &editor.active_document().network_interface),
|
||||||
|
"a fresh shape's adjacent Fill and Stroke pair should be swappable"
|
||||||
|
);
|
||||||
|
|
||||||
|
let stroke_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER);
|
||||||
|
let stroke_node_id = ModifyInputsContext::locate_node_in_layer_chain(&stroke_reference, layer, &editor.active_document().network_interface).unwrap();
|
||||||
|
|
||||||
|
let node_template = Box::new(resolve_proto_node_type(graphene_std::ops::passthrough::IDENTIFIER).unwrap().default_node_template());
|
||||||
|
let passthrough_node_id = NodeId::new();
|
||||||
|
editor
|
||||||
|
.handle_message(NodeGraphMessage::InsertNode {
|
||||||
|
node_id: passthrough_node_id,
|
||||||
|
node_template,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
editor
|
||||||
|
.handle_message(NodeGraphMessage::InsertNodeBetween {
|
||||||
|
node_id: passthrough_node_id,
|
||||||
|
input_connector: InputConnector::node_at_index(stroke_node_id, 0),
|
||||||
|
insert_node_input_index: 0,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!stroke_paint_order_applicable(layer, &editor.active_document().network_interface),
|
||||||
|
"a node between the pair should gray the order radio out"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn color_interpolation_resolves_per_gradient_with_inheritance_and_style_priority() {
|
||||||
|
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" color-interpolation="linearRGB">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="inherited"/>
|
||||||
|
<linearGradient id="attribute" color-interpolation="sRGB"/>
|
||||||
|
<linearGradient id="styled" color-interpolation="sRGB" style="fill: red; color-interpolation: linearRGB"/>
|
||||||
|
<radialGradient id="auto" color-interpolation="auto"/>
|
||||||
|
</defs>
|
||||||
|
</svg>"##;
|
||||||
|
|
||||||
|
let spaces = extract_gradient_spaces(svg);
|
||||||
|
assert_eq!(spaces.get("inherited"), Some(&GradientSpace::RgbLinear), "an undeclared gradient should inherit from its ancestors");
|
||||||
|
assert_eq!(spaces.get("attribute"), Some(&GradientSpace::RgbGamma), "an sRGB declaration should beat the inherited linearRGB");
|
||||||
|
assert_eq!(spaces.get("styled"), Some(&GradientSpace::RgbLinear), "the inline style should beat the presentation attribute");
|
||||||
|
assert_eq!(
|
||||||
|
spaces.get("auto"),
|
||||||
|
Some(&GradientSpace::RgbGamma),
|
||||||
|
"auto should mean gamma like browsers treat it, not defer to ancestors"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn graphite_stop_extraction_keeps_real_stops_and_linearizes_their_colors() {
|
||||||
|
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" xmlns:graphite="https://graphite.art">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="ramp">
|
||||||
|
<stop stop-color="#000000" graphite:midpoint="0.3" />
|
||||||
|
<stop offset="0.25" stop-color="#404040" />
|
||||||
|
<stop offset="0.5" stop-color="#808080" stop-opacity="0.5" graphite:midpoint="0.5" />
|
||||||
|
<stop offset="1" stop-color="#ffffff" graphite:midpoint="0.5" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>"##;
|
||||||
|
|
||||||
|
let stops = extract_graphite_gradient_stops(svg);
|
||||||
|
let gradient = stops.get("ramp").expect("the tagged gradient should be recovered");
|
||||||
|
|
||||||
|
// The untagged stop is baked approximation residue, not authored data
|
||||||
|
assert_eq!(gradient.len(), 3, "only stops tagged with a midpoint should survive");
|
||||||
|
assert_eq!(gradient.positions(false), vec![0., 0.5, 1.]);
|
||||||
|
assert_eq!(gradient.midpoints(), vec![0.3, 0.5, 0.5]);
|
||||||
|
|
||||||
|
// Hex stop bytes are gamma-encoded, so the recovered color must lift them to linear light
|
||||||
|
assert_eq!(gradient.color(1), Some(Color::from_gamma_srgb_channels(128. / 255., 128. / 255., 128. / 255., 0.5)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn color_interpolation_reads_style_blocks_with_selector_specificity() {
|
||||||
|
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
linearGradient { color-interpolation: linearRGB }
|
||||||
|
.classy { color-interpolation: sRGB }
|
||||||
|
#exact { color-interpolation: linearRGB }
|
||||||
|
</style>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="from-type-rule"/>
|
||||||
|
<linearGradient id="from-class-rule" class="classy"/>
|
||||||
|
<linearGradient id="exact" class="classy"/>
|
||||||
|
<linearGradient id="inline-beats-rules" class="classy" style="color-interpolation: linearRGB"/>
|
||||||
|
<linearGradient id="rule-beats-attribute" class="classy" color-interpolation="linearRGB"/>
|
||||||
|
</defs>
|
||||||
|
</svg>"##;
|
||||||
|
|
||||||
|
let spaces = extract_gradient_spaces(svg);
|
||||||
|
assert_eq!(spaces.get("from-type-rule"), Some(&GradientSpace::RgbLinear), "a type rule in a style block should reach the gradient");
|
||||||
|
assert_eq!(
|
||||||
|
spaces.get("from-class-rule"),
|
||||||
|
Some(&GradientSpace::RgbGamma),
|
||||||
|
"the class rule should outrank the type rule by specificity"
|
||||||
|
);
|
||||||
|
assert_eq!(spaces.get("exact"), Some(&GradientSpace::RgbLinear), "the ID rule should outrank the class rule");
|
||||||
|
assert_eq!(spaces.get("inline-beats-rules"), Some(&GradientSpace::RgbLinear), "the inline style should beat every style block rule");
|
||||||
|
assert_eq!(
|
||||||
|
spaces.get("rule-beats-attribute"),
|
||||||
|
Some(&GradientSpace::RgbGamma),
|
||||||
|
"a style block rule should beat the presentation attribute"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repeated_and_important_declarations_resolve_by_cascade_order() {
|
||||||
|
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>.forced { color-interpolation: linearRGB !important }</style>
|
||||||
|
<linearGradient id="last-declaration-wins" style="color-interpolation: sRGB; color-interpolation: linearRGB"/>
|
||||||
|
<linearGradient id="important-beats-later" style="color-interpolation: linearRGB !important; color-interpolation: sRGB"/>
|
||||||
|
<linearGradient id="important-rule-beats-inline" class="forced" style="color-interpolation: sRGB"/>
|
||||||
|
</svg>"##;
|
||||||
|
|
||||||
|
let spaces = extract_gradient_spaces(svg);
|
||||||
|
assert_eq!(
|
||||||
|
spaces.get("last-declaration-wins"),
|
||||||
|
Some(&GradientSpace::RgbLinear),
|
||||||
|
"the last of repeated inline declarations should win"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
spaces.get("important-beats-later"),
|
||||||
|
Some(&GradientSpace::RgbLinear),
|
||||||
|
"an `!important` declaration should beat a later normal one"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
spaces.get("important-rule-beats-inline"),
|
||||||
|
Some(&GradientSpace::RgbLinear),
|
||||||
|
"an `!important` style block rule should beat the inline style"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn color_interpolation_yields_nothing_when_never_declared() {
|
||||||
|
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg"><linearGradient id="plain"/></svg>"##;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
extract_gradient_spaces(svg).is_empty(),
|
||||||
|
"gradients without any declaration should fall back to the caller's gamma default"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{Inp
|
|||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{NodeId, NodeInput};
|
use graph_craft::document::{NodeId, NodeInput};
|
||||||
use graphene_std::subpath::Subpath;
|
|
||||||
use graphene_std::transform::Transform;
|
use graphene_std::transform::Transform;
|
||||||
use graphene_std::vector::PointId;
|
|
||||||
|
|
||||||
/// Update the inputs of the transform node to match a new transform
|
/// Update the inputs of the transform node to match a new transform
|
||||||
pub fn update_transform(network_interface: &mut NodeNetworkInterface, node_id: &NodeId, transform: DAffine2) {
|
pub fn update_transform(network_interface: &mut NodeNetworkInterface, node_id: &NodeId, transform: DAffine2) {
|
||||||
@@ -14,10 +12,26 @@ pub fn update_transform(network_interface: &mut NodeNetworkInterface, node_id: &
|
|||||||
let rotation = rotation.to_degrees();
|
let rotation = rotation.to_degrees();
|
||||||
let skew = DVec2::new(skew.atan().to_degrees(), 0.);
|
let skew = DVec2::new(skew.atan().to_degrees(), 0.);
|
||||||
|
|
||||||
network_interface.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::DVec2(translation), false), &[]);
|
network_interface.set_input(
|
||||||
network_interface.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::F64(rotation), false), &[]);
|
&InputConnector::node(*node_id, graphene_std::transform_nodes::transform::TranslationInput),
|
||||||
network_interface.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::DVec2(scale), false), &[]);
|
NodeInput::value(TaggedValue::DVec2(translation), false),
|
||||||
network_interface.set_input(&InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::DVec2(skew), false), &[]);
|
&[],
|
||||||
|
);
|
||||||
|
network_interface.set_input(
|
||||||
|
&InputConnector::node(*node_id, graphene_std::transform_nodes::transform::RotationInput),
|
||||||
|
NodeInput::value(TaggedValue::F64(rotation), false),
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
network_interface.set_input(
|
||||||
|
&InputConnector::node(*node_id, graphene_std::transform_nodes::transform::ScaleInput),
|
||||||
|
NodeInput::value(TaggedValue::DVec2(scale), false),
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
network_interface.set_input(
|
||||||
|
&InputConnector::node(*node_id, graphene_std::transform_nodes::transform::SkewInput),
|
||||||
|
NodeInput::value(TaggedValue::DVec2(skew), false),
|
||||||
|
&[],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This should be extracted from the graph at the location of the transform node.
|
// TODO: This should be extracted from the graph at the location of the transform node.
|
||||||
@@ -71,32 +85,6 @@ pub fn get_current_normalized_pivot(inputs: &[NodeInput]) -> DVec2 {
|
|||||||
if let Some(&TaggedValue::DVec2(pivot)) = inputs[5].as_value() { pivot } else { DVec2::splat(0.5) }
|
if let Some(&TaggedValue::DVec2(pivot)) = inputs[5].as_value() { pivot } else { DVec2::splat(0.5) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Expand a bounds to avoid div zero errors
|
|
||||||
fn clamp_bounds(bounds_min: DVec2, mut bounds_max: DVec2) -> [DVec2; 2] {
|
|
||||||
let bounds_size = bounds_max - bounds_min;
|
|
||||||
if bounds_size.x < 1e-10 {
|
|
||||||
bounds_max.x = bounds_min.x + 1.;
|
|
||||||
}
|
|
||||||
if bounds_size.y < 1e-10 {
|
|
||||||
bounds_max.y = bounds_min.y + 1.;
|
|
||||||
}
|
|
||||||
[bounds_min, bounds_max]
|
|
||||||
}
|
|
||||||
/// Returns corners of all subpaths
|
|
||||||
fn subpath_bounds(subpaths: &[Subpath<PointId>]) -> [DVec2; 2] {
|
|
||||||
subpaths
|
|
||||||
.iter()
|
|
||||||
.filter_map(|subpath| subpath.bounding_box())
|
|
||||||
.reduce(|b1, b2| [b1[0].min(b2[0]), b1[1].max(b2[1])])
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns corners of all subpaths (but expanded to avoid division-by-zero errors)
|
|
||||||
pub fn nonzero_subpath_bounds(subpaths: &[Subpath<PointId>]) -> [DVec2; 2] {
|
|
||||||
let [bounds_min, bounds_max] = subpath_bounds(subpaths);
|
|
||||||
clamp_bounds(bounds_min, bounds_max)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
use super::transform_utils;
|
use super::transform_utils;
|
||||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_document_node_type, resolve_network_node_type, resolve_proto_node_type};
|
use crate::messages::portfolio::document::node_graph::document_node_definitions::{
|
||||||
|
ARTBOARD_DIMENSIONS_INPUT_INDEX, ARTBOARD_LOCATION_INPUT_INDEX, DefinitionIdentifier, resolve_document_node_type, resolve_network_node_type, resolve_proto_node_type,
|
||||||
|
};
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface, OutputConnector};
|
use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface};
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils::{get_fill_input_node_id, get_upstream_gradient_value_node_id, gradient_chain_target_input};
|
use crate::messages::tool::common_functionality::graph_modification_utils::{
|
||||||
|
ReplaceablePaintChain, get_fill_input_node_id, get_upstream_gradient_value_node_id, gradient_chain_target_input, replaceable_paint_chain,
|
||||||
|
};
|
||||||
use glam::{DAffine2, DVec2, IVec2};
|
use glam::{DAffine2, DVec2, IVec2};
|
||||||
use graph_craft::application_io::resource::ResourceId;
|
use graph_craft::application_io::resource::ResourceId;
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{NodeId, NodeInput};
|
use graph_craft::document::{NodeId, NodeInput};
|
||||||
use graph_craft::{ProtoNodeIdentifier, concrete, descriptor};
|
use graph_craft::{ProtoNodeIdentifier, concrete};
|
||||||
use graphene_std::brush::brush_stroke::BrushStroke;
|
|
||||||
use graphene_std::list::List;
|
use graphene_std::list::List;
|
||||||
use graphene_std::raster::BlendMode;
|
use graphene_std::raster::BlendMode;
|
||||||
use graphene_std::raster_types::Image;
|
use graphene_std::raster_types::Image;
|
||||||
use graphene_std::subpath::Subpath;
|
|
||||||
use graphene_std::text::{Font, TypesettingConfig};
|
use graphene_std::text::{Font, TypesettingConfig};
|
||||||
use graphene_std::vector::style::{GradientSpreadMethod, GradientType, Stroke};
|
use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace, GradientSpread, PaintOrder, Stroke};
|
||||||
use graphene_std::vector::{GradientStops, PointId, Vector, VectorModification, VectorModificationType};
|
use graphene_std::vector::{Gradient, GradientRamp, Vector, VectorModification, VectorModificationType};
|
||||||
use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration};
|
use graphene_std::{Artboard, Color, Graphic};
|
||||||
|
use kurbo::BezPath;
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum TransformIn {
|
pub enum TransformIn {
|
||||||
@@ -58,72 +61,6 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
Some(document)
|
Some(document)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts at any folder, or the output, and skips layer nodes based on insert_index. Non layer nodes are always skipped. Returns the post node InputConnector and pre node OutputConnector
|
|
||||||
/// Non layer nodes directly upstream of a layer are treated as part of that layer. See insert_index == 2 in the diagram
|
|
||||||
/// -----> Post node
|
|
||||||
/// | if insert_index == 0, return (Post node, Some(Layer1))
|
|
||||||
/// -> Layer1
|
|
||||||
/// ↑ if insert_index == 1, return (Layer1, Some(Layer2))
|
|
||||||
/// -> Layer2
|
|
||||||
/// ↑
|
|
||||||
/// -> NonLayerNode
|
|
||||||
/// ↑ if insert_index == 2, return (NonLayerNode, Some(Layer3))
|
|
||||||
/// -> Layer3
|
|
||||||
/// if insert_index == 3, return (Layer3, None)
|
|
||||||
pub fn get_post_node_with_index(network_interface: &NodeNetworkInterface, parent: LayerNodeIdentifier, insert_index: usize) -> InputConnector {
|
|
||||||
let mut post_node_input_connector = if parent == LayerNodeIdentifier::ROOT_PARENT {
|
|
||||||
InputConnector::Export(0)
|
|
||||||
} else {
|
|
||||||
InputConnector::node(parent.to_node(), 1)
|
|
||||||
};
|
|
||||||
// Skip layers based on skip_layer_nodes, which inserts the new layer at a certain index of the layer stack.
|
|
||||||
let mut current_index = 0;
|
|
||||||
|
|
||||||
// Set the post node to the layer node at insert_index
|
|
||||||
loop {
|
|
||||||
if current_index == insert_index {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let next_node_in_stack_id = network_interface
|
|
||||||
.input_from_connector(&post_node_input_connector, &[])
|
|
||||||
.and_then(|input_from_connector| if let NodeInput::Node { node_id, .. } = input_from_connector { Some(node_id) } else { None });
|
|
||||||
|
|
||||||
if let Some(next_node_in_stack_id) = next_node_in_stack_id {
|
|
||||||
// Only increment index for layer nodes
|
|
||||||
if network_interface.is_layer(next_node_in_stack_id, &[]) {
|
|
||||||
current_index += 1;
|
|
||||||
}
|
|
||||||
// Input as a sibling to the Layer node above
|
|
||||||
post_node_input_connector = InputConnector::node(*next_node_in_stack_id, 0);
|
|
||||||
} else {
|
|
||||||
log::error!("Error getting post node: insert_index out of bounds");
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let layer_input_connector = post_node_input_connector;
|
|
||||||
|
|
||||||
// Sink post_node down to the end of the non layer chain that feeds into post_node, such that pre_node is the layer node at insert_index + 1, or None if insert_index is the last layer
|
|
||||||
loop {
|
|
||||||
let pre_node_output_connector = network_interface.upstream_output_connector(&post_node_input_connector, &[]);
|
|
||||||
|
|
||||||
match pre_node_output_connector {
|
|
||||||
Some(OutputConnector::Node { node_id: pre_node_id, .. }) if !network_interface.is_layer(&pre_node_id, &[]) => {
|
|
||||||
// Update post_node_input_connector for the next iteration
|
|
||||||
post_node_input_connector = InputConnector::node(pre_node_id, 0);
|
|
||||||
// Insert directly under layer if moving to the end of a layer stack that ends with a non layer node that does not have an exposed primary input
|
|
||||||
let primary_is_exposed = network_interface.input_from_connector(&post_node_input_connector, &[]).is_some_and(|input| input.is_exposed());
|
|
||||||
if !primary_is_exposed {
|
|
||||||
return layer_input_connector;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => break, // Break if pre_node_output_connector is None or if pre_node_id is a layer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
post_node_input_connector
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new layer and adds it to the document network. network_interface.move_layer_to_stack should be called after
|
/// Creates a new layer and adds it to the document network. network_interface.move_layer_to_stack should be called after
|
||||||
pub fn create_layer(&mut self, new_id: NodeId) -> LayerNodeIdentifier {
|
pub fn create_layer(&mut self, new_id: NodeId) -> LayerNodeIdentifier {
|
||||||
let new_merge_node = resolve_network_node_type("Merge").expect("Merge node").default_node_template();
|
let new_merge_node = resolve_network_node_type("Merge").expect("Merge node").default_node_template();
|
||||||
@@ -134,11 +71,11 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
/// Creates an artboard as the primary export for the document network.
|
/// Creates an artboard as the primary export for the document network.
|
||||||
pub fn create_artboard(&mut self, new_id: NodeId, location: DVec2, dimensions: DVec2, background: Color, clip: bool) -> LayerNodeIdentifier {
|
pub fn create_artboard(&mut self, new_id: NodeId, location: DVec2, dimensions: DVec2, background: Color, clip: bool) -> LayerNodeIdentifier {
|
||||||
let artboard_node_template = resolve_network_node_type("Artboard").expect("Node").node_template_input_override([
|
let artboard_node_template = resolve_network_node_type("Artboard").expect("Node").node_template_input_override([
|
||||||
Some(NodeInput::type_default(descriptor!(List<Artboard>), true)),
|
Some(NodeInput::type_default(concrete!(List<Artboard>), true)),
|
||||||
Some(NodeInput::type_default(descriptor!(List<Graphic>), true)),
|
Some(NodeInput::type_default(concrete!(List<Graphic>), true)),
|
||||||
Some(NodeInput::value(TaggedValue::DVec2(location), false)),
|
Some(NodeInput::value(TaggedValue::DVec2(location), false)),
|
||||||
Some(NodeInput::value(TaggedValue::DVec2(dimensions), false)),
|
Some(NodeInput::value(TaggedValue::DVec2(dimensions), false)),
|
||||||
Some(NodeInput::value(TaggedValue::Color(Some(background)), false)),
|
Some(NodeInput::value(TaggedValue::Color(background), false)),
|
||||||
Some(NodeInput::value(TaggedValue::Bool(clip), false)),
|
Some(NodeInput::value(TaggedValue::Bool(clip), false)),
|
||||||
]);
|
]);
|
||||||
self.network_interface.insert_node(new_id, artboard_node_template, &[]);
|
self.network_interface.insert_node(new_id, artboard_node_template, &[]);
|
||||||
@@ -149,7 +86,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
let boolean = resolve_proto_node_type(graphene_std::path_bool_nodes::boolean_operation::IDENTIFIER)
|
let boolean = resolve_proto_node_type(graphene_std::path_bool_nodes::boolean_operation::IDENTIFIER)
|
||||||
.expect("Boolean node does not exist")
|
.expect("Boolean node does not exist")
|
||||||
.node_template_input_override([
|
.node_template_input_override([
|
||||||
Some(NodeInput::type_default(descriptor!(List<Graphic>), true)),
|
Some(NodeInput::type_default(concrete!(List<Graphic>), true)),
|
||||||
Some(NodeInput::value(TaggedValue::BooleanOperation(operation), false)),
|
Some(NodeInput::value(TaggedValue::BooleanOperation(operation), false)),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -161,7 +98,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
pub fn insert_blend_data(&mut self, layer: LayerNodeIdentifier, count: f64) -> NodeId {
|
pub fn insert_blend_data(&mut self, layer: LayerNodeIdentifier, count: f64) -> NodeId {
|
||||||
let blend = resolve_network_node_type("Blend")
|
let blend = resolve_network_node_type("Blend")
|
||||||
.expect("Blend node does not exist")
|
.expect("Blend node does not exist")
|
||||||
.node_template_input_override([Some(NodeInput::type_default(descriptor!(List<Graphic>), true)), Some(NodeInput::value(TaggedValue::F64(count), false))]);
|
.node_template_input_override([Some(NodeInput::type_default(concrete!(List<Graphic>), true)), Some(NodeInput::value(TaggedValue::F64(count), false))]);
|
||||||
|
|
||||||
let blend_id = NodeId::new();
|
let blend_id = NodeId::new();
|
||||||
self.network_interface.insert_node(blend_id, blend, &[]);
|
self.network_interface.insert_node(blend_id, blend, &[]);
|
||||||
@@ -173,7 +110,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
pub fn insert_morph_data(&mut self, layer: LayerNodeIdentifier) -> NodeId {
|
pub fn insert_morph_data(&mut self, layer: LayerNodeIdentifier) -> NodeId {
|
||||||
let morph = resolve_proto_node_type(graphene_std::vector::morph::IDENTIFIER)
|
let morph = resolve_proto_node_type(graphene_std::vector::morph::IDENTIFIER)
|
||||||
.expect("Morph node does not exist")
|
.expect("Morph node does not exist")
|
||||||
.node_template_input_override([Some(NodeInput::type_default(descriptor!(List<Graphic>), true)), Some(NodeInput::value(TaggedValue::F64(0.5), false))]);
|
.node_template_input_override([Some(NodeInput::type_default(concrete!(List<Graphic>), true)), Some(NodeInput::value(TaggedValue::F64(0.5), false))]);
|
||||||
|
|
||||||
let morph_id = NodeId::new();
|
let morph_id = NodeId::new();
|
||||||
self.network_interface.insert_node(morph_id, morph, &[]);
|
self.network_interface.insert_node(morph_id, morph, &[]);
|
||||||
@@ -209,9 +146,9 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
path_id
|
path_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_vector(&mut self, subpaths: Vec<Subpath<PointId>>, layer: LayerNodeIdentifier, include_transform: bool, include_fill: bool, include_stroke: bool) {
|
pub fn insert_vector(&mut self, bezpath: BezPath, layer: LayerNodeIdentifier, include_transform: bool, include_fill: bool, include_stroke: bool) {
|
||||||
// Build a VectorModification that reproduces the geometry (same format the Pen tool uses)
|
// Build a VectorModification that reproduces the geometry (same format the Pen tool uses)
|
||||||
let vector = Vector::from_subpaths(subpaths, true);
|
let vector = Vector::from_bezpath(bezpath);
|
||||||
let modification = Box::new(VectorModification::create_from_vector(&vector));
|
let modification = Box::new(VectorModification::create_from_vector(&vector));
|
||||||
|
|
||||||
let shape = resolve_network_node_type("Path")
|
let shape = resolve_network_node_type("Path")
|
||||||
@@ -297,14 +234,41 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
self.network_interface.move_node_to_chain_start(&fill_id, layer, &[], self.import);
|
self.network_interface.move_node_to_chain_start(&fill_id, layer, &[], self.import);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_color_value(&mut self, color: Color, layer: LayerNodeIdentifier) {
|
pub fn insert_color_value(&mut self, color: Color, layer: LayerNodeIdentifier, attachment_input: InputConnector) -> NodeId {
|
||||||
let color_value = resolve_proto_node_type(graphene_std::math_nodes::color_value::IDENTIFIER)
|
let color_value = resolve_proto_node_type(graphene_std::math_nodes::color_value::IDENTIFIER)
|
||||||
.expect("Color Value node does not exist")
|
.expect("Color Value node does not exist")
|
||||||
.node_template_input_override([Some(NodeInput::value(TaggedValue::None, false)), Some(NodeInput::value(TaggedValue::Color(Some(color)), false))]);
|
.node_template_input_override([Some(NodeInput::value(TaggedValue::None, false)), Some(NodeInput::value(TaggedValue::Color(color), false))]);
|
||||||
|
|
||||||
let color_value_id = NodeId::new();
|
let color_value_id = NodeId::new();
|
||||||
self.network_interface.insert_node(color_value_id, color_value, &[]);
|
self.network_interface.insert_node(color_value_id, color_value, &[]);
|
||||||
self.network_interface.move_node_to_chain_start(&color_value_id, layer, &[], self.import);
|
self.start_paint_chain(&color_value_id, layer, attachment_input);
|
||||||
|
|
||||||
|
color_value_id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the whole-expanse paint one tool left on a layer so the other can start its own chain there.
|
||||||
|
/// Severing at the attachment detaches the layer from whatever the walk stopped at, which is the only part a node
|
||||||
|
/// the rest of the graph also draws from is subjected to, since such a node is never among those deleted.
|
||||||
|
fn clear_paint_chain(&mut self, paint_chain: &ReplaceablePaintChain) {
|
||||||
|
self.network_interface.disconnect_input(&paint_chain.attachment_input, &[]);
|
||||||
|
|
||||||
|
if !paint_chain.nodes.is_empty() {
|
||||||
|
self.network_interface.delete_nodes(paint_chain.nodes.clone(), false, &[]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wire a node that paints the layer's whole expanse into the start of its chain,
|
||||||
|
/// or past the 'Transform' nodes a blank layer already carries so those go on applying to the paint.
|
||||||
|
fn start_paint_chain(&mut self, node_id: &NodeId, layer: LayerNodeIdentifier, attachment_input: InputConnector) {
|
||||||
|
let layer_content_input = InputConnector::layer_secondary_input(layer.to_node());
|
||||||
|
|
||||||
|
if attachment_input == layer_content_input {
|
||||||
|
self.network_interface.move_node_to_chain_start(node_id, layer, &[], self.import);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.network_interface.set_input(&attachment_input, NodeInput::node(*node_id, 0), &[]);
|
||||||
|
self.network_interface.set_chain_position(node_id, &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_image_data(&mut self, image: Image<Color>, layer: LayerNodeIdentifier) {
|
pub fn insert_image_data(&mut self, image: Image<Color>, layer: LayerNodeIdentifier) {
|
||||||
@@ -383,6 +347,16 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
self.existing_node_id(&DefinitionIdentifier::ProtoNode(reference), create_if_nonexistent)
|
self.existing_node_id(&DefinitionIdentifier::ProtoNode(reference), create_if_nonexistent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The same as [`Self::existing_proto_node_id`], but yielding `None` on layers whose chain cannot host the node.
|
||||||
|
fn existing_chain_hosted_node_id(&mut self, reference: ProtoNodeIdentifier, create_if_nonexistent: bool) -> Option<NodeId> {
|
||||||
|
let output_layer = self.get_output_layer()?;
|
||||||
|
if !self.network_interface.layer_chain_hosts_node(&output_layer.to_node(), &[], &reference) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.existing_proto_node_id(reference, create_if_nonexistent)
|
||||||
|
}
|
||||||
|
|
||||||
/// Gets the node id of a document node with a specific reference that is upstream from the layer node, and optionally creates it if it does not exist.
|
/// Gets the node id of a document node with a specific reference that is upstream from the layer node, and optionally creates it if it does not exist.
|
||||||
fn existing_node_id(&mut self, reference: &DefinitionIdentifier, create_if_nonexistent: bool) -> Option<NodeId> {
|
fn existing_node_id(&mut self, reference: &DefinitionIdentifier, create_if_nonexistent: bool) -> Option<NodeId> {
|
||||||
// Start from the layer node or export
|
// Start from the layer node or export
|
||||||
@@ -433,17 +407,20 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
// If inserting a 'Path' node, insert a 'Flatten Path' node if the type is `Graphic`.
|
// Without a secondary input there is no chain to hold the node, so inserting it would strand it at the graph origin
|
||||||
|
self.network_interface.input_from_connector(&InputConnector::layer_secondary_input(output_layer.to_node()), &[])?;
|
||||||
|
|
||||||
|
// If inserting a 'Path' node, insert a 'Combine Paths' node if the type is `Graphic`.
|
||||||
// TODO: Allow the 'Path' node to operate on `List` data by utilizing the reference (index or ID?) for each item.
|
// TODO: Allow the 'Path' node to operate on `List` data by utilizing the reference (index or ID?) for each item.
|
||||||
if node_definition.identifier == "Path" {
|
if node_definition.identifier == "Path" {
|
||||||
let layer_input_type = self.network_interface.input_type(&InputConnector::node(output_layer.to_node(), 1), &[]);
|
let layer_input_type = self.network_interface.input_type(&InputConnector::layer_secondary_input(output_layer.to_node()), &[]);
|
||||||
if layer_input_type.compiled_nested_type() == Some(&concrete!(List<Graphic>)) {
|
if layer_input_type.compiled_element_name().as_deref() == Some("Graphic") {
|
||||||
let Some(flatten_path_definition) = resolve_proto_node_type(graphene_std::vector_nodes::flatten_path::IDENTIFIER) else {
|
let Some(combine_paths_definition) = resolve_proto_node_type(graphene_std::vector_nodes::combine_paths::IDENTIFIER) else {
|
||||||
log::error!("Flatten Path does not exist in ModifyInputsContext::existing_node_id");
|
log::error!("Combine Paths does not exist in ModifyInputsContext::existing_node_id");
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let node_id = NodeId::new();
|
let node_id = NodeId::new();
|
||||||
self.network_interface.insert_node(node_id, flatten_path_definition.default_node_template(), &[]);
|
self.network_interface.insert_node(node_id, combine_paths_definition.default_node_template(), &[]);
|
||||||
self.network_interface.move_node_to_chain_start(&node_id, output_layer, &[], self.import);
|
self.network_interface.move_node_to_chain_start(&node_id, output_layer, &[], self.import);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -454,28 +431,35 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn fill_color_set(&mut self, color: Option<Color>) {
|
pub fn fill_color_set(&mut self, color: Option<Color>) {
|
||||||
let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
|
let existing_fill_node_id = self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, false);
|
||||||
|
let Some(fill_node_id) = existing_fill_node_id.or_else(|| self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true)) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::INDEX);
|
let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::PaintInput);
|
||||||
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput::INDEX);
|
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput);
|
||||||
|
|
||||||
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Color(color), false), true);
|
// The backup remembers the last solid color, so the red-slash "none" choice leaves it untouched
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(color), false), false);
|
if let Some(color) = color {
|
||||||
|
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Color(color), false), true);
|
||||||
|
}
|
||||||
|
let fill_value = color.map_or_else(TaggedValue::no_paint, TaggedValue::Color);
|
||||||
|
self.set_input_with_refresh(input_connector, NodeInput::value(fill_value, false), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fill_gradient_set(&mut self, gradient: GradientStops, gradient_type: GradientType, spread_method: GradientSpreadMethod, transform: DAffine2) {
|
pub fn fill_gradient_set(&mut self, gradient: Gradient, gradient_form: GradientForm, settings: GradientSettings, transform: DAffine2) {
|
||||||
let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
|
let existing_fill_node_id = self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, false);
|
||||||
|
let Some(fill_node_id) = existing_fill_node_id.or_else(|| self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true)) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput::INDEX);
|
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput);
|
||||||
|
|
||||||
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Gradient(gradient.clone()), false), true);
|
let ramp = GradientRamp::from(gradient).with_settings(settings);
|
||||||
|
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp.clone()), false), true);
|
||||||
|
|
||||||
// Skip the rerender on all but the last input so the whole update triggers a single graph run
|
// Skip the rerender on all but the last input so the whole update triggers a single graph run
|
||||||
self.set_input_with_refresh(
|
self.set_input_with_refresh(
|
||||||
InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::INDEX),
|
InputConnector::node(fill_node_id, graphene_std::vector::fill::PaintInput),
|
||||||
NodeInput::value(TaggedValue::Gradient(gradient), false),
|
NodeInput::value(TaggedValue::GradientRamp(ramp), false),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -485,49 +469,48 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
.document_network()
|
.document_network()
|
||||||
.nodes
|
.nodes
|
||||||
.get(&fill_node_id)
|
.get(&fill_node_id)
|
||||||
.and_then(|node| node.inputs.get(graphene_std::vector::fill::TransformInput::INDEX))
|
.and_then(|node| node.input(graphene_std::vector::fill::TransformInput))
|
||||||
.is_some_and(|input| input.as_value().is_some());
|
.is_some_and(|input| input.as_value().is_some());
|
||||||
if transform_is_value {
|
if transform_is_value {
|
||||||
self.set_input_with_refresh(
|
self.set_input_with_refresh(
|
||||||
InputConnector::node(fill_node_id, graphene_std::vector::fill::TransformInput::INDEX),
|
InputConnector::node(fill_node_id, graphene_std::vector::fill::HasTransformInput),
|
||||||
NodeInput::value(TaggedValue::OptionalDAffine2(Some(transform)), false),
|
NodeInput::value(TaggedValue::Bool(true), false),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
self.set_input_with_refresh(
|
||||||
|
InputConnector::node(fill_node_id, graphene_std::vector::fill::TransformInput),
|
||||||
|
NodeInput::value(TaggedValue::DAffine2(transform), false),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.set_input_with_refresh(
|
self.set_input_with_refresh(
|
||||||
InputConnector::node(fill_node_id, graphene_std::vector::fill::GradientTypeInput::INDEX),
|
InputConnector::node(fill_node_id, graphene_std::vector::fill::GradientFormInput),
|
||||||
NodeInput::value(TaggedValue::GradientType(gradient_type), false),
|
NodeInput::value(TaggedValue::GradientForm(gradient_form), false),
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
self.set_input_with_refresh(
|
|
||||||
InputConnector::node(fill_node_id, graphene_std::vector::fill::SpreadMethodInput::INDEX),
|
|
||||||
NodeInput::value(TaggedValue::GradientSpreadMethod(spread_method), false),
|
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
|
pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
|
||||||
let Some(blend_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blend_mode::IDENTIFIER, true) else {
|
let Some(blend_node_id) = self.existing_chain_hosted_node_id(graphene_std::blending_nodes::blend_mode::IDENTIFIER, true) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let input_connector = InputConnector::node(blend_node_id, graphene_std::blending_nodes::blend_mode::BlendModeInput::INDEX);
|
let input_connector = InputConnector::node(blend_node_id, graphene_std::blending_nodes::blend_mode::BlendModeInput);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false);
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn opacity_set(&mut self, opacity: f64) {
|
pub fn opacity_set(&mut self, opacity: f64) {
|
||||||
let Some(opacity_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::opacity::IDENTIFIER, true) else {
|
let Some(opacity_node_id) = self.existing_chain_hosted_node_id(graphene_std::blending_nodes::opacity::IDENTIFIER, true) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
// Enable the `has_opacity` checkbox so the value is applied
|
// Enable the `has_opacity` checkbox so the value is applied
|
||||||
self.set_input_with_refresh(
|
self.set_input_with_refresh(
|
||||||
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::HasOpacityInput::INDEX),
|
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::HasOpacityInput),
|
||||||
NodeInput::value(TaggedValue::Bool(true), false),
|
NodeInput::value(TaggedValue::Bool(true), false),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
self.set_input_with_refresh(
|
self.set_input_with_refresh(
|
||||||
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::OpacityInput::INDEX),
|
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::OpacityInput),
|
||||||
NodeInput::value(TaggedValue::F64(opacity * 100.), false),
|
NodeInput::value(TaggedValue::F64(opacity * 100.), false),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
@@ -536,47 +519,78 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
pub fn opacity_fill_set(&mut self, fill: f64) {
|
pub fn opacity_fill_set(&mut self, fill: f64) {
|
||||||
// Reuse an existing Opacity node to avoid a redundant chain walk on slider drags
|
// Reuse an existing Opacity node to avoid a redundant chain walk on slider drags
|
||||||
let identifier = graphene_std::blending_nodes::opacity::IDENTIFIER;
|
let identifier = graphene_std::blending_nodes::opacity::IDENTIFIER;
|
||||||
let existing = self.existing_proto_node_id(identifier.clone(), false);
|
let existing = self.existing_chain_hosted_node_id(identifier.clone(), false);
|
||||||
let existed = existing.is_some();
|
let existed = existing.is_some();
|
||||||
let Some(opacity_node_id) = existing.or_else(|| self.existing_proto_node_id(identifier, true)) else {
|
let Some(opacity_node_id) = existing.or_else(|| self.existing_chain_hosted_node_id(identifier, true)) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
// Freshly-created node defaults to opacity enabled; disable it so the fill slider works independently
|
// Freshly-created node defaults to opacity enabled; disable it so the fill slider works independently
|
||||||
if !existed {
|
if !existed {
|
||||||
self.set_input_with_refresh(
|
self.set_input_with_refresh(
|
||||||
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::HasOpacityInput::INDEX),
|
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::HasOpacityInput),
|
||||||
NodeInput::value(TaggedValue::Bool(false), false),
|
NodeInput::value(TaggedValue::Bool(false), false),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Enable the `has_fill` checkbox so the value is applied
|
// Enable the `has_fill` checkbox so the value is applied
|
||||||
self.set_input_with_refresh(
|
self.set_input_with_refresh(
|
||||||
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::HasFillInput::INDEX),
|
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::HasFillInput),
|
||||||
NodeInput::value(TaggedValue::Bool(true), false),
|
NodeInput::value(TaggedValue::Bool(true), false),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
self.set_input_with_refresh(
|
self.set_input_with_refresh(
|
||||||
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::FillInput::INDEX),
|
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::FillInput),
|
||||||
NodeInput::value(TaggedValue::F64(fill * 100.), false),
|
NodeInput::value(TaggedValue::F64(fill * 100.), false),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the chain's 'Color Value' node, or start a chain with one on an empty layer, painting the layer's whole expanse.
|
||||||
|
pub fn color_value_set(&mut self, color: Color) {
|
||||||
|
let Some(output_layer) = self.get_output_layer() else { return };
|
||||||
|
|
||||||
|
let target_input = gradient_chain_target_input(output_layer, self.network_interface);
|
||||||
|
if let Some(node_id) = self.existing_proto_node_id_at(&target_input, graphene_std::math_nodes::color_value::IDENTIFIER, false) {
|
||||||
|
let input_connector = InputConnector::node(node_id, graphene_std::math_nodes::color_value::ColorInput);
|
||||||
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(color), false), false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The 'Color Value' node discards its primary input, so only a blank 'Merge' layer may start a chain with one,
|
||||||
|
// which any whole-expanse paint the other tool left behind is cleared off to become
|
||||||
|
let Some(paint_chain) = replaceable_paint_chain(output_layer, self.network_interface) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.clear_paint_chain(&paint_chain);
|
||||||
|
|
||||||
|
let color_value_id = self.insert_color_value(color, output_layer, paint_chain.attachment_input);
|
||||||
|
let input_connector = InputConnector::node(color_value_id, graphene_std::math_nodes::color_value::ColorInput);
|
||||||
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(color), false), false);
|
||||||
|
}
|
||||||
|
|
||||||
/// Write the gradient stops to the 'Gradient Value' node feeding the layer.
|
/// Write the gradient stops to the 'Gradient Value' node feeding the layer.
|
||||||
pub fn gradient_stops_set(&mut self, stops: GradientStops) {
|
pub fn gradient_stops_set(&mut self, stops: Gradient) {
|
||||||
let Some(output_layer) = self.get_output_layer() else { return };
|
let Some(output_layer) = self.get_output_layer() else { return };
|
||||||
|
|
||||||
let gradient_value_id = match get_upstream_gradient_value_node_id(output_layer, self.network_interface) {
|
let gradient_value_id = match get_upstream_gradient_value_node_id(output_layer, self.network_interface) {
|
||||||
Some(id) => id,
|
Some(id) => id,
|
||||||
None => {
|
None => {
|
||||||
let target = gradient_chain_target_input(output_layer, self.network_interface);
|
let target = gradient_chain_target_input(output_layer, self.network_interface);
|
||||||
let starts_layer_chain = target == InputConnector::node(output_layer.to_node(), 1);
|
let starts_layer_chain = target == InputConnector::layer_secondary_input(output_layer.to_node());
|
||||||
|
|
||||||
// The Gradient Value node discards its primary input, so starting a chain ahead of existing layer content would drop that content; refuse instead
|
// The 'Gradient Value' node discards its primary input, so only a blank 'Merge' layer may start a chain
|
||||||
if starts_layer_chain && self.network_interface.upstream_output_connector(&target, &[]).is_some() {
|
// with one, which any whole-expanse paint the other tool left behind is cleared off to become
|
||||||
log::error!("Refusing to start a gradient chain ahead of existing layer content");
|
let paint_chain = if starts_layer_chain {
|
||||||
return;
|
let Some(paint_chain) = replaceable_paint_chain(output_layer, self.network_interface) else {
|
||||||
}
|
log::error!("Refusing to start a gradient chain on anything but a blank 'Merge' layer");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.clear_paint_chain(&paint_chain);
|
||||||
|
|
||||||
|
Some(paint_chain)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let Some(node_definition) = resolve_proto_node_type(graphene_std::math_nodes::gradient_value::IDENTIFIER) else {
|
let Some(node_definition) = resolve_proto_node_type(graphene_std::math_nodes::gradient_value::IDENTIFIER) else {
|
||||||
return;
|
return;
|
||||||
@@ -584,9 +598,9 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
let node_id = NodeId::new();
|
let node_id = NodeId::new();
|
||||||
self.network_interface.insert_node(node_id, node_definition.default_node_template(), &[]);
|
self.network_interface.insert_node(node_id, node_definition.default_node_template(), &[]);
|
||||||
|
|
||||||
if starts_layer_chain {
|
if let Some(paint_chain) = paint_chain {
|
||||||
// No Fill node: the new node starts the layer's chain
|
// No Fill node: the new node starts the layer's chain
|
||||||
self.network_interface.move_node_to_chain_start(&node_id, output_layer, &[], self.import);
|
self.start_paint_chain(&node_id, output_layer, paint_chain.attachment_input);
|
||||||
} else {
|
} else {
|
||||||
// Feeding a Fill node's paint input: wire it up and place it one chain-width left and a step below the Fill
|
// Feeding a Fill node's paint input: wire it up and place it one chain-width left and a step below the Fill
|
||||||
self.network_interface.set_input(&target, NodeInput::node(node_id, 0), &[]);
|
self.network_interface.set_input(&target, NodeInput::node(node_id, 0), &[]);
|
||||||
@@ -603,8 +617,65 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput::INDEX);
|
// Only the stops are being replaced, so the ramp's other settings stay as the value node already holds them
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Gradient(stops), false), false);
|
let ramp = GradientRamp {
|
||||||
|
stops: (&stops).into(),
|
||||||
|
..self.gradient_value_ramp(gradient_value_id).unwrap_or_default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput);
|
||||||
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the last 'Gradient Positions' node in the chain when one exists, so on-canvas stop drags stay live even
|
||||||
|
/// though that node would otherwise override the stops value's own placement. Never inserts one: the stops value
|
||||||
|
/// carries placement itself, and these setter nodes are user-authored procedural overrides. A wired input is
|
||||||
|
/// procedural authorship too, so it is likewise left untouched.
|
||||||
|
pub fn gradient_positions_set(&mut self, positions: Vec<f64>) {
|
||||||
|
let Some(output_layer) = self.get_output_layer() else { return };
|
||||||
|
|
||||||
|
let target_input = gradient_chain_target_input(output_layer, self.network_interface);
|
||||||
|
let identifier = graphene_std::math_nodes::gradient_positions::IDENTIFIER;
|
||||||
|
let Some(node_id) = self.existing_proto_node_id_at(&target_input, identifier, false) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let current_input = self
|
||||||
|
.network_interface
|
||||||
|
.document_network()
|
||||||
|
.nodes
|
||||||
|
.get(&node_id)
|
||||||
|
.and_then(|node| node.input(graphene_std::math_nodes::gradient_positions::PositionsInput));
|
||||||
|
if !current_input.is_some_and(|input| input.as_value().is_some()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let input_connector = InputConnector::node(node_id, graphene_std::math_nodes::gradient_positions::PositionsInput);
|
||||||
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64Array(positions), false), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The 'Gradient Midpoints' counterpart of [`Self::gradient_positions_set`], likewise update-only.
|
||||||
|
pub fn gradient_midpoints_set(&mut self, midpoints: Vec<f64>) {
|
||||||
|
let Some(output_layer) = self.get_output_layer() else { return };
|
||||||
|
|
||||||
|
let target_input = gradient_chain_target_input(output_layer, self.network_interface);
|
||||||
|
let identifier = graphene_std::math_nodes::gradient_midpoints::IDENTIFIER;
|
||||||
|
let Some(node_id) = self.existing_proto_node_id_at(&target_input, identifier, false) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let current_input = self
|
||||||
|
.network_interface
|
||||||
|
.document_network()
|
||||||
|
.nodes
|
||||||
|
.get(&node_id)
|
||||||
|
.and_then(|node| node.input(graphene_std::math_nodes::gradient_midpoints::MidpointsInput));
|
||||||
|
if !current_input.is_some_and(|input| input.as_value().is_some()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let input_connector = InputConnector::node(node_id, graphene_std::math_nodes::gradient_midpoints::MidpointsInput);
|
||||||
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64Array(midpoints), false), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the transform to map the unit gradient ((0,0), (1, 0)) to the geometry's local space.
|
/// Update the transform to map the unit gradient ((0,0), (1, 0)) to the geometry's local space.
|
||||||
@@ -669,69 +740,131 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
self.responses.add(NodeGraphMessage::RunDocumentGraph);
|
self.responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write the gradient type to the last 'Gradient Type' node in the chain, inserting one only when the value differs
|
/// Write the Gradient Form to the last 'Gradient Form' node in the chain, inserting one only when the value differs
|
||||||
/// from the default (`Linear`).
|
/// from the default (`Linear`).
|
||||||
pub fn gradient_type_set(&mut self, gradient_type: GradientType) {
|
pub fn gradient_form_set(&mut self, gradient_form: GradientForm) {
|
||||||
let Some(output_layer) = self.get_output_layer() else { return };
|
let Some(output_layer) = self.get_output_layer() else { return };
|
||||||
|
|
||||||
let target_input = gradient_chain_target_input(output_layer, self.network_interface);
|
let target_input = gradient_chain_target_input(output_layer, self.network_interface);
|
||||||
let identifier = graphene_std::math_nodes::gradient_type::IDENTIFIER;
|
let identifier = graphene_std::math_nodes::gradient_form::IDENTIFIER;
|
||||||
let create_if_nonexistent = gradient_type != GradientType::default();
|
let create_if_nonexistent = gradient_form != GradientForm::default();
|
||||||
let Some(node_id) = self.existing_proto_node_id_at(&target_input, identifier, create_if_nonexistent) else {
|
let Some(node_id) = self.existing_proto_node_id_at(&target_input, identifier, create_if_nonexistent) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let input_connector = InputConnector::node(node_id, graphene_std::math_nodes::gradient_type::GradientTypeInput::INDEX);
|
let input_connector = InputConnector::node(node_id, graphene_std::math_nodes::gradient_form::GradientFormInput);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientType(gradient_type), false), false);
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientForm(gradient_form), false), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write the spread method to the last 'Spread Method' node in the chain, inserting one only when the value differs
|
/// Set the spread on the chain's gradient value, which is where the ramp carries it.
|
||||||
/// from the default (`Pad`).
|
pub fn gradient_spread_set(&mut self, gradient_spread: GradientSpread) {
|
||||||
pub fn gradient_spread_method_set(&mut self, spread_method: GradientSpreadMethod) {
|
|
||||||
let Some(output_layer) = self.get_output_layer() else { return };
|
let Some(output_layer) = self.get_output_layer() else { return };
|
||||||
|
let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else {
|
||||||
let target_input = gradient_chain_target_input(output_layer, self.network_interface);
|
|
||||||
let identifier = graphene_std::math_nodes::spread_method::IDENTIFIER;
|
|
||||||
let create_if_nonexistent = spread_method != GradientSpreadMethod::default();
|
|
||||||
let Some(node_id) = self.existing_proto_node_id_at(&target_input, identifier, create_if_nonexistent) else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let Some(ramp) = self.gradient_value_ramp(gradient_value_id) else { return };
|
||||||
|
|
||||||
let input_connector = InputConnector::node(node_id, graphene_std::math_nodes::spread_method::SpreadMethodInput::INDEX);
|
let ramp = GradientRamp { gradient_spread, ..ramp };
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientSpreadMethod(spread_method), false), false);
|
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput);
|
||||||
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The ramp currently held by a 'Gradient Value' node.
|
||||||
|
fn gradient_value_ramp(&self, gradient_value_id: NodeId) -> Option<GradientRamp> {
|
||||||
|
let node = self.network_interface.document_network().nodes.get(&gradient_value_id)?;
|
||||||
|
let TaggedValue::GradientRamp(ramp) = node.input(graphene_std::math_nodes::gradient_value::GradientInput)?.as_value()? else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(ramp.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the space on the chain's gradient value, which is where the ramp carries it. Never touches a
|
||||||
|
/// 'Gradient Space' node: that one is a user-authored procedural override, not something the tools manage.
|
||||||
|
pub fn gradient_space_set(&mut self, gradient_space: GradientSpace) {
|
||||||
|
let Some(output_layer) = self.get_output_layer() else { return };
|
||||||
|
let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(ramp) = self.gradient_value_ramp(gradient_value_id) else { return };
|
||||||
|
|
||||||
|
let ramp = GradientRamp { gradient_space, ..ramp };
|
||||||
|
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput);
|
||||||
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the cyclic wrap flag on the chain's gradient value, which is where the ramp carries it. This holds the existing
|
||||||
|
/// stops in place by reading their positions under the old flag, so batch it before any stops write rather than after one,
|
||||||
|
/// or it would reinterpret incoming stops already authored under the new flag.
|
||||||
|
pub fn gradient_cyclic_set(&mut self, gradient_cyclic: bool) {
|
||||||
|
let Some(output_layer) = self.get_output_layer() else { return };
|
||||||
|
let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(ramp) = self.gradient_value_ramp(gradient_value_id) else { return };
|
||||||
|
|
||||||
|
let ramp = ramp.with_cyclic(gradient_cyclic);
|
||||||
|
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput);
|
||||||
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the interpolation on the chain's gradient value, which is where the ramp carries it.
|
||||||
|
pub fn gradient_interpolation_set(&mut self, gradient_interpolation: GradientInterpolation) {
|
||||||
|
let Some(output_layer) = self.get_output_layer() else { return };
|
||||||
|
let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(ramp) = self.gradient_value_ramp(gradient_value_id) else { return };
|
||||||
|
|
||||||
|
let ramp = GradientRamp { gradient_interpolation, ..ramp };
|
||||||
|
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput);
|
||||||
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the hue direction on the chain's gradient value, which is where the ramp carries it.
|
||||||
|
pub fn gradient_hue_direction_set(&mut self, gradient_hue_direction: GradientHueDirection) {
|
||||||
|
let Some(output_layer) = self.get_output_layer() else { return };
|
||||||
|
let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(ramp) = self.gradient_value_ramp(gradient_value_id) else { return };
|
||||||
|
|
||||||
|
let ramp = GradientRamp { gradient_hue_direction, ..ramp };
|
||||||
|
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput);
|
||||||
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clip_mode_toggle(&mut self, clip_mode: Option<bool>) {
|
pub fn clip_mode_toggle(&mut self, clip_mode: Option<bool>) {
|
||||||
let clip = !clip_mode.unwrap_or(false);
|
let clip = !clip_mode.unwrap_or(false);
|
||||||
let Some(clip_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::clipping_mask::IDENTIFIER, true) else {
|
let Some(clip_node_id) = self.existing_chain_hosted_node_id(graphene_std::blending_nodes::clipping_mask::IDENTIFIER, true) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let input_connector = InputConnector::node(clip_node_id, graphene_std::blending_nodes::clipping_mask::ClipInput::INDEX);
|
let input_connector = InputConnector::node(clip_node_id, graphene_std::blending_nodes::clipping_mask::ClipInput);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Bool(clip), false), false);
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Bool(clip), false), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stroke_set(&mut self, color: Option<Color>, stroke: Stroke) {
|
pub fn stroke_set(&mut self, color: Option<Color>, stroke: Stroke) {
|
||||||
let Some(stroke_node_id) = self.existing_proto_node_id(graphene_std::vector::stroke::IDENTIFIER, true) else {
|
let Some(stroke_node_id) = self.existing_chain_hosted_node_id(graphene_std::vector::stroke::IDENTIFIER, true) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintInput::INDEX);
|
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintInput);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(color), false), true);
|
self.set_input_with_refresh(input_connector, NodeInput::value(color.map_or_else(TaggedValue::no_paint, TaggedValue::Color), false), true);
|
||||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::WeightInput::INDEX);
|
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::WeightInput);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.weight), false), true);
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.weight), false), true);
|
||||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::AlignInput::INDEX);
|
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::AlignInput);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeAlign(stroke.align), false), false);
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeAlign(stroke.align), false), false);
|
||||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::CapInput::INDEX);
|
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::CapInput);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeCap(stroke.cap), false), true);
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeCap(stroke.cap), false), true);
|
||||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::JoinInput::INDEX);
|
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::JoinInput);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeJoin(stroke.join), false), true);
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeJoin(stroke.join), false), true);
|
||||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::MiterLimitInput::INDEX);
|
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::MiterLimitInput);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.join_miter_limit), false), false);
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.join_miter_limit), false), false);
|
||||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintOrderInput::INDEX);
|
// Our stroke node keeps a paint order input rather than encoding the order by Fill/Stroke node adjacency
|
||||||
|
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintOrderInput);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::PaintOrder(stroke.paint_order), false), false);
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::PaintOrder(stroke.paint_order), false), false);
|
||||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashLengthsInput::INDEX);
|
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashPatternInput);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64Array(stroke.dash_lengths), false), true);
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::DashPattern(stroke.dash_lengths), false), true);
|
||||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashOffsetInput::INDEX);
|
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashOffsetInput);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.dash_offset), false), true);
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.dash_offset), false), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -828,13 +961,6 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
self.responses.add(NodeGraphMessage::RunDocumentGraph);
|
self.responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn brush_modify(&mut self, strokes: Vec<BrushStroke>) {
|
|
||||||
let Some(brush_node_id) = self.existing_proto_node_id(graphene_std::brush::brush::brush::IDENTIFIER, true) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
self.set_input_with_refresh(InputConnector::node(brush_node_id, 1), NodeInput::value(TaggedValue::BrushStrokes(strokes), false), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resize_artboard(&mut self, location: DVec2, dimensions: DVec2) {
|
pub fn resize_artboard(&mut self, location: DVec2, dimensions: DVec2) {
|
||||||
let Some(artboard_node_id) = self.existing_network_node_id("Artboard", true) else {
|
let Some(artboard_node_id) = self.existing_network_node_id("Artboard", true) else {
|
||||||
return;
|
return;
|
||||||
@@ -851,8 +977,16 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
dimensions.y = -dimensions.y;
|
dimensions.y = -dimensions.y;
|
||||||
location.y -= dimensions.y;
|
location.y -= dimensions.y;
|
||||||
}
|
}
|
||||||
self.set_input_with_refresh(InputConnector::node(artboard_node_id, 2), NodeInput::value(TaggedValue::DVec2(location), false), false);
|
self.set_input_with_refresh(
|
||||||
self.set_input_with_refresh(InputConnector::node(artboard_node_id, 3), NodeInput::value(TaggedValue::DVec2(dimensions), false), false);
|
InputConnector::node_at_index(artboard_node_id, ARTBOARD_LOCATION_INPUT_INDEX),
|
||||||
|
NodeInput::value(TaggedValue::DVec2(location), false),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
self.set_input_with_refresh(
|
||||||
|
InputConnector::node_at_index(artboard_node_id, ARTBOARD_DIMENSIONS_INPUT_INDEX),
|
||||||
|
NodeInput::value(TaggedValue::DVec2(dimensions), false),
|
||||||
|
false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the input, refresh the Properties panel, and run the document graph if skip_rerender is false
|
/// Set the input, refresh the Properties panel, and run the document graph if skip_rerender is false
|
||||||
@@ -864,3 +998,16 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the Stroke node's paint order input, which decides whether the stroke paints above or below the fill.
|
||||||
|
/// Returns whether the graph changed.
|
||||||
|
pub fn set_stroke_paint_order(network_interface: &mut NodeNetworkInterface, network_path: &[NodeId], stroke_node_id: NodeId, paint_order: PaintOrder) -> bool {
|
||||||
|
let connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintOrderInput);
|
||||||
|
if let Some(NodeInput::Value { tagged_value, .. }) = network_interface.input_from_connector(&connector, network_path)
|
||||||
|
&& **tagged_value == TaggedValue::PaintOrder(paint_order)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
network_interface.set_input(&connector, NodeInput::value(TaggedValue::PaintOrder(paint_order), false), network_path);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
use crate::messages::input_mapper::utility_types::keyboard::Key;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ use crate::consts::{
|
|||||||
VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR,
|
VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR,
|
||||||
};
|
};
|
||||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion};
|
use crate::messages::input_mapper::utility_types::keyboard::{Key, MouseMotion};
|
||||||
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
|
use crate::messages::input_mapper::utility_types::pointer::ViewportPosition;
|
||||||
use crate::messages::portfolio::document::navigation::utility_types::NavigationOperation;
|
use crate::messages::portfolio::document::navigation::utility_types::NavigationOperation;
|
||||||
use crate::messages::portfolio::document::utility_types::misc::PTZ;
|
use crate::messages::portfolio::document::utility_types::misc::PTZ;
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+39
-46
@@ -1,7 +1,6 @@
|
|||||||
use super::DocumentNodeDefinition;
|
use super::DocumentNodeDefinition;
|
||||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
|
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::{DocumentNodePersistentMetadata, InputMetadata, NodeTemplate, WidgetOverride};
|
use crate::messages::portfolio::document::utility_types::network_interface::{InputMetadata, NodeTemplate, NodeTemplateImplementation, WidgetOverride};
|
||||||
use graph_craft::document::*;
|
|
||||||
use graphene_std::registry::*;
|
use graphene_std::registry::*;
|
||||||
use graphene_std::*;
|
use graphene_std::*;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
@@ -14,7 +13,7 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
|
|||||||
let network_nodes = custom
|
let network_nodes = custom
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|definition| {
|
.filter_map(|definition| {
|
||||||
if let DocumentNodeImplementation::ProtoNode(proto_node_identifier) = &definition.node_template.document_node.implementation {
|
if let NodeTemplateImplementation::ProtoNode(proto_node_identifier) = &definition.node_template.implementation {
|
||||||
definitions_map.insert(DefinitionIdentifier::ProtoNode(proto_node_identifier.clone()), definition);
|
definitions_map.insert(DefinitionIdentifier::ProtoNode(proto_node_identifier.clone()), definition);
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
@@ -71,29 +70,22 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
|
|||||||
DocumentNodeDefinition {
|
DocumentNodeDefinition {
|
||||||
identifier: display_name,
|
identifier: display_name,
|
||||||
node_template: NodeTemplate {
|
node_template: NodeTemplate {
|
||||||
document_node: DocumentNode {
|
inputs,
|
||||||
inputs,
|
call_argument: input_type.clone(),
|
||||||
call_argument: input_type.clone(),
|
implementation: NodeTemplateImplementation::ProtoNode(id.clone()),
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(id.clone()),
|
// Context features are resolved from the registry at compile time, not persisted on the node
|
||||||
visible: true,
|
context_features: ContextDependencies::default(),
|
||||||
skip_deduplication: false,
|
// TODO: Store information for input overrides in the node macro
|
||||||
context_features: ContextDependencies::default(),
|
input_metadata: fields
|
||||||
..Default::default()
|
.iter()
|
||||||
},
|
.map(|f| match f.widget_override {
|
||||||
persistent_node_metadata: DocumentNodePersistentMetadata {
|
RegistryWidgetOverride::None => (f.name, f.description).into(),
|
||||||
// TODO: Store information for input overrides in the node macro
|
RegistryWidgetOverride::Hidden => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Hidden),
|
||||||
input_metadata: fields
|
RegistryWidgetOverride::String(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::String(str.to_string())),
|
||||||
.iter()
|
RegistryWidgetOverride::Custom(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Custom(str.to_string())),
|
||||||
.map(|f| match f.widget_override {
|
})
|
||||||
RegistryWidgetOverride::None => (f.name, f.description).into(),
|
.collect(),
|
||||||
RegistryWidgetOverride::Hidden => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Hidden),
|
..Default::default()
|
||||||
RegistryWidgetOverride::String(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::String(str.to_string())),
|
|
||||||
RegistryWidgetOverride::Custom(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Custom(str.to_string())),
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
locked: false,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
category,
|
category,
|
||||||
description: Cow::Borrowed(description),
|
description: Cow::Borrowed(description),
|
||||||
@@ -104,13 +96,14 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
|
|||||||
|
|
||||||
// Add the rest of the network nodes to the map and add the metadata for their internal protonodes
|
// Add the rest of the network nodes to the map and add the metadata for their internal protonodes
|
||||||
for mut network_node in network_nodes {
|
for mut network_node in network_nodes {
|
||||||
traverse_node(&network_node.node_template.document_node, &mut network_node.node_template.persistent_node_metadata, &definitions_map);
|
fill_proto_node_metadata(&mut network_node.node_template, &definitions_map);
|
||||||
|
|
||||||
// Set the reference to the node identifier
|
// Set the reference to the node identifier
|
||||||
if let Some(nested_metadata) = network_node.node_template.persistent_node_metadata.network_metadata.as_mut() {
|
if let NodeTemplateImplementation::Network(network_template) = &mut network_node.node_template.implementation {
|
||||||
nested_metadata.persistent_metadata.reference = Some(network_node.identifier.to_string());
|
network_template.reference = Some(network_node.identifier.to_string());
|
||||||
// If it is not a merge node, then set the display name to the identifier/reference
|
// If it is not a merge node, then set the display name to the identifier/reference
|
||||||
if network_node.identifier != "Merge" {
|
if network_node.identifier != "Merge" {
|
||||||
network_node.node_template.persistent_node_metadata.display_name = network_node.identifier.to_string();
|
network_node.node_template.display_name = network_node.identifier.to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
definitions_map.insert(DefinitionIdentifier::Network(network_node.identifier.to_string()), network_node);
|
definitions_map.insert(DefinitionIdentifier::Network(network_node.identifier.to_string()), network_node);
|
||||||
@@ -119,27 +112,27 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
|
|||||||
definitions_map
|
definitions_map
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Traverses a document node template and metadata in parallel to add metadata to the protonodes
|
/// Recursively fills each nested proto node's editor metadata from its definition, preserving only the authored position.
|
||||||
fn traverse_node(node: &DocumentNode, node_metadata: &mut DocumentNodePersistentMetadata, definitions_map: &HashMap<DefinitionIdentifier, DocumentNodeDefinition>) {
|
fn fill_proto_node_metadata(node_template: &mut NodeTemplate, definitions_map: &HashMap<DefinitionIdentifier, DocumentNodeDefinition>) {
|
||||||
match &node.implementation {
|
match &mut node_template.implementation {
|
||||||
DocumentNodeImplementation::Network(node_network) => {
|
NodeTemplateImplementation::Network(network_template) => {
|
||||||
for (nested_node_id, nested_node) in node_network.nodes.iter() {
|
for nested_template in network_template.nodes.values_mut() {
|
||||||
let nested_metadata = node_metadata.network_metadata.as_mut().unwrap().persistent_metadata.node_metadata.get_mut(nested_node_id).unwrap();
|
fill_proto_node_metadata(nested_template, definitions_map);
|
||||||
traverse_node(nested_node, &mut nested_metadata.persistent_metadata, definitions_map);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DocumentNodeImplementation::ProtoNode(id) => {
|
NodeTemplateImplementation::ProtoNode(id) => {
|
||||||
// Set all the metadata except the position to the proto node information from the macro
|
// If this lookup fails then the proto node id in the definition doesn't match what is generated by the macro
|
||||||
// TODO: Use options in the template to specify what you want to default and what you want to override
|
|
||||||
// If this fails then the proto node id in the definition doesn't match what is generated by the macro
|
|
||||||
let Some(definition) = definitions_map.get(&DefinitionIdentifier::ProtoNode(id.clone())) else {
|
let Some(definition) = definitions_map.get(&DefinitionIdentifier::ProtoNode(id.clone())) else {
|
||||||
// log::error!("Could not get definition for id {} when filling in protonode metadata for a custom node", id.clone());
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let mut new_metadata = definition.node_template.persistent_node_metadata.clone();
|
|
||||||
new_metadata.node_type_metadata = node_metadata.node_type_metadata.clone();
|
let definition_template = &definition.node_template;
|
||||||
*node_metadata = new_metadata
|
node_template.display_name = definition_template.display_name.clone();
|
||||||
|
node_template.input_metadata = definition_template.input_metadata.clone();
|
||||||
|
node_template.output_names = definition_template.output_names.clone();
|
||||||
|
node_template.locked = definition_template.locked;
|
||||||
|
node_template.pinned = definition_template.pinned;
|
||||||
}
|
}
|
||||||
DocumentNodeImplementation::Extract => {}
|
NodeTemplateImplementation::Extract => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use super::utility_types::Direction;
|
use super::utility_types::Direction;
|
||||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
use crate::messages::input_mapper::utility_types::keyboard::Key;
|
||||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
|
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::{ImportOrExport, InputConnector, NodeTemplate, OutputConnector};
|
use crate::messages::portfolio::document::utility_types::network_interface::{ImportOrExport, InputConnector, NodeTemplate, OutputConnector};
|
||||||
@@ -157,7 +157,7 @@ pub enum NodeGraphMessage {
|
|||||||
SetInputValue {
|
SetInputValue {
|
||||||
node_id: NodeId,
|
node_id: NodeId,
|
||||||
input_index: usize,
|
input_index: usize,
|
||||||
value: TaggedValue,
|
value: Box<TaggedValue>,
|
||||||
},
|
},
|
||||||
SetInput {
|
SetInput {
|
||||||
input_connector: InputConnector,
|
input_connector: InputConnector,
|
||||||
|
|||||||
@@ -234,8 +234,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
x: (mid_point.x / 24.) as i32,
|
x: (mid_point.x / 24.) as i32,
|
||||||
y: (mid_point.y / 24.) as i32,
|
y: (mid_point.y / 24.) as i32,
|
||||||
});
|
});
|
||||||
let node_input_connector = InputConnector::node(node_id, 0);
|
let node_input_connector = InputConnector::primary_input(node_id);
|
||||||
let node_output_connector = OutputConnector::node(node_id, 0);
|
let node_output_connector = OutputConnector::primary_output(node_id);
|
||||||
responses.add(NodeGraphMessage::CreateWire {
|
responses.add(NodeGraphMessage::CreateWire {
|
||||||
output_connector,
|
output_connector,
|
||||||
input_connector: node_input_connector,
|
input_connector: node_input_connector,
|
||||||
@@ -301,7 +301,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
// A freshly added Text node carries no font, so give it the default font (registered like the Text tool does)
|
// A freshly added Text node carries no font, so give it the default font (registered like the Text tool does)
|
||||||
if node_type == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) {
|
if node_type == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) {
|
||||||
let font_resource_id = graph_craft::application_io::resource::ResourceId::new();
|
let font_resource_id = graph_craft::application_io::resource::ResourceId::new();
|
||||||
if let Some(font_input) = node_template.document_node.inputs.get_mut(graphene_std::text::text::FontInput::INDEX) {
|
if let Some(font_input) = node_template.input_mut(graphene_std::text::text::FontInput) {
|
||||||
*font_input = NodeInput::value(TaggedValue::Resource(font_resource_id), false);
|
*font_input = NodeInput::value(TaggedValue::Resource(font_resource_id), false);
|
||||||
}
|
}
|
||||||
responses.add(DocumentMessage::Resource(ResourceMessage::AddFont {
|
responses.add(DocumentMessage::Resource(ResourceMessage::AddFont {
|
||||||
@@ -336,10 +336,10 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Ensure connection is to correct input of new node. If it does not have an input then do not connect
|
// Ensure connection is to correct input of new node. If it does not have an input then do not connect
|
||||||
if let Some((input_index, _)) = node_template.document_node.inputs.iter().enumerate().find(|(_, input)| input.is_exposed()) {
|
if let Some((input_index, _)) = node_template.inputs.iter().enumerate().find(|(_, input)| input.is_exposed()) {
|
||||||
responses.add(NodeGraphMessage::CreateWire {
|
responses.add(NodeGraphMessage::CreateWire {
|
||||||
output_connector: *output_connector,
|
output_connector: *output_connector,
|
||||||
input_connector: InputConnector::node(node_id, input_index),
|
input_connector: InputConnector::node_at_index(node_id, input_index),
|
||||||
});
|
});
|
||||||
|
|
||||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
@@ -493,7 +493,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
*exposed = set_to_exposed;
|
*exposed = set_to_exposed;
|
||||||
}
|
}
|
||||||
responses.add(NodeGraphMessage::SetInput {
|
responses.add(NodeGraphMessage::SetInput {
|
||||||
input_connector: InputConnector::node(node_id, input_index),
|
input_connector: InputConnector::node_at_index(node_id, input_index),
|
||||||
input: node_input,
|
input: node_input,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -515,7 +515,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let encapsulating_connector = InputConnector::node(*node_id, 0);
|
let encapsulating_connector = InputConnector::primary_input(*node_id);
|
||||||
if !exposed {
|
if !exposed {
|
||||||
network_interface.disconnect_input(&encapsulating_connector, network_path);
|
network_interface.disconnect_input(&encapsulating_connector, network_path);
|
||||||
}
|
}
|
||||||
@@ -530,19 +530,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
|
|
||||||
network_interface.set_input(&encapsulating_connector, input, network_path);
|
network_interface.set_input(&encapsulating_connector, input, network_path);
|
||||||
|
|
||||||
let Some(outward_wires) = network_interface.outward_wires(breadcrumb_network_path) else {
|
// Disconnect all connections fed by the hidden import in the nested network
|
||||||
log::error!("Could not get outward wires in remove_import");
|
network_interface.disconnect_import_wires(0, breadcrumb_network_path);
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(downstream_connections) = outward_wires.get(&OutputConnector::Import(0)).cloned() else {
|
|
||||||
log::error!("Could not get outward wires for import in remove_import");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Disconnect all connections in the encapsulating network
|
|
||||||
for downstream_connection in &downstream_connections {
|
|
||||||
network_interface.disconnect_input(downstream_connection, breadcrumb_network_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
responses.add(NodeGraphMessage::UpdateImportsExports);
|
responses.add(NodeGraphMessage::UpdateImportsExports);
|
||||||
responses.add(NodeGraphMessage::SendWires);
|
responses.add(NodeGraphMessage::SendWires);
|
||||||
@@ -565,18 +554,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
|
|
||||||
// Disconnect all connections in the encapsulating network
|
// Disconnect all connections in the encapsulating network
|
||||||
if let Some((encapsulating_node, encapsulating_path)) = breadcrumb_network_path.split_last() {
|
if let Some((encapsulating_node, encapsulating_path)) = breadcrumb_network_path.split_last() {
|
||||||
let Some(outward_wires) = network_interface.outward_wires(encapsulating_path) else {
|
network_interface.disconnect_output_wires(&OutputConnector::primary_output(*encapsulating_node), encapsulating_path);
|
||||||
log::error!("Could not get outward wires in remove_import");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(downstream_connections) = outward_wires.get(&OutputConnector::node(*encapsulating_node, 0)).cloned() else {
|
|
||||||
log::error!("Could not get outward wires for import in remove_import");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
for downstream_connection in &downstream_connections {
|
|
||||||
network_interface.disconnect_input(downstream_connection, encapsulating_path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
responses.add(NodeGraphMessage::UpdateImportsExports);
|
responses.add(NodeGraphMessage::UpdateImportsExports);
|
||||||
@@ -618,7 +596,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
// Ensure that nodes can be grouped by checking if there is an unselected node between selected nodes
|
// Ensure that nodes can be grouped by checking if there is an unselected node between selected nodes
|
||||||
for selected_node_id in &selected_node_ids {
|
for selected_node_id in &selected_node_ids {
|
||||||
for input_index in 0..network_interface.number_of_inputs(selected_node_id, breadcrumb_network_path) {
|
for input_index in 0..network_interface.number_of_inputs(selected_node_id, breadcrumb_network_path) {
|
||||||
let input_connector = InputConnector::node(*selected_node_id, input_index);
|
let input_connector = InputConnector::node_at_index(*selected_node_id, input_index);
|
||||||
if let Some(upstream_deselected_node_id) = network_interface
|
if let Some(upstream_deselected_node_id) = network_interface
|
||||||
.upstream_output_connector(&input_connector, breadcrumb_network_path)
|
.upstream_output_connector(&input_connector, breadcrumb_network_path)
|
||||||
.and_then(|output_connector| output_connector.node_id())
|
.and_then(|output_connector| output_connector.node_id())
|
||||||
@@ -640,7 +618,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
}
|
}
|
||||||
for node_id in nodes_sorted_top_to_bottom {
|
for node_id in nodes_sorted_top_to_bottom {
|
||||||
for input_index in 0..network_interface.number_of_inputs(&node_id, breadcrumb_network_path) {
|
for input_index in 0..network_interface.number_of_inputs(&node_id, breadcrumb_network_path) {
|
||||||
let current_input_connector = InputConnector::node(node_id, input_index);
|
let current_input_connector = InputConnector::node_at_index(node_id, input_index);
|
||||||
let Some(upstream_connector) = network_interface.upstream_output_connector(¤t_input_connector, breadcrumb_network_path) else {
|
let Some(upstream_connector) = network_interface.upstream_output_connector(¤t_input_connector, breadcrumb_network_path) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -693,7 +671,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let center_of_selected_nodes_grid_space = IVec2::new((center_of_selected_nodes.x / 24. + 0.5).floor() as i32, (center_of_selected_nodes.y / 24. + 0.5).floor() as i32);
|
let center_of_selected_nodes_grid_space = IVec2::new((center_of_selected_nodes.x / 24. + 0.5).floor() as i32, (center_of_selected_nodes.y / 24. + 0.5).floor() as i32);
|
||||||
default_node_template.persistent_node_metadata.node_type_metadata = NodeTypePersistentMetadata::node(center_of_selected_nodes_grid_space - IVec2::new(3, 1));
|
default_node_template.node_type_metadata = NodeTypePersistentMetadata::node(center_of_selected_nodes_grid_space - IVec2::new(3, 1));
|
||||||
responses.add(DocumentMessage::AddTransaction);
|
responses.add(DocumentMessage::AddTransaction);
|
||||||
responses.add(NodeGraphMessage::InsertNode {
|
responses.add(NodeGraphMessage::InsertNode {
|
||||||
node_id: encapsulating_node_id,
|
node_id: encapsulating_node_id,
|
||||||
@@ -737,7 +715,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
for (input_index, output_connector) in input_connections.into_iter().enumerate() {
|
for (input_index, output_connector) in input_connections.into_iter().enumerate() {
|
||||||
responses.add(NodeGraphMessage::CreateWire {
|
responses.add(NodeGraphMessage::CreateWire {
|
||||||
output_connector,
|
output_connector,
|
||||||
input_connector: InputConnector::node(encapsulating_node_id, input_index),
|
input_connector: InputConnector::node_at_index(encapsulating_node_id, input_index),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (output_index, input_connectors) in output_connections.into_iter().enumerate() {
|
for (output_index, input_connectors) in output_connections.into_iter().enumerate() {
|
||||||
@@ -1179,6 +1157,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
data_type: self.wire_in_progress_type,
|
data_type: self.wire_in_progress_type,
|
||||||
thick: false,
|
thick: false,
|
||||||
dashed: false,
|
dashed: false,
|
||||||
|
is_list: false,
|
||||||
|
center_path_string: String::new(),
|
||||||
};
|
};
|
||||||
responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: Some(wire_path) });
|
responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: Some(wire_path) });
|
||||||
}
|
}
|
||||||
@@ -1350,7 +1330,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
self.shift_without_push = false;
|
self.shift_without_push = false;
|
||||||
|
|
||||||
// Reset all offsets to end the rubber banding while dragging
|
// Reset all offsets to end the rubber banding while dragging
|
||||||
network_interface.unload_stack_dependents_y_offset(selection_network_path);
|
network_interface.clear_drag_offsets(selection_network_path);
|
||||||
let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else {
|
let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else {
|
||||||
log::error!("Could not get selected nodes in PointerUp");
|
log::error!("Could not get selected nodes in PointerUp");
|
||||||
return;
|
return;
|
||||||
@@ -1379,7 +1359,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
{
|
{
|
||||||
network_interface.try_set_upstream_to_chain(&InputConnector::node(layer, 1), selection_network_path);
|
network_interface.try_set_upstream_to_chain(&InputConnector::layer_secondary_input(layer), selection_network_path);
|
||||||
}
|
}
|
||||||
responses.add(NodeGraphMessage::SendGraph);
|
responses.add(NodeGraphMessage::SendGraph);
|
||||||
|
|
||||||
@@ -1390,9 +1370,11 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
// Check if a single node was dragged onto a wire and that the node was dragged onto the wire
|
// Check if a single node was dragged onto a wire and that the node was dragged onto the wire
|
||||||
if selected_nodes.selected_nodes_ref().len() == 1 && !self.begin_dragging {
|
if selected_nodes.selected_nodes_ref().len() == 1 && !self.begin_dragging {
|
||||||
let selected_node_id = selected_nodes.selected_nodes_ref()[0];
|
let selected_node_id = selected_nodes.selected_nodes_ref()[0];
|
||||||
let has_primary_output_connection = network_interface
|
let has_primary_output_connection = network_interface.outward_wires(selection_network_path).is_some_and(|outward_wires| {
|
||||||
.outward_wires(selection_network_path)
|
outward_wires
|
||||||
.is_some_and(|outward_wires| outward_wires.get(&OutputConnector::node(selected_node_id, 0)).is_some_and(|outward_wires| !outward_wires.is_empty()));
|
.get(&OutputConnector::primary_output(selected_node_id))
|
||||||
|
.is_some_and(|outward_wires| !outward_wires.is_empty())
|
||||||
|
});
|
||||||
if !has_primary_output_connection {
|
if !has_primary_output_connection {
|
||||||
let Some(network) = network_interface.nested_network(selection_network_path) else {
|
let Some(network) = network_interface.nested_network(selection_network_path) else {
|
||||||
return;
|
return;
|
||||||
@@ -1417,7 +1399,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
// Prevent inserting on a link that is connected upstream to the selected node
|
// Prevent inserting on a link that is connected upstream to the selected node
|
||||||
for upstream_node in network_interface.upstream_flow_back_from_nodes(vec![selected_node_id], selection_network_path, network_interface::FlowType::UpstreamFlow) {
|
for upstream_node in network_interface.upstream_flow_back_from_nodes(vec![selected_node_id], selection_network_path, network_interface::FlowType::UpstreamFlow) {
|
||||||
for input_index in 0..network_interface.number_of_inputs(&upstream_node, selection_network_path) {
|
for input_index in 0..network_interface.number_of_inputs(&upstream_node, selection_network_path) {
|
||||||
wires_to_check.remove(&InputConnector::node(upstream_node, input_index));
|
wires_to_check.remove(&InputConnector::node_at_index(upstream_node, input_index));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1431,7 +1413,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (wire, is_stack) = network_interface.vector_wire_from_input(&input, preferences.graph_wire_style, selection_network_path)?;
|
let (wire, _center_line, is_stack) = network_interface.vector_wire_from_input(&input, preferences.graph_wire_style, selection_network_path)?;
|
||||||
|
|
||||||
let node_bbox = kurbo::Rect::new(node_bbox[0].x, node_bbox[0].y, node_bbox[1].x, node_bbox[1].y).to_path(DEFAULT_ACCURACY);
|
let node_bbox = kurbo::Rect::new(node_bbox[0].x, node_bbox[0].y, node_bbox[1].x, node_bbox[1].y).to_path(DEFAULT_ACCURACY);
|
||||||
let inside = bezpath_is_inside_bezpath(&wire, &node_bbox, None, None);
|
let inside = bezpath_is_inside_bezpath(&wire, &node_bbox, None, None);
|
||||||
@@ -1527,7 +1509,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
NodeGraphMessage::ShakeNode => {
|
NodeGraphMessage::ShakeNode => {
|
||||||
let Some(drag_start) = &self.drag_start else {
|
let Some((drag_start, _)) = &mut self.drag_start else {
|
||||||
log::error!("Drag start should be initialized when shaking a node");
|
log::error!("Drag start should be initialized when shaking a node");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -1545,7 +1527,11 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
.transform_point2(viewport_location);
|
.transform_point2(viewport_location);
|
||||||
|
|
||||||
// Collect the distance to move the shaken nodes after the undo
|
// Collect the distance to move the shaken nodes after the undo
|
||||||
let graph_delta = IVec2::new(((point.x - drag_start.0.start_x) / 24.).round() as i32, ((point.y - drag_start.0.start_y) / 24.).round() as i32);
|
let graph_delta = IVec2::new(((point.x - drag_start.start_x) / 24.).round() as i32, ((point.y - drag_start.start_y) / 24.).round() as i32);
|
||||||
|
|
||||||
|
// Keep the incremental rounding baseline in sync with the total shift reapplied below
|
||||||
|
drag_start.round_x = graph_delta.x;
|
||||||
|
drag_start.round_y = graph_delta.y;
|
||||||
|
|
||||||
// Undo to the state of the graph before shaking
|
// Undo to the state of the graph before shaking
|
||||||
responses.add(DocumentMessage::AbortTransaction);
|
responses.add(DocumentMessage::AbortTransaction);
|
||||||
@@ -1574,7 +1560,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
for selected_node in &all_selected_nodes {
|
for selected_node in &all_selected_nodes {
|
||||||
// Handle inputs of selected node
|
// Handle inputs of selected node
|
||||||
for input_index in 0..network_interface.number_of_inputs(selected_node, selection_network_path) {
|
for input_index in 0..network_interface.number_of_inputs(selected_node, selection_network_path) {
|
||||||
let input_connector = InputConnector::node(*selected_node, input_index);
|
let input_connector = InputConnector::node_at_index(*selected_node, input_index);
|
||||||
// Only disconnect inputs to non selected nodes
|
// Only disconnect inputs to non selected nodes
|
||||||
if network_interface
|
if network_interface
|
||||||
.upstream_output_connector(&input_connector, selection_network_path)
|
.upstream_output_connector(&input_connector, selection_network_path)
|
||||||
@@ -1585,13 +1571,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
}
|
}
|
||||||
|
|
||||||
let number_of_outputs = network_interface.number_of_outputs(selected_node, selection_network_path);
|
let number_of_outputs = network_interface.number_of_outputs(selected_node, selection_network_path);
|
||||||
let mut first_deselected_upstream_output = network_interface.upstream_output_connector(&InputConnector::node(*selected_node, 0), selection_network_path);
|
let mut first_deselected_upstream_output = network_interface.upstream_output_connector(&InputConnector::primary_input(*selected_node), selection_network_path);
|
||||||
while let Some(OutputConnector::Node { node_id, .. }) = &first_deselected_upstream_output {
|
while let Some(OutputConnector::Node { node_id, .. }) = &first_deselected_upstream_output {
|
||||||
if !all_selected_nodes.contains(node_id) {
|
if !all_selected_nodes.contains(node_id) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
first_deselected_upstream_output = network_interface.upstream_output_connector(&InputConnector::node(*node_id, 0), selection_network_path);
|
first_deselected_upstream_output = network_interface.upstream_output_connector(&InputConnector::primary_input(*node_id), selection_network_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(outward_wires) = network_interface.outward_wires(selection_network_path) else {
|
let Some(outward_wires) = network_interface.outward_wires(selection_network_path) else {
|
||||||
@@ -1614,7 +1600,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
// Handle reconnection
|
// Handle reconnection
|
||||||
// Find first non selected upstream node by primary flow
|
// Find first non selected upstream node by primary flow
|
||||||
if let Some(first_deselected_upstream_output) = first_deselected_upstream_output {
|
if let Some(first_deselected_upstream_output) = first_deselected_upstream_output {
|
||||||
let Some(downstream_connections_to_first_output) = outward_wires.get(&OutputConnector::node(*selected_node, 0)).cloned() else {
|
let Some(downstream_connections_to_first_output) = outward_wires.get(&OutputConnector::primary_output(*selected_node)).cloned() else {
|
||||||
log::error!("Could not get downstream_connections_to_first_output in shake node");
|
log::error!("Could not get downstream_connections_to_first_output in shake node");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -1726,7 +1712,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
if node_bbox[1].x >= document_bbox[0].x && node_bbox[0].x <= document_bbox[1].x && node_bbox[1].y >= document_bbox[0].y && node_bbox[0].y <= document_bbox[1].y {
|
// Expand the cull box by a grid cell so a node stays rendered until its connectors, which reach beyond its bounding box, also leave the viewport
|
||||||
|
let cull_margin = 24.;
|
||||||
|
if node_bbox[1].x + cull_margin >= document_bbox[0].x
|
||||||
|
&& node_bbox[0].x - cull_margin <= document_bbox[1].x
|
||||||
|
&& node_bbox[1].y + cull_margin >= document_bbox[0].y
|
||||||
|
&& node_bbox[0].y - cull_margin <= document_bbox[1].y
|
||||||
|
{
|
||||||
nodes.push(*node_id);
|
nodes.push(*node_id);
|
||||||
}
|
}
|
||||||
for error in &network_interface.resolved_types.node_graph_errors {
|
for error in &network_interface.resolved_types.node_graph_errors {
|
||||||
@@ -1742,6 +1734,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
responses.add(NodeGraphMessage::UpdateLayerPanel);
|
responses.add(NodeGraphMessage::UpdateLayerPanel);
|
||||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||||
responses.add(PropertiesPanelMessage::Refresh);
|
responses.add(PropertiesPanelMessage::Refresh);
|
||||||
|
responses.add(EventMessage::GraphChanged);
|
||||||
if breadcrumb_network_path == selection_network_path && graph_view_overlay_open {
|
if breadcrumb_network_path == selection_network_path && graph_view_overlay_open {
|
||||||
let nodes = self.collect_nodes(network_interface, breadcrumb_network_path);
|
let nodes = self.collect_nodes(network_interface, breadcrumb_network_path);
|
||||||
self.frontend_nodes = nodes.iter().map(|node| node.id).collect();
|
self.frontend_nodes = nodes.iter().map(|node| node.id).collect();
|
||||||
@@ -1769,16 +1762,16 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
let is_text_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER));
|
let is_text_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER));
|
||||||
let is_stroke_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER));
|
let is_stroke_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER));
|
||||||
let is_fill_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER));
|
let is_fill_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER));
|
||||||
let is_fill_input = is_fill_node && input_index == graphene_std::vector::fill::FillInput::INDEX;
|
let is_fill_input = is_fill_node && input_index == graphene_std::vector::fill::PaintInput::INDEX;
|
||||||
let is_shape_generator_node = reference.as_ref().is_some_and(|r| {
|
let is_shape_generator_node = reference.as_ref().is_some_and(|r| {
|
||||||
[regular_polygon::IDENTIFIER, star::IDENTIFIER, arc::IDENTIFIER, spiral::IDENTIFIER, grid::IDENTIFIER, arrow::IDENTIFIER]
|
[regular_polygon::IDENTIFIER, star::IDENTIFIER, arc::IDENTIFIER, spiral::IDENTIFIER, grid::IDENTIFIER, arrow::IDENTIFIER]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.any(|id| *r == DefinitionIdentifier::ProtoNode(id))
|
.any(|id| *r == DefinitionIdentifier::ProtoNode(id))
|
||||||
});
|
});
|
||||||
|
|
||||||
let input = NodeInput::value(value, false);
|
let input = NodeInput::value(*value, false);
|
||||||
responses.add(NodeGraphMessage::SetInput {
|
responses.add(NodeGraphMessage::SetInput {
|
||||||
input_connector: InputConnector::node(node_id, input_index),
|
input_connector: InputConnector::node_at_index(node_id, input_index),
|
||||||
input,
|
input,
|
||||||
});
|
});
|
||||||
responses.add(PropertiesPanelMessage::Refresh);
|
responses.add(PropertiesPanelMessage::Refresh);
|
||||||
@@ -1804,7 +1797,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
network_interface.shift_selected_nodes(direction, self.shift_without_push, selection_network_path);
|
network_interface.shift_selected_nodes(direction, self.shift_without_push, selection_network_path);
|
||||||
|
|
||||||
if !rubber_band {
|
if !rubber_band {
|
||||||
network_interface.unload_stack_dependents_y_offset(selection_network_path);
|
network_interface.clear_drag_offsets(selection_network_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if graph_view_overlay_open {
|
if graph_view_overlay_open {
|
||||||
@@ -2167,7 +2160,37 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
responses.add(NodeGraphMessage::SendGraph);
|
responses.add(NodeGraphMessage::SendGraph);
|
||||||
}
|
}
|
||||||
NodeGraphMessage::UpdateTypes { resolved_types, node_graph_errors } => {
|
NodeGraphMessage::UpdateTypes { resolved_types, node_graph_errors } => {
|
||||||
|
// Hidden passthrough nodes let a wire borrow its color and rank from an upstream node, so any type change can restyle wires whose own node is unchanged.
|
||||||
|
// Compare each displayed wire's style (color, rank) across the update and unload only those that changed, so value-only recompiles keep their built wire paths.
|
||||||
|
let types_changed = !resolved_types.add.is_empty() || !resolved_types.remove.is_empty();
|
||||||
|
let wire_style = |network_interface: &mut NodeNetworkInterface, input: &InputConnector| {
|
||||||
|
network_interface.upstream_output_connector(input, breadcrumb_network_path).map(|output| {
|
||||||
|
let output_type = network_interface.output_type(&output, breadcrumb_network_path);
|
||||||
|
(output_type.displayed_type(), output_type.is_list())
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let styles_before = types_changed.then(|| {
|
||||||
|
network_interface
|
||||||
|
.node_graph_input_connectors(breadcrumb_network_path)
|
||||||
|
.into_iter()
|
||||||
|
.map(|input| {
|
||||||
|
let style = wire_style(network_interface, &input);
|
||||||
|
(input, style)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
|
||||||
network_interface.resolved_types.update(resolved_types, node_graph_errors);
|
network_interface.resolved_types.update(resolved_types, node_graph_errors);
|
||||||
|
|
||||||
|
if let Some(styles_before) = styles_before {
|
||||||
|
for (input, style_before) in styles_before {
|
||||||
|
if wire_style(network_interface, &input) != style_before {
|
||||||
|
network_interface.unload_wire(&input, breadcrumb_network_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responses.add(NodeGraphMessage::SendGraph);
|
||||||
}
|
}
|
||||||
NodeGraphMessage::UpdateActionButtons => {
|
NodeGraphMessage::UpdateActionButtons => {
|
||||||
if selection_network_path == breadcrumb_network_path {
|
if selection_network_path == breadcrumb_network_path {
|
||||||
@@ -2406,7 +2429,7 @@ impl NodeGraphMessageHandler {
|
|||||||
if subgraph_path_names_length >= 2 {
|
if subgraph_path_names_length >= 2 {
|
||||||
widgets.extend([
|
widgets.extend([
|
||||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
BreadcrumbTrailButtons::new(subgraph_path_names)
|
BreadcrumbTrailButtons::new(subgraph_path_names.iter().map(|name| truncate_breadcrumb_label(name)).collect())
|
||||||
.on_update(move |index| {
|
.on_update(move |index| {
|
||||||
DocumentMessage::ExitNestedNetwork {
|
DocumentMessage::ExitNestedNetwork {
|
||||||
steps_back: subgraph_path_names_length - (*index as usize) - 1,
|
steps_back: subgraph_path_names_length - (*index as usize) - 1,
|
||||||
@@ -2591,7 +2614,7 @@ impl NodeGraphMessageHandler {
|
|||||||
.popover_layout({
|
.popover_layout({
|
||||||
let compatible_type = context
|
let compatible_type = context
|
||||||
.network_interface
|
.network_interface
|
||||||
.upstream_output_connector(&InputConnector::node(layer, 1), &[])
|
.upstream_output_connector(&InputConnector::layer_secondary_input(layer), &[])
|
||||||
.and_then(|upstream_output| context.network_interface.output_type(&upstream_output, &[]).add_node_string());
|
.and_then(|upstream_output| context.network_interface.output_type(&upstream_output, &[]).add_node_string());
|
||||||
|
|
||||||
let mut node_chooser = NodeCatalog::new();
|
let mut node_chooser = NodeCatalog::new();
|
||||||
@@ -2688,7 +2711,7 @@ impl NodeGraphMessageHandler {
|
|||||||
};
|
};
|
||||||
let mut nodes = Vec::new();
|
let mut nodes = Vec::new();
|
||||||
for (node_id, visible) in network.nodes.iter().map(|(node_id, node)| (*node_id, node.visible)).collect::<Vec<_>>() {
|
for (node_id, visible) in network.nodes.iter().map(|(node_id, node)| (*node_id, node.visible)).collect::<Vec<_>>() {
|
||||||
let primary_input_connector = InputConnector::node(node_id, 0);
|
let primary_input_connector = InputConnector::primary_input(node_id);
|
||||||
|
|
||||||
let primary_input = if network_interface
|
let primary_input = if network_interface
|
||||||
.input_from_connector(&primary_input_connector, breadcrumb_network_path)
|
.input_from_connector(&primary_input_connector, breadcrumb_network_path)
|
||||||
@@ -2699,10 +2722,10 @@ impl NodeGraphMessageHandler {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
let exposed_inputs = (1..network_interface.number_of_inputs(&node_id, breadcrumb_network_path))
|
let exposed_inputs = (1..network_interface.number_of_inputs(&node_id, breadcrumb_network_path))
|
||||||
.filter_map(|input_index| network_interface.frontend_input_from_connector(&InputConnector::node(node_id, input_index), breadcrumb_network_path))
|
.filter_map(|input_index| network_interface.frontend_input_from_connector(&InputConnector::node_at_index(node_id, input_index), breadcrumb_network_path))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let primary_output = network_interface.frontend_output_from_connector(&OutputConnector::node(node_id, 0), breadcrumb_network_path);
|
let primary_output = network_interface.frontend_output_from_connector(&OutputConnector::primary_output(node_id), breadcrumb_network_path);
|
||||||
|
|
||||||
let exposed_outputs = (1..network_interface.number_of_outputs(&node_id, breadcrumb_network_path))
|
let exposed_outputs = (1..network_interface.number_of_outputs(&node_id, breadcrumb_network_path))
|
||||||
.filter_map(|output_index| network_interface.frontend_output_from_connector(&OutputConnector::node(node_id, output_index), breadcrumb_network_path))
|
.filter_map(|output_index| network_interface.frontend_output_from_connector(&OutputConnector::node(node_id, output_index), breadcrumb_network_path))
|
||||||
@@ -2830,7 +2853,7 @@ impl NodeGraphMessageHandler {
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
let clippable = layer.can_be_clipped(network_interface.document_metadata());
|
let clippable = layer.can_be_clipped(network_interface.document_metadata()) && network_interface.layer_hosts_blending_nodes(&node_id, &[]);
|
||||||
|
|
||||||
let data = LayerPanelEntry {
|
let data = LayerPanelEntry {
|
||||||
id: node_id,
|
id: node_id,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -105,6 +105,13 @@ pub struct FrontendNode {
|
|||||||
pub locked: bool,
|
pub locked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum InputTypeConstraint {
|
||||||
|
Limited(std::collections::BTreeSet<String>),
|
||||||
|
All,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct FrontendNodeType {
|
pub struct FrontendNodeType {
|
||||||
@@ -112,7 +119,7 @@ pub struct FrontendNodeType {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub category: String,
|
pub category: String,
|
||||||
#[serde(rename = "inputTypes")]
|
#[serde(rename = "inputTypes")]
|
||||||
pub input_types: Vec<String>,
|
pub input_types: Vec<InputTypeConstraint>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::messages::prelude::*;
|
|||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use graphene_std::color::SRGBA8;
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::renderer::Quad;
|
use graphene_std::renderer::Quad;
|
||||||
use graphene_std::vector::style::FillChoiceUI;
|
use graphene_std::vector::style::FillChoice;
|
||||||
|
|
||||||
fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) {
|
fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) {
|
||||||
let origin = document.snapping_state.grid.origin;
|
let origin = document.snapping_state.grid.origin;
|
||||||
@@ -274,7 +274,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
|||||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||||
]);
|
]);
|
||||||
color_widgets.push(
|
color_widgets.push(
|
||||||
ColorInput::new(FillChoiceUI::Solid(SRGBA8::from_hex_str(&grid.color).unwrap_or(SRGBA8::BLACK)))
|
ColorInput::new(FillChoice::<SRGBA8>::Solid(SRGBA8::from_hex_str(&grid.color).unwrap_or(SRGBA8::BLACK)))
|
||||||
.tooltip_label("Grid Display Color")
|
.tooltip_label("Grid Display Color")
|
||||||
.allow_none(false)
|
.allow_none(false)
|
||||||
.on_update(update_val::<ColorInput, _>(grid, |grid, color| {
|
.on_update(update_val::<ColorInput, _>(grid, |grid, color| {
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
use super::utility_types::{DrawHandles, OverlayContext};
|
use super::utility_types::{DrawHandles, OverlayContext};
|
||||||
use crate::consts::HIDE_HANDLE_DISTANCE;
|
use crate::consts::{HIDE_HANDLE_DISTANCE, SNAP_POINT_TOLERANCE};
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||||
use crate::messages::portfolio::fonts::FALLBACK_FONT_RESOURCE;
|
pub use crate::messages::portfolio::document::utility_types::text_metrics::text_width;
|
||||||
use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState};
|
use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState};
|
||||||
|
use crate::messages::tool::common_functionality::utility_functions::closest_open_path_endpoint;
|
||||||
use crate::messages::tool::tool_messages::tool_prelude::DocumentMessageHandler;
|
use crate::messages::tool::tool_messages::tool_prelude::DocumentMessageHandler;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graphene_std::subpath::{Bezier, BezierHandles};
|
use graphene_std::vector::misc::{BezierHandles, ManipulatorPointId, point_to_dvec2, segment_to_handles};
|
||||||
use graphene_std::text::{TextAlign, TextContext, TypesettingConfig};
|
|
||||||
use graphene_std::vector::misc::ManipulatorPointId;
|
|
||||||
use graphene_std::vector::{PointId, SegmentId, Vector};
|
use graphene_std::vector::{PointId, SegmentId, Vector};
|
||||||
|
use kurbo::{Affine, ParamCurve, PathSeg};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{LazyLock, Mutex};
|
|
||||||
#[cfg(target_family = "wasm")]
|
#[cfg(target_family = "wasm")]
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
|
|
||||||
@@ -61,7 +60,7 @@ pub fn selected_segments_for_layer(vector: &Vector, state: &SelectedLayerState)
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// Adding segments which are are connected to selected anchors
|
// Adding segments which are are connected to selected anchors
|
||||||
for (segment_id, _bezier, start, end) in vector.segment_bezier_iter() {
|
for (segment_id, _, start, end) in vector.segment_iter() {
|
||||||
if selected_anchors.contains(&start) || selected_anchors.contains(&end) {
|
if selected_anchors.contains(&start) || selected_anchors.contains(&end) {
|
||||||
selected_segments.push(segment_id);
|
selected_segments.push(segment_id);
|
||||||
}
|
}
|
||||||
@@ -69,23 +68,25 @@ pub fn selected_segments_for_layer(vector: &Vector, state: &SelectedLayerState)
|
|||||||
selected_segments
|
selected_segments
|
||||||
}
|
}
|
||||||
|
|
||||||
fn overlay_bezier_handles(bezier: Bezier, segment_id: SegmentId, transform: DAffine2, is_selected: impl Fn(ManipulatorPointId) -> bool, overlay_context: &mut OverlayContext) {
|
fn overlay_bezier_handles(segment: PathSeg, segment_id: SegmentId, transform: DAffine2, is_selected: impl Fn(ManipulatorPointId) -> bool, overlay_context: &mut OverlayContext) {
|
||||||
let bezier = bezier.apply_transformation(|point| transform.transform_point2(point));
|
let segment = Affine::new(transform.to_cols_array()) * segment;
|
||||||
|
let segment_start = point_to_dvec2(segment.start());
|
||||||
|
let segment_end = point_to_dvec2(segment.end());
|
||||||
let not_under_anchor = |position: DVec2, anchor: DVec2| position.distance_squared(anchor) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE;
|
let not_under_anchor = |position: DVec2, anchor: DVec2| position.distance_squared(anchor) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE;
|
||||||
|
|
||||||
match bezier.handles {
|
match segment_to_handles(&segment) {
|
||||||
BezierHandles::Quadratic { handle } if not_under_anchor(handle, bezier.start) && not_under_anchor(handle, bezier.end) => {
|
BezierHandles::Quadratic { handle } if not_under_anchor(handle, segment_start) && not_under_anchor(handle, segment_end) => {
|
||||||
overlay_context.line(handle, bezier.start, None, None);
|
overlay_context.line(handle, segment_start, None, None);
|
||||||
overlay_context.line(handle, bezier.end, None, None);
|
overlay_context.line(handle, segment_end, None, None);
|
||||||
overlay_context.manipulator_handle(handle, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None);
|
overlay_context.manipulator_handle(handle, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None);
|
||||||
}
|
}
|
||||||
BezierHandles::Cubic { handle_start, handle_end } => {
|
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||||
if not_under_anchor(handle_start, bezier.start) {
|
if not_under_anchor(handle_start, segment_start) {
|
||||||
overlay_context.line(handle_start, bezier.start, None, None);
|
overlay_context.line(handle_start, segment_start, None, None);
|
||||||
overlay_context.manipulator_handle(handle_start, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None);
|
overlay_context.manipulator_handle(handle_start, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None);
|
||||||
}
|
}
|
||||||
if not_under_anchor(handle_end, bezier.end) {
|
if not_under_anchor(handle_end, segment_end) {
|
||||||
overlay_context.line(handle_end, bezier.end, None, None);
|
overlay_context.line(handle_end, segment_end, None, None);
|
||||||
overlay_context.manipulator_handle(handle_end, is_selected(ManipulatorPointId::EndHandle(segment_id)), None);
|
overlay_context.manipulator_handle(handle_end, is_selected(ManipulatorPointId::EndHandle(segment_id)), None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,7 +95,7 @@ fn overlay_bezier_handles(bezier: Bezier, segment_id: SegmentId, transform: DAff
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn overlay_bezier_handle_specific_point(
|
fn overlay_bezier_handle_specific_point(
|
||||||
bezier: Bezier,
|
segment: PathSeg,
|
||||||
segment_id: SegmentId,
|
segment_id: SegmentId,
|
||||||
(start, end): (PointId, PointId),
|
(start, end): (PointId, PointId),
|
||||||
point_to_render: PointId,
|
point_to_render: PointId,
|
||||||
@@ -102,24 +103,24 @@ fn overlay_bezier_handle_specific_point(
|
|||||||
is_selected: impl Fn(ManipulatorPointId) -> bool,
|
is_selected: impl Fn(ManipulatorPointId) -> bool,
|
||||||
overlay_context: &mut OverlayContext,
|
overlay_context: &mut OverlayContext,
|
||||||
) {
|
) {
|
||||||
let bezier = bezier.apply_transformation(|point| transform.transform_point2(point));
|
let segment = Affine::new(transform.to_cols_array()) * segment;
|
||||||
|
let segment_start = point_to_dvec2(segment.start());
|
||||||
|
let segment_end = point_to_dvec2(segment.end());
|
||||||
let not_under_anchor = |position: DVec2, anchor: DVec2| position.distance_squared(anchor) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE;
|
let not_under_anchor = |position: DVec2, anchor: DVec2| position.distance_squared(anchor) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE;
|
||||||
|
|
||||||
match bezier.handles {
|
match segment_to_handles(&segment) {
|
||||||
BezierHandles::Quadratic { handle } => {
|
BezierHandles::Quadratic { handle } if not_under_anchor(handle, segment_start) && not_under_anchor(handle, segment_end) => {
|
||||||
if not_under_anchor(handle, bezier.start) && not_under_anchor(handle, bezier.end) {
|
let anchor = if start == point_to_render { segment_start } else { segment_end };
|
||||||
let end = if start == point_to_render { bezier.start } else { bezier.end };
|
overlay_context.line(handle, anchor, None, None);
|
||||||
overlay_context.line(handle, end, None, None);
|
overlay_context.manipulator_handle(handle, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None);
|
||||||
overlay_context.manipulator_handle(handle, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
BezierHandles::Cubic { handle_start, handle_end } => {
|
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||||
if not_under_anchor(handle_start, bezier.start) && (point_to_render == start) {
|
if not_under_anchor(handle_start, segment_start) && (point_to_render == start) {
|
||||||
overlay_context.line(handle_start, bezier.start, None, None);
|
overlay_context.line(handle_start, segment_start, None, None);
|
||||||
overlay_context.manipulator_handle(handle_start, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None);
|
overlay_context.manipulator_handle(handle_start, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None);
|
||||||
}
|
}
|
||||||
if not_under_anchor(handle_end, bezier.end) && (point_to_render == end) {
|
if not_under_anchor(handle_end, segment_end) && (point_to_render == end) {
|
||||||
overlay_context.line(handle_end, bezier.end, None, None);
|
overlay_context.line(handle_end, segment_end, None, None);
|
||||||
overlay_context.manipulator_handle(handle_end, is_selected(ManipulatorPointId::EndHandle(segment_id)), None);
|
overlay_context.manipulator_handle(handle_end, is_selected(ManipulatorPointId::EndHandle(segment_id)), None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,7 +133,7 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
|
|||||||
let display_handles = overlay_context.visibility_settings.handles();
|
let display_handles = overlay_context.visibility_settings.handles();
|
||||||
let display_anchors = overlay_context.visibility_settings.anchors();
|
let display_anchors = overlay_context.visibility_settings.anchors();
|
||||||
|
|
||||||
for layer in document.network_interface.selected_nodes().selected_layers(document.metadata()) {
|
for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) {
|
||||||
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue };
|
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue };
|
||||||
let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface);
|
let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface);
|
||||||
if display_path {
|
if display_path {
|
||||||
@@ -154,23 +155,23 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
|
|||||||
|
|
||||||
match draw_handles {
|
match draw_handles {
|
||||||
DrawHandles::All => {
|
DrawHandles::All => {
|
||||||
vector.segment_bezier_iter().for_each(|(segment_id, bezier, _start, _end)| {
|
vector.segment_iter().for_each(|(segment_id, segment, _start, _end)| {
|
||||||
overlay_bezier_handles(bezier, segment_id, transform, is_selected, overlay_context);
|
overlay_bezier_handles(segment, segment_id, transform, is_selected, overlay_context);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
DrawHandles::SelectedAnchors(ref selected_segments) => {
|
DrawHandles::SelectedAnchors(ref selected_segments) => {
|
||||||
let Some(focused_segments) = selected_segments.get(&layer) else { continue };
|
let Some(focused_segments) = selected_segments.get(&layer) else { continue };
|
||||||
|
|
||||||
vector
|
vector
|
||||||
.segment_bezier_iter()
|
.segment_iter()
|
||||||
.filter(|(segment_id, ..)| focused_segments.contains(segment_id))
|
.filter(|(segment_id, ..)| focused_segments.contains(segment_id))
|
||||||
.for_each(|(segment_id, bezier, _start, _end)| {
|
.for_each(|(segment_id, segment, _start, _end)| {
|
||||||
overlay_bezier_handles(bezier, segment_id, transform, is_selected, overlay_context);
|
overlay_bezier_handles(segment, segment_id, transform, is_selected, overlay_context);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (segment_id, bezier, start, end) in vector.segment_bezier_iter() {
|
for (segment_id, segment, start, end) in vector.segment_iter() {
|
||||||
if let Some((corresponding_anchor, _)) = opposite_handles_data.iter().find(|(_, adj_segment_id)| adj_segment_id == &segment_id) {
|
if let Some((corresponding_anchor, _)) = opposite_handles_data.iter().find(|(_, adj_segment_id)| adj_segment_id == &segment_id) {
|
||||||
overlay_bezier_handle_specific_point(bezier, segment_id, (start, end), *corresponding_anchor, transform, is_selected, overlay_context);
|
overlay_bezier_handle_specific_point(segment, segment_id, (start, end), *corresponding_anchor, transform, is_selected, overlay_context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,14 +179,14 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
|
|||||||
let Some(segment_endpoints) = segment_endpoints_by_layer.get(&layer) else { continue };
|
let Some(segment_endpoints) = segment_endpoints_by_layer.get(&layer) else { continue };
|
||||||
|
|
||||||
vector
|
vector
|
||||||
.segment_bezier_iter()
|
.segment_iter()
|
||||||
.filter(|(segment_id, ..)| segment_endpoints.contains_key(segment_id))
|
.filter(|(segment_id, ..)| segment_endpoints.contains_key(segment_id))
|
||||||
.for_each(|(segment_id, bezier, start, end)| {
|
.for_each(|(segment_id, segment, start, end)| {
|
||||||
if segment_endpoints.get(&segment_id).unwrap().len() == 1 {
|
if segment_endpoints.get(&segment_id).unwrap().len() == 1 {
|
||||||
let point_to_render = segment_endpoints.get(&segment_id).unwrap()[0];
|
let point_to_render = segment_endpoints.get(&segment_id).unwrap()[0];
|
||||||
overlay_bezier_handle_specific_point(bezier, segment_id, (start, end), point_to_render, transform, is_selected, overlay_context);
|
overlay_bezier_handle_specific_point(segment, segment_id, (start, end), point_to_render, transform, is_selected, overlay_context);
|
||||||
} else {
|
} else {
|
||||||
overlay_bezier_handles(bezier, segment_id, transform, is_selected, overlay_context);
|
overlay_bezier_handles(segment, segment_id, transform, is_selected, overlay_context);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -201,43 +202,41 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &mut ShapeState, overlay_context: &mut OverlayContext) {
|
/// Draws an anchor overlay at each endpoint of every open path on the selected visible layers, in the selected style for endpoints that are part of the path editing selection.
|
||||||
|
/// Given a pointer position, the endpoint a press there would continue from is drawn in the hover style instead.
|
||||||
|
pub fn open_path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &ShapeState, pointer: Option<DVec2>, overlay_context: &mut OverlayContext) {
|
||||||
if !overlay_context.visibility_settings.anchors() {
|
if !overlay_context.visibility_settings.anchors() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for layer in document.network_interface.selected_nodes().selected_layers(document.metadata()) {
|
let selected_nodes = document.network_interface.selected_nodes();
|
||||||
let Some(vector) = document.network_interface.compute_modified_vector(layer) else {
|
let is_selected = |layer: LayerNodeIdentifier, id: PointId| {
|
||||||
continue;
|
shape_editor
|
||||||
};
|
.selected_shape_state
|
||||||
let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface);
|
.get(&layer)
|
||||||
let selected = shape_editor.selected_shape_state.get(&layer);
|
.is_some_and(|state| state.is_point_selected(ManipulatorPointId::Anchor(id)))
|
||||||
let is_selected = |selected: Option<&SelectedLayerState>, point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_point_selected(point));
|
};
|
||||||
|
let hovered = pointer.and_then(|pointer| closest_open_path_endpoint(document, pointer, SNAP_POINT_TOLERANCE, selected_nodes.selected_visible_layers(&document.network_interface)));
|
||||||
|
|
||||||
for point in vector.anchor_endpoints() {
|
for layer in selected_nodes.selected_visible_layers(&document.network_interface) {
|
||||||
let Some(position) = vector.point_domain.position_from_id(point) else { continue };
|
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue };
|
||||||
let position = transform.transform_point2(position);
|
let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface);
|
||||||
overlay_context.manipulator_anchor(position, is_selected(selected, ManipulatorPointId::Anchor(point)), None);
|
|
||||||
|
for id in vector.anchor_endpoints() {
|
||||||
|
if hovered.is_some_and(|(hovered_layer, hovered_id, _)| hovered_layer == layer && hovered_id == id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(position) = vector.point_domain.position_from_id(id) else { continue };
|
||||||
|
|
||||||
|
overlay_context.manipulator_anchor(transform.transform_point2(position), is_selected(layer, id), None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub static GLOBAL_TEXT_CONTEXT: LazyLock<Mutex<TextContext>> = LazyLock::new(|| Mutex::new(TextContext::default()));
|
// Drawn last so its halo sits above any other endpoint at the same spot
|
||||||
|
if let Some((layer, id, position)) = hovered {
|
||||||
pub fn text_width(text: &str, font_size: f64) -> f64 {
|
let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface);
|
||||||
let typesetting = TypesettingConfig {
|
overlay_context.hover_manipulator_anchor(transform.transform_point2(position), is_selected(layer, id));
|
||||||
font_size,
|
}
|
||||||
line_height_ratio: 1.2,
|
|
||||||
letter_spacing: 0.,
|
|
||||||
letter_tilt: 0.,
|
|
||||||
max_width: None,
|
|
||||||
max_height: None,
|
|
||||||
align: TextAlign::AlignLeft,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut text_context = GLOBAL_TEXT_CONTEXT.lock().expect("Failed to lock global text context");
|
|
||||||
let bounds = text_context.bounding_box(text, &FALLBACK_FONT_RESOURCE, typesetting, false);
|
|
||||||
bounds.x
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hex_to_rgba_u8(hex: &str) -> [u8; 4] {
|
pub fn hex_to_rgba_u8(hex: &str) -> [u8; 4] {
|
||||||
|
|||||||
@@ -3,19 +3,19 @@ use crate::consts::{
|
|||||||
COLOR_OVERLAY_YELLOW_DULL, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS,
|
COLOR_OVERLAY_YELLOW_DULL, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS,
|
||||||
GRADIENT_MIDPOINT_DIAMOND_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE,
|
GRADIENT_MIDPOINT_DIAMOND_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE,
|
||||||
};
|
};
|
||||||
use crate::messages::portfolio::document::overlays::utility_functions::{GLOBAL_TEXT_CONTEXT, hex_to_rgba_u8};
|
use crate::messages::portfolio::document::overlays::utility_functions::hex_to_rgba_u8;
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use crate::messages::portfolio::fonts::FALLBACK_FONT_RESOURCE;
|
use crate::messages::portfolio::fonts::FALLBACK_FONT_RESOURCE;
|
||||||
use crate::messages::prelude::Message;
|
use crate::messages::prelude::Message;
|
||||||
use crate::messages::prelude::ViewportMessageHandler;
|
use crate::messages::prelude::ViewportMessageHandler;
|
||||||
|
use crate::messages::tool::common_functionality::shapes::shape_utility::format_rounded;
|
||||||
use core::borrow::Borrow;
|
use core::borrow::Borrow;
|
||||||
use core::f64::consts::{FRAC_PI_2, PI, TAU};
|
use core::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graphene_std::ATTR_TRANSFORM;
|
use graphene_std::ATTR_TRANSFORM;
|
||||||
use graphene_std::list::List;
|
use graphene_std::list::List;
|
||||||
use graphene_std::math::quad::Quad;
|
use graphene_std::math::quad::Quad;
|
||||||
use graphene_std::subpath::{self, Subpath};
|
use graphene_std::text::{TextAlign, TextContext, TypesettingConfig};
|
||||||
use graphene_std::text::{TextAlign, TypesettingConfig};
|
|
||||||
use graphene_std::vector::click_target::ClickTargetType;
|
use graphene_std::vector::click_target::ClickTargetType;
|
||||||
use graphene_std::vector::misc::point_to_dvec2;
|
use graphene_std::vector::misc::point_to_dvec2;
|
||||||
use graphene_std::vector::{PointId, SegmentId, Vector};
|
use graphene_std::vector::{PointId, SegmentId, Vector};
|
||||||
@@ -403,14 +403,14 @@ impl OverlayContext {
|
|||||||
|
|
||||||
/// Fills the area inside the path. Assumes `color` is in gamma space.
|
/// Fills the area inside the path. Assumes `color` is in gamma space.
|
||||||
/// Used by the Pen tool to show the path being closed.
|
/// Used by the Pen tool to show the path being closed.
|
||||||
pub fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
|
pub fn fill_path(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
|
||||||
self.internal().fill_path(subpaths, transform, color);
|
self.internal().fill_path(bezpath, transform, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fills the area inside the path with a pattern. Assumes `color` is an sRGB hex string.
|
/// Fills the area inside the path with a pattern. Assumes `color` is an sRGB hex string.
|
||||||
/// Used by the fill tool to show the area to be filled.
|
/// Used by the fill tool to show the area to be filled.
|
||||||
pub fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
|
pub fn fill_path_pattern(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
|
||||||
self.internal().fill_path_pattern(subpaths, transform, color);
|
self.internal().fill_path_pattern(bezpath, transform, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
|
pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
|
||||||
@@ -977,49 +977,25 @@ impl OverlayContextInternal {
|
|||||||
path.push(bezier.as_path_el());
|
path.push(bezier.as_path_el());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) -> BezPath {
|
fn push_path(&mut self, bezpath: &BezPath, transform: DAffine2) -> BezPath {
|
||||||
let mut path = BezPath::new();
|
let mut path = BezPath::new();
|
||||||
|
|
||||||
for subpath in subpaths {
|
let snap_start = |context: &Self, point: kurbo::Point| {
|
||||||
let subpath = subpath.borrow();
|
let snapped = context.snap_to_physical_pixel(transform.transform_point2(point_to_dvec2(point)));
|
||||||
let mut curves = subpath.iter().peekable();
|
kurbo::Point::new(snapped.x, snapped.y)
|
||||||
|
};
|
||||||
|
let snap_center = |context: &Self, point: kurbo::Point| {
|
||||||
|
let snapped = context.snap_to_physical_pixel_center(transform.transform_point2(point_to_dvec2(point)));
|
||||||
|
kurbo::Point::new(snapped.x, snapped.y)
|
||||||
|
};
|
||||||
|
|
||||||
let Some(first) = curves.peek() else {
|
for element in bezpath.elements() {
|
||||||
continue;
|
match *element {
|
||||||
};
|
kurbo::PathEl::MoveTo(point) => path.move_to(snap_start(self, point)),
|
||||||
|
kurbo::PathEl::LineTo(point) => path.line_to(snap_center(self, point)),
|
||||||
let start_point = transform.transform_point2(point_to_dvec2(first.start()));
|
kurbo::PathEl::QuadTo(a, b) => path.quad_to(snap_center(self, a), snap_center(self, b)),
|
||||||
let start_point = self.snap_to_physical_pixel(start_point);
|
kurbo::PathEl::CurveTo(a, b, c) => path.curve_to(snap_center(self, a), snap_center(self, b), snap_center(self, c)),
|
||||||
path.move_to(kurbo::Point::new(start_point.x, start_point.y));
|
kurbo::PathEl::ClosePath => path.close_path(),
|
||||||
|
|
||||||
for curve in curves {
|
|
||||||
match curve {
|
|
||||||
PathSeg::Line(line) => {
|
|
||||||
let a = transform.transform_point2(point_to_dvec2(line.p1));
|
|
||||||
let a = self.snap_to_physical_pixel_center(a);
|
|
||||||
path.line_to(kurbo::Point::new(a.x, a.y));
|
|
||||||
}
|
|
||||||
PathSeg::Quad(quad_bez) => {
|
|
||||||
let a = transform.transform_point2(point_to_dvec2(quad_bez.p1));
|
|
||||||
let b = transform.transform_point2(point_to_dvec2(quad_bez.p2));
|
|
||||||
let a = self.snap_to_physical_pixel_center(a);
|
|
||||||
let b = self.snap_to_physical_pixel_center(b);
|
|
||||||
path.quad_to(kurbo::Point::new(a.x, a.y), kurbo::Point::new(b.x, b.y));
|
|
||||||
}
|
|
||||||
PathSeg::Cubic(cubic_bez) => {
|
|
||||||
let a = transform.transform_point2(point_to_dvec2(cubic_bez.p1));
|
|
||||||
let b = transform.transform_point2(point_to_dvec2(cubic_bez.p2));
|
|
||||||
let c = transform.transform_point2(point_to_dvec2(cubic_bez.p3));
|
|
||||||
let a = self.snap_to_physical_pixel_center(a);
|
|
||||||
let b = self.snap_to_physical_pixel_center(b);
|
|
||||||
let c = self.snap_to_physical_pixel_center(c);
|
|
||||||
path.curve_to(kurbo::Point::new(a.x, a.y), kurbo::Point::new(b.x, b.y), kurbo::Point::new(c.x, c.y));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if subpath.closed() {
|
|
||||||
path.close_path();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1028,20 +1004,19 @@ impl OverlayContextInternal {
|
|||||||
|
|
||||||
/// Used by the Select tool to outline a path or a free point when selected or hovered.
|
/// Used by the Select tool to outline a path or a free point when selected or hovered.
|
||||||
fn outline(&mut self, target_types: impl Iterator<Item = impl Borrow<ClickTargetType>>, transform: DAffine2, color: Option<&str>) {
|
fn outline(&mut self, target_types: impl Iterator<Item = impl Borrow<ClickTargetType>>, transform: DAffine2, color: Option<&str>) {
|
||||||
let mut subpaths: Vec<subpath::Subpath<PointId>> = vec![];
|
let mut combined = BezPath::new();
|
||||||
|
|
||||||
for target_type in target_types {
|
for target_type in target_types {
|
||||||
match target_type.borrow() {
|
match target_type.borrow() {
|
||||||
ClickTargetType::FreePoint(point) => {
|
ClickTargetType::FreePoint(point) => {
|
||||||
self.manipulator_anchor(transform.transform_point2(point.position), false, None);
|
self.manipulator_anchor(transform.transform_point2(point.position), false, None);
|
||||||
}
|
}
|
||||||
ClickTargetType::Subpath(subpath) => subpaths.push(subpath.clone()),
|
ClickTargetType::Path(bezpath) => combined.extend(bezpath.elements().iter().copied()),
|
||||||
ClickTargetType::CompoundPath(compound) => subpaths.extend(compound.iter().cloned()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !subpaths.is_empty() {
|
if !combined.is_empty() {
|
||||||
let path = self.push_path(subpaths.iter(), transform);
|
let path = self.push_path(&combined, transform);
|
||||||
let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
|
let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||||
|
|
||||||
self.scene.stroke(&kurbo::Stroke::new(1.), self.get_transform(), Self::parse_color(color), None, &path);
|
self.scene.stroke(&kurbo::Stroke::new(1.), self.get_transform(), Self::parse_color(color), None, &path);
|
||||||
@@ -1050,15 +1025,15 @@ impl OverlayContextInternal {
|
|||||||
|
|
||||||
/// Fills the area inside the path. Assumes `color` is in gamma space.
|
/// Fills the area inside the path. Assumes `color` is in gamma space.
|
||||||
/// Used by the Pen tool to show the path being closed.
|
/// Used by the Pen tool to show the path being closed.
|
||||||
fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
|
fn fill_path(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
|
||||||
let path = self.push_path(subpaths, transform);
|
let path = self.push_path(bezpath, transform);
|
||||||
|
|
||||||
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), Self::parse_color(color), None, &path);
|
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), Self::parse_color(color), None, &path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fills the area inside the path with a pattern. Assumes `color` is an sRGB hex string.
|
/// Fills the area inside the path with a pattern. Assumes `color` is an sRGB hex string.
|
||||||
/// Used by the fill tool to show the area to be filled.
|
/// Used by the fill tool to show the area to be filled.
|
||||||
fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
|
fn fill_path_pattern(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
|
||||||
const PATTERN_WIDTH: u32 = 4;
|
const PATTERN_WIDTH: u32 = 4;
|
||||||
const PATTERN_HEIGHT: u32 = 4;
|
const PATTERN_HEIGHT: u32 = 4;
|
||||||
|
|
||||||
@@ -1095,7 +1070,7 @@ impl OverlayContextInternal {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let path = self.push_path(subpaths, transform);
|
let path = self.push_path(bezpath, transform);
|
||||||
let brush = peniko::Brush::Image(image);
|
let brush = peniko::Brush::Image(image);
|
||||||
|
|
||||||
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), &brush, None, &path);
|
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), &brush, None, &path);
|
||||||
@@ -1116,17 +1091,17 @@ impl OverlayContextInternal {
|
|||||||
align: TextAlign::AlignLeft,
|
align: TextAlign::AlignLeft,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get text dimensions directly from layout
|
// Lay out the text once, taking its dimensions and vector paths from the same thread-local context pass
|
||||||
let mut text_context = GLOBAL_TEXT_CONTEXT.lock().expect("Failed to lock global text context");
|
let (text_size, text_list) = TextContext::with_thread_local(|text_context| {
|
||||||
let text_size = text_context.bounding_box(text, &FALLBACK_FONT_RESOURCE, typesetting, false);
|
let text_size = text_context.bounding_box(text, &FALLBACK_FONT_RESOURCE, typesetting, false);
|
||||||
|
let text_list = text_context.to_path(text, &FALLBACK_FONT_RESOURCE, typesetting, false);
|
||||||
|
(text_size, text_list)
|
||||||
|
});
|
||||||
let text_width = text_size.x;
|
let text_width = text_size.x;
|
||||||
let text_height = text_size.y;
|
let text_height = text_size.y;
|
||||||
// Create a rect from the size (assuming text starts at origin)
|
// Create a rect from the size (assuming text starts at origin)
|
||||||
let text_bounds = kurbo::Rect::new(0., 0., text_width, text_height);
|
let text_bounds = kurbo::Rect::new(0., 0., text_width, text_height);
|
||||||
|
|
||||||
// Convert text to vector paths for rendering
|
|
||||||
let text_list = text_context.to_path(text, &FALLBACK_FONT_RESOURCE, typesetting, false);
|
|
||||||
|
|
||||||
// Calculate position based on pivot
|
// Calculate position based on pivot
|
||||||
let mut position = DVec2::ZERO;
|
let mut position = DVec2::ZERO;
|
||||||
match pivot[0] {
|
match pivot[0] {
|
||||||
@@ -1189,7 +1164,7 @@ impl OverlayContextInternal {
|
|||||||
|
|
||||||
let width = match typed_string {
|
let width = match typed_string {
|
||||||
Some(ref typed_string) => typed_string,
|
Some(ref typed_string) => typed_string,
|
||||||
None => &format!("{:.2}", translation.x).trim_end_matches('0').trim_end_matches('.').to_string(),
|
None => &format_rounded(translation.x, 2),
|
||||||
};
|
};
|
||||||
let x_transform = DAffine2::from_translation((quad.top_left() + quad.top_right()) / 2.);
|
let x_transform = DAffine2::from_translation((quad.top_left() + quad.top_right()) / 2.);
|
||||||
self.text(width, COLOR_OVERLAY_BLUE, None, x_transform, 4., [Pivot::Middle, Pivot::End]);
|
self.text(width, COLOR_OVERLAY_BLUE, None, x_transform, 4., [Pivot::Middle, Pivot::End]);
|
||||||
@@ -1200,7 +1175,7 @@ impl OverlayContextInternal {
|
|||||||
|
|
||||||
let height = match typed_string {
|
let height = match typed_string {
|
||||||
Some(ref typed_string) => typed_string,
|
Some(ref typed_string) => typed_string,
|
||||||
None => &format!("{:.2}", translation.y).trim_end_matches('0').trim_end_matches('.').to_string(),
|
None => &format_rounded(translation.y, 2),
|
||||||
};
|
};
|
||||||
let y_transform = DAffine2::from_translation((quad.top_left() + quad.bottom_left()) / 2.);
|
let y_transform = DAffine2::from_translation((quad.top_left() + quad.bottom_left()) / 2.);
|
||||||
let height_pivot = if translation.x > -1e-3 { Pivot::Start } else { Pivot::End };
|
let height_pivot = if translation.x > -1e-3 { Pivot::Start } else { Pivot::End };
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ use crate::consts::{
|
|||||||
};
|
};
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use crate::messages::prelude::Message;
|
use crate::messages::prelude::Message;
|
||||||
|
use crate::messages::tool::common_functionality::shapes::shape_utility::format_rounded;
|
||||||
use crate::messages::viewport::ViewportMessageHandler;
|
use crate::messages::viewport::ViewportMessageHandler;
|
||||||
use core::borrow::Borrow;
|
use core::borrow::Borrow;
|
||||||
use core::f64::consts::{FRAC_PI_2, PI, TAU};
|
use core::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graphene_std::math::quad::Quad;
|
use graphene_std::math::quad::Quad;
|
||||||
use graphene_std::subpath::Subpath;
|
|
||||||
use graphene_std::vector::click_target::ClickTargetType;
|
use graphene_std::vector::click_target::ClickTargetType;
|
||||||
use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2};
|
use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2};
|
||||||
use graphene_std::vector::{PointId, SegmentId, Vector};
|
use graphene_std::vector::{PointId, SegmentId, Vector};
|
||||||
use kurbo::{self, Affine, CubicBez, ParamCurve, PathSeg};
|
use kurbo::{self, Affine, BezPath, CubicBez, ParamCurve, PathSeg};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use wasm_bindgen::{JsCast, JsValue};
|
use wasm_bindgen::{JsCast, JsValue};
|
||||||
use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d};
|
use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d};
|
||||||
@@ -930,50 +930,33 @@ impl OverlayContext {
|
|||||||
self.end_dpi_aware_transform();
|
self.end_dpi_aware_transform();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) {
|
fn push_path(&mut self, bezpath: &BezPath, transform: DAffine2) {
|
||||||
self.start_dpi_aware_transform();
|
self.start_dpi_aware_transform();
|
||||||
|
|
||||||
self.render_context.begin_path();
|
self.render_context.begin_path();
|
||||||
for subpath in subpaths {
|
|
||||||
let subpath = subpath.borrow();
|
|
||||||
let mut curves = subpath.iter().peekable();
|
|
||||||
|
|
||||||
let Some(&first) = curves.peek() else {
|
let snap_start = |context: &Self, point: kurbo::Point| context.snap_to_physical_pixel(transform.transform_point2(point_to_dvec2(point)));
|
||||||
continue;
|
let snap_center = |context: &Self, point: kurbo::Point| context.snap_to_physical_pixel_center(transform.transform_point2(point_to_dvec2(point)));
|
||||||
};
|
|
||||||
|
|
||||||
let start_point = transform.transform_point2(point_to_dvec2(first.start()));
|
for element in bezpath.elements() {
|
||||||
let start_point = self.snap_to_physical_pixel(start_point);
|
match *element {
|
||||||
self.render_context.move_to(start_point.x, start_point.y);
|
kurbo::PathEl::MoveTo(point) => {
|
||||||
|
let point = snap_start(self, point);
|
||||||
for curve in curves {
|
self.render_context.move_to(point.x, point.y);
|
||||||
match curve {
|
|
||||||
PathSeg::Line(line) => {
|
|
||||||
let a = transform.transform_point2(point_to_dvec2(line.p1));
|
|
||||||
let a = self.snap_to_physical_pixel_center(a);
|
|
||||||
self.render_context.line_to(a.x, a.y);
|
|
||||||
}
|
|
||||||
PathSeg::Quad(quad_bez) => {
|
|
||||||
let a = transform.transform_point2(point_to_dvec2(quad_bez.p1));
|
|
||||||
let b = transform.transform_point2(point_to_dvec2(quad_bez.p2));
|
|
||||||
let a = self.snap_to_physical_pixel_center(a);
|
|
||||||
let b = self.snap_to_physical_pixel_center(b);
|
|
||||||
self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y);
|
|
||||||
}
|
|
||||||
PathSeg::Cubic(cubic_bez) => {
|
|
||||||
let a = transform.transform_point2(point_to_dvec2(cubic_bez.p1));
|
|
||||||
let b = transform.transform_point2(point_to_dvec2(cubic_bez.p2));
|
|
||||||
let c = transform.transform_point2(point_to_dvec2(cubic_bez.p3));
|
|
||||||
let a = self.snap_to_physical_pixel_center(a);
|
|
||||||
let b = self.snap_to_physical_pixel_center(b);
|
|
||||||
let c = self.snap_to_physical_pixel_center(c);
|
|
||||||
self.render_context.bezier_curve_to(a.x, a.y, b.x, b.y, c.x, c.y);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
kurbo::PathEl::LineTo(point) => {
|
||||||
|
let point = snap_center(self, point);
|
||||||
if subpath.closed() {
|
self.render_context.line_to(point.x, point.y);
|
||||||
self.render_context.close_path();
|
}
|
||||||
|
kurbo::PathEl::QuadTo(a, b) => {
|
||||||
|
let (a, b) = (snap_center(self, a), snap_center(self, b));
|
||||||
|
self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y);
|
||||||
|
}
|
||||||
|
kurbo::PathEl::CurveTo(a, b, c) => {
|
||||||
|
let (a, b, c) = (snap_center(self, a), snap_center(self, b), snap_center(self, c));
|
||||||
|
self.render_context.bezier_curve_to(a.x, a.y, b.x, b.y, c.x, c.y);
|
||||||
|
}
|
||||||
|
kurbo::PathEl::ClosePath => self.render_context.close_path(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -982,18 +965,17 @@ impl OverlayContext {
|
|||||||
|
|
||||||
/// Used by the Select tool to outline a path or a free point when selected or hovered.
|
/// Used by the Select tool to outline a path or a free point when selected or hovered.
|
||||||
pub fn outline(&mut self, target_types: impl Iterator<Item = impl Borrow<ClickTargetType>>, transform: DAffine2, color: Option<&str>) {
|
pub fn outline(&mut self, target_types: impl Iterator<Item = impl Borrow<ClickTargetType>>, transform: DAffine2, color: Option<&str>) {
|
||||||
let mut subpaths: Vec<Subpath<PointId>> = vec![];
|
let mut combined = BezPath::new();
|
||||||
|
|
||||||
target_types.for_each(|target_type| match target_type.borrow() {
|
target_types.for_each(|target_type| match target_type.borrow() {
|
||||||
ClickTargetType::FreePoint(point) => {
|
ClickTargetType::FreePoint(point) => {
|
||||||
self.manipulator_anchor(transform.transform_point2(point.position), false, None);
|
self.manipulator_anchor(transform.transform_point2(point.position), false, None);
|
||||||
}
|
}
|
||||||
ClickTargetType::Subpath(subpath) => subpaths.push(subpath.clone()),
|
ClickTargetType::Path(bezpath) => combined.extend(bezpath.elements().iter().copied()),
|
||||||
ClickTargetType::CompoundPath(compound) => subpaths.extend(compound.iter().cloned()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if !subpaths.is_empty() {
|
if !combined.is_empty() {
|
||||||
self.push_path(subpaths.iter(), transform);
|
self.push_path(&combined, transform);
|
||||||
|
|
||||||
let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
|
let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||||
self.render_context.set_stroke_style_str(color);
|
self.render_context.set_stroke_style_str(color);
|
||||||
@@ -1004,8 +986,8 @@ impl OverlayContext {
|
|||||||
|
|
||||||
/// Fills the area inside the path. Assumes `color` is in gamma space.
|
/// Fills the area inside the path. Assumes `color` is in gamma space.
|
||||||
/// Used by the Pen tool to show the path being closed.
|
/// Used by the Pen tool to show the path being closed.
|
||||||
pub fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
|
pub fn fill_path(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
|
||||||
self.push_path(subpaths, transform);
|
self.push_path(bezpath, transform);
|
||||||
|
|
||||||
self.render_context.set_fill_style_str(color);
|
self.render_context.set_fill_style_str(color);
|
||||||
self.render_context.fill();
|
self.render_context.fill();
|
||||||
@@ -1013,7 +995,7 @@ impl OverlayContext {
|
|||||||
|
|
||||||
/// Fills the area inside the path with a pattern. Assumes `color` is an sRGB hex string.
|
/// Fills the area inside the path with a pattern. Assumes `color` is an sRGB hex string.
|
||||||
/// Used by the fill tool to show the area to be filled.
|
/// Used by the fill tool to show the area to be filled.
|
||||||
pub fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
|
pub fn fill_path_pattern(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
|
||||||
const PATTERN_WIDTH: usize = 4;
|
const PATTERN_WIDTH: usize = 4;
|
||||||
const PATTERN_HEIGHT: usize = 4;
|
const PATTERN_HEIGHT: usize = 4;
|
||||||
|
|
||||||
@@ -1046,7 +1028,7 @@ impl OverlayContext {
|
|||||||
pattern_context.put_image_data(&image_data, 0, 0).unwrap();
|
pattern_context.put_image_data(&image_data, 0, 0).unwrap();
|
||||||
let pattern = self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap();
|
let pattern = self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap();
|
||||||
|
|
||||||
self.push_path(subpaths, transform);
|
self.push_path(bezpath, transform);
|
||||||
|
|
||||||
self.render_context.set_fill_style_canvas_pattern(&pattern);
|
self.render_context.set_fill_style_canvas_pattern(&pattern);
|
||||||
self.render_context.fill();
|
self.render_context.fill();
|
||||||
@@ -1090,7 +1072,7 @@ impl OverlayContext {
|
|||||||
|
|
||||||
let width = match typed_string {
|
let width = match typed_string {
|
||||||
Some(ref typed_string) => typed_string,
|
Some(ref typed_string) => typed_string,
|
||||||
None => &format!("{:.2}", translation.x).trim_end_matches('0').trim_end_matches('.').to_string(),
|
None => &format_rounded(translation.x, 2),
|
||||||
};
|
};
|
||||||
let x_transform = DAffine2::from_translation((quad.top_left() + quad.top_right()) / 2.);
|
let x_transform = DAffine2::from_translation((quad.top_left() + quad.top_right()) / 2.);
|
||||||
self.text(width, COLOR_OVERLAY_BLUE, None, x_transform, 4., [Pivot::Middle, Pivot::End]);
|
self.text(width, COLOR_OVERLAY_BLUE, None, x_transform, 4., [Pivot::Middle, Pivot::End]);
|
||||||
@@ -1101,7 +1083,7 @@ impl OverlayContext {
|
|||||||
|
|
||||||
let height = match typed_string {
|
let height = match typed_string {
|
||||||
Some(ref typed_string) => typed_string,
|
Some(ref typed_string) => typed_string,
|
||||||
None => &format!("{:.2}", translation.y).trim_end_matches('0').trim_end_matches('.').to_string(),
|
None => &format_rounded(translation.y, 2),
|
||||||
};
|
};
|
||||||
let y_transform = DAffine2::from_translation((quad.top_left() + quad.bottom_left()) / 2.);
|
let y_transform = DAffine2::from_translation((quad.top_left() + quad.bottom_left()) / 2.);
|
||||||
let height_pivot = if translation.x > -1e-3 { Pivot::Start } else { Pivot::End };
|
let height_pivot = if translation.x > -1e-3 { Pivot::Start } else { Pivot::End };
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::messages::portfolio::{document::resource::utility_types::EmbeddedReso
|
|||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||||
use graph_craft::application_io::resource::{DataSource, LoadResource, Resource, ResourceHash, ResourceId, ResourceRegistry};
|
use graph_craft::application_io::resource::{DataSource, LoadResource, Resource, ResourceHash, ResourceId, ResourceRegistry, ResourceStorage};
|
||||||
use graphene_std::text::Font;
|
use graphene_std::text::Font;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
@@ -12,6 +12,7 @@ use url::Url;
|
|||||||
pub struct ResourceMessageContext<'a> {
|
pub struct ResourceMessageContext<'a> {
|
||||||
pub document_id: DocumentId,
|
pub document_id: DocumentId,
|
||||||
pub fonts: &'a FontsMessageHandler,
|
pub fonts: &'a FontsMessageHandler,
|
||||||
|
pub resource_storage: &'a ResourceStorageMessageHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, ExtractField)]
|
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, ExtractField)]
|
||||||
@@ -25,7 +26,7 @@ pub struct ResourceMessageHandler {
|
|||||||
#[message_handler_data]
|
#[message_handler_data]
|
||||||
impl MessageHandler<ResourceMessage, ResourceMessageContext<'_>> for ResourceMessageHandler {
|
impl MessageHandler<ResourceMessage, ResourceMessageContext<'_>> for ResourceMessageHandler {
|
||||||
fn process_message(&mut self, message: ResourceMessage, responses: &mut VecDeque<Message>, context: ResourceMessageContext) {
|
fn process_message(&mut self, message: ResourceMessage, responses: &mut VecDeque<Message>, context: ResourceMessageContext) {
|
||||||
let ResourceMessageContext { document_id, fonts } = context;
|
let ResourceMessageContext { document_id, fonts, resource_storage } = context;
|
||||||
|
|
||||||
match message {
|
match message {
|
||||||
ResourceMessage::StoreEmbedded { resource_id, data } => {
|
ResourceMessage::StoreEmbedded { resource_id, data } => {
|
||||||
@@ -48,8 +49,9 @@ impl MessageHandler<ResourceMessage, ResourceMessageContext<'_>> for ResourceMes
|
|||||||
responses.add(ResourceMessage::Resolve { resource_id });
|
responses.add(ResourceMessage::Resolve { resource_id });
|
||||||
}
|
}
|
||||||
ResourceMessage::ResolveAll => {
|
ResourceMessage::ResolveAll => {
|
||||||
let unresolved_ids: Vec<ResourceId> = self.registry.unresolved().map(|info| info.id).collect();
|
let storage = resource_storage.resources_mut();
|
||||||
for id in unresolved_ids {
|
let ids: Vec<ResourceId> = self.registry.ids().filter(|id| !self.registry.hash(id).is_some_and(|hash| storage.contains(&hash))).collect();
|
||||||
|
for id in ids {
|
||||||
if self.pending_resolves.contains(&id) {
|
if self.pending_resolves.contains(&id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -65,7 +67,7 @@ impl MessageHandler<ResourceMessage, ResourceMessageContext<'_>> for ResourceMes
|
|||||||
log::error!("Resolve for {resource_id}: no registry entry");
|
log::error!("Resolve for {resource_id}: no registry entry");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if info.hash.is_some() {
|
if info.hash.is_some_and(|hash| resource_storage.resources_mut().contains(hash)) {
|
||||||
log::warn!("Resource {resource_id} already resolved");
|
log::warn!("Resource {resource_id} already resolved");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -297,3 +299,47 @@ impl<'de> serde::Deserialize<'de> for ResourceMessageHandler {
|
|||||||
deserializer.deserialize_map(EmbeddedResourcesVisitor { human_readable })
|
deserializer.deserialize_map(EmbeddedResourcesVisitor { human_readable })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use graph_craft::application_io::resource::ResourceStorage;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_all_refetches_resources_whose_bytes_are_missing() {
|
||||||
|
let mut handler = ResourceMessageHandler::default();
|
||||||
|
let storage = ResourceStorageMessageHandler::default();
|
||||||
|
let fonts = FontsMessageHandler::default();
|
||||||
|
|
||||||
|
let font = |style: &str| DataSource::Font {
|
||||||
|
family: "Lato".into(),
|
||||||
|
style: Some(style.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let cached = ResourceId::from(1);
|
||||||
|
let cached_hash = storage.resources_mut().store(b"stored font bytes");
|
||||||
|
handler.registry.resolve(&cached, cached_hash);
|
||||||
|
handler.registry.push_source_back(&cached, font("Regular (400)"));
|
||||||
|
|
||||||
|
let evicted = ResourceId::from(2);
|
||||||
|
let evicted_hash = ResourceHash::from(b"evicted font bytes".as_slice());
|
||||||
|
handler.registry.resolve(&evicted, evicted_hash);
|
||||||
|
handler.registry.push_source_back(&evicted, font("Black (900)"));
|
||||||
|
|
||||||
|
let mut responses = VecDeque::new();
|
||||||
|
handler.process_message(
|
||||||
|
ResourceMessage::ResolveAll,
|
||||||
|
&mut responses,
|
||||||
|
ResourceMessageContext {
|
||||||
|
document_id: DocumentId(0),
|
||||||
|
fonts: &fonts,
|
||||||
|
resource_storage: &storage,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let resolve_requested = |id: ResourceId| responses.contains(&Message::from(ResourceMessage::Resolve { resource_id: id }));
|
||||||
|
assert!(resolve_requested(evicted), "a resource whose bytes are gone should be resolved again");
|
||||||
|
assert!(!resolve_requested(cached), "a resource whose bytes are in storage should be left alone");
|
||||||
|
assert_eq!(handler.registry.hash(&evicted), Some(evicted_hash), "the hash keeps naming the content");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
|
|||||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::storage_metadata::{StorageMetadataView, build_interface_from_storage};
|
use crate::messages::portfolio::document::utility_types::network_interface::storage_metadata::{StorageMetadataView, build_interface_from_storage};
|
||||||
use crate::test_utils::test_prelude::*;
|
use crate::test_utils::test_prelude::*;
|
||||||
|
use graphene_std::NodeParameter;
|
||||||
use graphene_std::vector::style::RenderMode;
|
use graphene_std::vector::style::RenderMode;
|
||||||
|
|
||||||
/// Every node addressable in `original` resolves identically through the round-tripped interface:
|
/// Every node addressable in `original` resolves identically through the round-tripped interface:
|
||||||
@@ -708,3 +709,171 @@ async fn demo_artwork_edit_autosaves_and_round_trips() {
|
|||||||
// Autosaving the undone state still round-trips cleanly (no drift panic).
|
// Autosaving the undone state still round-trips cleanly (no drift panic).
|
||||||
editor.active_document_mut().commit_storage_snapshot(&byte_store, true);
|
editor.active_document_mut().commit_storage_snapshot(&byte_store, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The document's single Fill node, as `(network_path, node_id)`.
|
||||||
|
fn find_fill_node(document: &DocumentMessageHandler) -> (Vec<graph_craft::document::NodeId>, graph_craft::document::NodeId) {
|
||||||
|
node_paths(&document.network_interface)
|
||||||
|
.into_iter()
|
||||||
|
.find(|(network_path, node_id)| {
|
||||||
|
let Some(network) = document.network_interface.nested_network(network_path) else { return false };
|
||||||
|
network.nodes[node_id].implementation == graph_craft::document::DocumentNodeImplementation::ProtoNode(graphene_std::vector_nodes::fill::IDENTIFIER)
|
||||||
|
})
|
||||||
|
.expect("the document should contain a Fill node")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The stored paint value of the document's single Fill node.
|
||||||
|
fn fill_paint_value(document: &DocumentMessageHandler) -> graph_craft::document::value::TaggedValue {
|
||||||
|
let (network_path, node_id) = find_fill_node(document);
|
||||||
|
let network = document.network_interface.nested_network(&network_path).expect("the found network path should resolve");
|
||||||
|
let input = network.nodes[&node_id].input(graphene_std::vector::fill::PaintInput).expect("Fill should have a paint input");
|
||||||
|
input.as_value().expect("the paint input should hold a value").clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn none_fill_survives_document_reopen() {
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||||
|
|
||||||
|
// Pick the red-slash "none" paint, stored the same way as the Fill widget's None choice
|
||||||
|
let (_, fill_node_id) = find_fill_node(editor.active_document());
|
||||||
|
editor
|
||||||
|
.handle_message(NodeGraphMessage::SetInputValue {
|
||||||
|
node_id: fill_node_id,
|
||||||
|
input_index: graphene_std::vector::fill::PaintInput::INDEX,
|
||||||
|
value: graph_craft::document::value::TaggedValue::no_paint().into(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(fill_paint_value(editor.active_document()).is_no_paint(), "the None pick should store as no_paint");
|
||||||
|
|
||||||
|
// Reopen through the editor's real open path, which runs the document migrations
|
||||||
|
let serialized = editor.active_document().serialize_document();
|
||||||
|
editor
|
||||||
|
.handle_message(PortfolioMessage::OpenDocumentFile {
|
||||||
|
document_name: None,
|
||||||
|
document_path: None,
|
||||||
|
document_serialized_content: serialized,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let reopened_paint = fill_paint_value(editor.active_document());
|
||||||
|
assert!(reopened_paint.is_no_paint(), "a none fill should survive reopening, but the stored paint became {reopened_paint:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn legacy_four_input_fill_migrates_to_the_split_transform_shape() {
|
||||||
|
use graph_craft::document::value::TaggedValue;
|
||||||
|
|
||||||
|
// A minimal master-era document: a 4-input Fill (content, fill: wired, backup color, backup gradient) fed by another node
|
||||||
|
const LEGACY_DOCUMENT: &str = r#"{"network_interface":{"network":{"exports":[{"Node":{"node_id":1,"output_index":0,"lambda":false}}],"nodes":[[1,{"inputs":[{"Value":{"tagged_value":{"GraphicGroup":{"instance":[],"transform":[],"alpha_blending":[],"source_node_id":[]}},"exposed":true}},{"Node":{"node_id":2,"output_index":0,"lambda":false}},{"Value":{"tagged_value":{"OptionalColor":null},"exposed":false}},{"Value":{"tagged_value":{"Gradient":{"stops":[[0.0,{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}],[1.0,{"red":1.0,"green":1.0,"blue":1.0,"alpha":1.0}]],"gradient_type":"Linear","start":[0.0,0.5],"end":[1.0,0.5],"transform":[1.0,0.0,0.0,1.0,0.0,0.0]}},"exposed":false}}],"manual_composition":{"Concrete":{"name":"core::option::Option<alloc::sync::Arc<graphene_core::context::OwnedContextImpl>>","alias":null}},"implementation":{"ProtoNode":{"name":"graphene_core::vector::FillNode"}},"visible":true,"skip_deduplication":false}],[2,{"inputs":[{"Value":{"tagged_value":"None","exposed":false}},{"Value":{"tagged_value":{"GradientStops":[[0.0,{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}],[1.0,{"red":1.0,"green":1.0,"blue":1.0,"alpha":1.0}]]},"exposed":false}},{"Value":{"tagged_value":{"F64":0.5},"exposed":false}}],"manual_composition":{"Concrete":{"name":"core::option::Option<alloc::sync::Arc<graphene_core::context::OwnedContextImpl>>","alias":null}},"implementation":{"ProtoNode":{"name":"graphene_core::ops::SampleGradientNode"}},"visible":true,"skip_deduplication":false}]],"scope_injections":[]},"network_metadata":{"persistent_metadata":{"node_metadata":[[1,{"persistent_metadata":{"reference":"Fill","display_name":"","input_properties":[{"input_data":{"input_name":"Vector Data"},"widget_override":null},{"input_data":{"input_name":"Fill"},"widget_override":null},{"input_data":{"input_name":"Backup Color"},"widget_override":null},{"input_data":{"input_name":"Backup Gradient"},"widget_override":null}],"output_names":["Future<Instances<VectorData>>"],"has_primary_output":true,"locked":false,"pinned":false,"node_type_metadata":{"Node":{"position":{"Absolute":[0,0]}}},"network_metadata":null}}],[2,{"persistent_metadata":{"reference":"Sample Gradient","display_name":"","input_properties":[{"input_data":{"input_name":"Primary"},"widget_override":null},{"input_data":{"input_name":"Gradient"},"widget_override":null},{"input_data":{"input_name":"Position"},"widget_override":null}],"output_names":["Future<Color>"],"has_primary_output":true,"locked":false,"pinned":false,"node_type_metadata":{"Node":{"position":{"Absolute":[-20,0]}}},"network_metadata":null}}]],"previewing":"No","navigation_metadata":{"node_graph_ptz":{"pan":[0.0,0.0],"tilt":0.0,"zoom":1.0,"flip":false},"node_graph_to_viewport":[1.0,0.0,0.0,1.0,0.0,0.0],"node_graph_top_right":[0.0,0.0]},"selection_undo_history":[],"selection_redo_history":[]}}},"collapsed":[],"name":"legacy_fill.graphite","commit_hash":"0000000000000000000000000000000000000000","document_ptz":{"pan":[0.0,0.0],"tilt":0.0,"zoom":1.0,"flip":false},"document_mode":"DesignMode","view_mode":"Normal","overlays_visibility_settings":{"all":true,"artboard_name":true,"compass_rose":true,"quick_measurement":true,"transform_measurement":true,"transform_cage":true,"hover_outline":true,"selection_outline":true,"pivot":true,"path":true,"anchors":true,"handles":true},"rulers_visible":true,"snapping_state":{"snapping_enabled":true,"grid_snapping":false,"artboards":true,"tolerance":8.0,"bounding_box":{"center_point":true,"corner_point":true,"edge_midpoint":true,"align_with_edges":true,"distribute_evenly":true},"path":{"anchor_point":true,"line_midpoint":true,"along_path":true,"normal_to_path":true,"tangent_to_path":true,"path_intersection_point":true,"align_with_anchor_point":true,"perpendicular_from_endpoint":true},"grid":{"origin":[0.0,0.0],"grid_type":{"Rectangular":{"spacing":[1.0,1.0]}},"grid_color":{"red":0.6,"green":0.6,"blue":0.6,"alpha":1.0},"dot_display":false}},"graph_view_overlay_open":false,"graph_fade_artwork_percentage":80.0}"#;
|
||||||
|
|
||||||
|
// Deserializing alone must succeed, so a failure below is attributable to the migrations
|
||||||
|
DocumentMessageHandler::deserialize_document(LEGACY_DOCUMENT).expect("the legacy document should deserialize");
|
||||||
|
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor
|
||||||
|
.handle_message(PortfolioMessage::OpenDocumentFile {
|
||||||
|
document_name: None,
|
||||||
|
document_path: None,
|
||||||
|
document_serialized_content: LEGACY_DOCUMENT.to_string(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let document = editor.active_document();
|
||||||
|
let (network_path, node_id) = find_fill_node(document);
|
||||||
|
let network = document.network_interface.nested_network(&network_path).expect("the found network path should resolve");
|
||||||
|
let fill_node = &network.nodes[&node_id];
|
||||||
|
|
||||||
|
assert_eq!(fill_node.inputs.len(), 7, "the legacy Fill should upgrade to the 7-input shape");
|
||||||
|
let paint = fill_node.input(graphene_std::vector::fill::PaintInput);
|
||||||
|
assert!(
|
||||||
|
matches!(paint, Some(graph_craft::document::NodeInput::Node { .. })),
|
||||||
|
"the wired legacy fill should keep its connection, but became {paint:?}"
|
||||||
|
);
|
||||||
|
let has_transform = fill_node.input_value(graphene_std::vector::fill::HasTransformInput);
|
||||||
|
assert!(
|
||||||
|
matches!(has_transform, Some(TaggedValue::Bool(_))),
|
||||||
|
"the has-transform input should hold a bool, but became {has_transform:?}"
|
||||||
|
);
|
||||||
|
let transform = fill_node.input_value(graphene_std::vector::fill::TransformInput);
|
||||||
|
assert!(
|
||||||
|
matches!(transform, Some(TaggedValue::DAffine2(_))),
|
||||||
|
"the transform input should hold a matrix, but became {transform:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// The Evaluate Gradient parameter held the tuple-form stops, which parse as the ramp value with even positions elided
|
||||||
|
let evaluate_gradient_node = &network.nodes[&graph_craft::document::NodeId(2)];
|
||||||
|
let stops = evaluate_gradient_node.input_value(graphene_std::math_nodes::evaluate_gradient::GradientInput);
|
||||||
|
let Some(TaggedValue::GradientRamp(ramp)) = stops else {
|
||||||
|
panic!("the legacy stops parameter should become a gradient ramp value, but became {stops:?}");
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
ramp.gradient_space,
|
||||||
|
graphene_std::vector::style::GradientSpace::RgbGamma,
|
||||||
|
"a legacy document's ramps should deserialize with the explicit gamma interpolation"
|
||||||
|
);
|
||||||
|
let stops = graphene_std::vector::Gradient::from(ramp);
|
||||||
|
assert_eq!(stops.len(), 2);
|
||||||
|
assert!(!stops.has_position_attribute(), "even legacy tuple positions should elide rather than materialize");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn eight_input_fill_migrates_the_spread_input_into_the_ramp() {
|
||||||
|
use graph_craft::document::value::TaggedValue;
|
||||||
|
use graphene_std::vector::style::GradientSpread;
|
||||||
|
|
||||||
|
// A minimal document from the era when the gradient spread was the Fill node's own sixth input (tagged `GradientSpreadMethod`), here set to Repeat
|
||||||
|
const EIGHT_INPUT_DOCUMENT: &str = r#"{"network_interface":{"network":{"exports":[{"Node":{"node_id":1,"output_index":0,"lambda":false}}],"nodes":[[1,{"inputs":[{"Value":{"tagged_value":{"GraphicGroup":{"instance":[],"transform":[],"alpha_blending":[],"source_node_id":[]}},"exposed":true}},{"Value":{"tagged_value":{"GradientRamp":{"stops":{"color":[{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0},{"red":1.0,"green":1.0,"blue":1.0,"alpha":1.0}]}}},"exposed":false}},{"Value":{"tagged_value":{"Color":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}},"exposed":false}},{"Value":{"tagged_value":{"GradientRamp":{"stops":{"color":[{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0},{"red":1.0,"green":1.0,"blue":1.0,"alpha":1.0}]}}},"exposed":false}},{"Value":{"tagged_value":{"GradientType":"Linear"},"exposed":false}},{"Value":{"tagged_value":{"GradientSpreadMethod":"Repeat"},"exposed":false}},{"Value":{"tagged_value":{"Bool":false},"exposed":false}},{"Value":{"tagged_value":{"DAffine2":[1.0,0.0,0.0,1.0,0.0,0.0]},"exposed":false}}],"manual_composition":{"Concrete":{"name":"core::option::Option<alloc::sync::Arc<graphene_core::context::OwnedContextImpl>>","alias":null}},"implementation":{"ProtoNode":{"name":"graphene_core::vector::FillNode"}},"visible":true,"skip_deduplication":false}]],"scope_injections":[]},"network_metadata":{"persistent_metadata":{"node_metadata":[[1,{"persistent_metadata":{"reference":"Fill","display_name":"","input_properties":[{"input_data":{"input_name":"Content"},"widget_override":null},{"input_data":{"input_name":"Fill"},"widget_override":null},{"input_data":{"input_name":"Backup Color"},"widget_override":null},{"input_data":{"input_name":"Backup Gradient"},"widget_override":null},{"input_data":{"input_name":"Gradient Type"},"widget_override":null},{"input_data":{"input_name":"Spread Method"},"widget_override":null},{"input_data":{"input_name":"Has Transform"},"widget_override":null},{"input_data":{"input_name":"Transform"},"widget_override":null}],"output_names":[""],"has_primary_output":true,"locked":false,"pinned":false,"node_type_metadata":{"Node":{"position":{"Absolute":[0,0]}}},"network_metadata":null}}]],"previewing":"No","navigation_metadata":{"node_graph_ptz":{"pan":[0.0,0.0],"tilt":0.0,"zoom":1.0,"flip":false},"node_graph_to_viewport":[1.0,0.0,0.0,1.0,0.0,0.0],"node_graph_top_right":[0.0,0.0]},"selection_undo_history":[],"selection_redo_history":[]}}},"collapsed":[],"name":"eight_input_fill.graphite","commit_hash":"0000000000000000000000000000000000000000","document_ptz":{"pan":[0.0,0.0],"tilt":0.0,"zoom":1.0,"flip":false},"document_mode":"DesignMode","view_mode":"Normal","overlays_visibility_settings":{"all":true,"artboard_name":true,"compass_rose":true,"quick_measurement":true,"transform_measurement":true,"transform_cage":true,"hover_outline":true,"selection_outline":true,"pivot":true,"path":true,"anchors":true,"handles":true},"rulers_visible":true,"snapping_state":{"snapping_enabled":true,"grid_snapping":false,"artboards":true,"tolerance":8.0,"bounding_box":{"center_point":true,"corner_point":true,"edge_midpoint":true,"align_with_edges":true,"distribute_evenly":true},"path":{"anchor_point":true,"line_midpoint":true,"along_path":true,"normal_to_path":true,"tangent_to_path":true,"path_intersection_point":true,"align_with_anchor_point":true,"perpendicular_from_endpoint":true},"grid":{"origin":[0.0,0.0],"grid_type":{"Rectangular":{"spacing":[1.0,1.0]}},"grid_color":{"red":0.6,"green":0.6,"blue":0.6,"alpha":1.0},"dot_display":false}},"graph_view_overlay_open":false,"graph_fade_artwork_percentage":80.0}"#;
|
||||||
|
|
||||||
|
// Deserializing alone must succeed, so a failure below is attributable to the migrations
|
||||||
|
DocumentMessageHandler::deserialize_document(EIGHT_INPUT_DOCUMENT).expect("the eight-input document should deserialize");
|
||||||
|
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor
|
||||||
|
.handle_message(PortfolioMessage::OpenDocumentFile {
|
||||||
|
document_name: None,
|
||||||
|
document_path: None,
|
||||||
|
document_serialized_content: EIGHT_INPUT_DOCUMENT.to_string(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let document = editor.active_document();
|
||||||
|
let (network_path, node_id) = find_fill_node(document);
|
||||||
|
let network = document.network_interface.nested_network(&network_path).expect("the found network path should resolve");
|
||||||
|
let fill_node = &network.nodes[&node_id];
|
||||||
|
|
||||||
|
assert_eq!(fill_node.inputs.len(), 7, "the eight-input Fill should fold down to the 7-input shape");
|
||||||
|
|
||||||
|
let paint = fill_node.input_value(graphene_std::vector::fill::PaintInput);
|
||||||
|
let Some(TaggedValue::GradientRamp(ramp)) = paint else {
|
||||||
|
panic!("the fill input should keep its gradient ramp value, but became {paint:?}");
|
||||||
|
};
|
||||||
|
assert_eq!(ramp.gradient_spread, GradientSpread::Repeat, "the spread input's value should fold into the fill ramp");
|
||||||
|
assert_eq!(
|
||||||
|
ramp.gradient_space,
|
||||||
|
graphene_std::vector::style::GradientSpace::RgbGamma,
|
||||||
|
"a legacy document's ramps should deserialize with the explicit gamma interpolation"
|
||||||
|
);
|
||||||
|
|
||||||
|
let backup = fill_node.input_value(graphene_std::vector::fill::BackupGradientInput);
|
||||||
|
let Some(TaggedValue::GradientRamp(backup_ramp)) = backup else {
|
||||||
|
panic!("the backup gradient input should keep its gradient ramp value, but became {backup:?}");
|
||||||
|
};
|
||||||
|
assert_eq!(backup_ramp.gradient_spread, GradientSpread::Repeat, "the spread input's value should fold into the backup ramp");
|
||||||
|
assert_eq!(
|
||||||
|
backup_ramp.gradient_space,
|
||||||
|
graphene_std::vector::style::GradientSpace::RgbGamma,
|
||||||
|
"the backup ramp should carry the explicit gamma interpolation too"
|
||||||
|
);
|
||||||
|
|
||||||
|
let has_transform = fill_node.input_value(graphene_std::vector::fill::HasTransformInput);
|
||||||
|
assert!(
|
||||||
|
matches!(has_transform, Some(TaggedValue::Bool(false))),
|
||||||
|
"the has-transform input should shift down intact, but became {has_transform:?}"
|
||||||
|
);
|
||||||
|
let transform = fill_node.input_value(graphene_std::vector::fill::TransformInput);
|
||||||
|
assert!(
|
||||||
|
matches!(transform, Some(TaggedValue::DAffine2(_))),
|
||||||
|
"the transform input should shift down intact, but became {transform:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ use graph_craft::document::NodeId;
|
|||||||
use graphene_std::Graphic;
|
use graphene_std::Graphic;
|
||||||
use graphene_std::list::List;
|
use graphene_std::list::List;
|
||||||
use graphene_std::math::quad::Quad;
|
use graphene_std::math::quad::Quad;
|
||||||
use graphene_std::subpath;
|
|
||||||
use graphene_std::transform::Footprint;
|
use graphene_std::transform::Footprint;
|
||||||
|
use graphene_std::vector::Vector;
|
||||||
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
|
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
|
||||||
use graphene_std::vector::{PointId, Vector};
|
use kurbo::{Affine, BezPath};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::num::NonZeroU64;
|
use std::num::NonZeroU64;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -193,11 +193,13 @@ impl DocumentMetadata {
|
|||||||
self.visual_targets(layer)?
|
self.visual_targets(layer)?
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|click_target| match click_target.target_type() {
|
.filter_map(|click_target| match click_target.target_type() {
|
||||||
ClickTargetType::Subpath(subpath) => subpath.loose_bounding_box_with_transform(transform),
|
ClickTargetType::Path(path) => {
|
||||||
ClickTargetType::CompoundPath(subpaths) => subpaths
|
let mut transformed = path.clone();
|
||||||
.iter()
|
transformed.apply_affine(Affine::new(transform.to_cols_array()));
|
||||||
.filter_map(|subpath| subpath.loose_bounding_box_with_transform(transform))
|
|
||||||
.reduce(|[a_min, a_max], [b_min, b_max]| [a_min.min(b_min), a_max.max(b_max)]),
|
let control_box = transformed.control_box();
|
||||||
|
(!transformed.is_empty()).then(|| [DVec2::new(control_box.min_x(), control_box.min_y()), DVec2::new(control_box.max_x(), control_box.max_y())])
|
||||||
|
}
|
||||||
ClickTargetType::FreePoint(_) => click_target.bounding_box_with_transform(transform),
|
ClickTargetType::FreePoint(_) => click_target.bounding_box_with_transform(transform),
|
||||||
})
|
})
|
||||||
.reduce(Quad::combine_bounds)
|
.reduce(Quad::combine_bounds)
|
||||||
@@ -233,10 +235,15 @@ impl DocumentMetadata {
|
|||||||
/// stroke geometry when the layer is a vector with a stroke style. Falls back to the click-target-based
|
/// stroke geometry when the layer is a vector with a stroke style. Falls back to the click-target-based
|
||||||
/// bounds for non-vector layers (groups, raster, text, color, gradient).
|
/// bounds for non-vector layers (groups, raster, text, color, gradient).
|
||||||
pub fn bounding_box_document_with_stroke(&self, layer: LayerNodeIdentifier) -> Option<[DVec2; 2]> {
|
pub fn bounding_box_document_with_stroke(&self, layer: LayerNodeIdentifier) -> Option<[DVec2; 2]> {
|
||||||
if let Some(vector) = self.layer_vector_data.get(&layer)
|
if let Some(vector) = self.layer_vector_data.get(&layer) {
|
||||||
&& let Some(bounds) = vector.stroke_inclusive_bounding_box_with_transform(self.transform_to_document(layer))
|
let stroke = self
|
||||||
{
|
.layer_appearance_attributes
|
||||||
return Some(bounds);
|
.get(&layer)
|
||||||
|
.and_then(|appearance| appearance.first_coverage_of(graphene_std::Cover::Stroke))
|
||||||
|
.map(graphene_std::Coverage::stroke_params);
|
||||||
|
if let Some(bounds) = vector.stroke_inclusive_bounding_box_with_transform(self.transform_to_document(layer), stroke.as_ref()) {
|
||||||
|
return Some(bounds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.bounding_box_document(layer)
|
self.bounding_box_document(layer)
|
||||||
}
|
}
|
||||||
@@ -251,11 +258,10 @@ impl DocumentMetadata {
|
|||||||
self.all_layers().filter_map(|layer| self.bounding_box_viewport(layer)).reduce(Quad::combine_bounds)
|
self.all_layers().filter_map(|layer| self.bounding_box_viewport(layer)).reduce(Quad::combine_bounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &subpath::Subpath<PointId>> {
|
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &BezPath> {
|
||||||
self.visual_targets(layer).unwrap_or(&[]).iter().flat_map(|target| match target.target_type() {
|
self.visual_targets(layer).unwrap_or(&[]).iter().filter_map(|target| match target.target_type() {
|
||||||
ClickTargetType::Subpath(subpath) => std::slice::from_ref(subpath),
|
ClickTargetType::Path(path) => Some(path),
|
||||||
ClickTargetType::CompoundPath(subpaths) => subpaths.as_slice(),
|
ClickTargetType::FreePoint(_) => None,
|
||||||
ClickTargetType::FreePoint(_) => &[],
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user