Compare commits

..

9 Commits

Author SHA1 Message Date
Yifeng Wang 9c696d278b Add lint-staged script 2025-05-29 16:23:30 +08:00
Yifeng Wang 372fc126b5 test: add test 2025-05-29 16:13:48 +08:00
Yifeng Wang 6d57c01dd4 feat(editor): support brush dom renderer 2025-05-29 16:13:48 +08:00
Yifeng Wang 3d2d399796 refactor: extract common builder 2025-05-29 16:13:48 +08:00
Yifeng Wang 6483f36723 refactor: use path builder 2025-05-29 16:13:48 +08:00
Yifeng Wang df2ecf2bec feat(editor): support connector dom renderer 2025-05-29 16:13:48 +08:00
Yifeng Wang 148c718a12 fix: review 2025-05-29 16:13:48 +08:00
Yifeng Wang c4af1e77d0 fix: test 2025-05-29 16:13:48 +08:00
Yifeng Wang 6a0eb80903 feat(editor): support triangle and diamond shape in shape dom renderer 2025-05-29 16:13:48 +08:00
180 changed files with 2608 additions and 3327 deletions
+2 -3
View File
@@ -151,8 +151,7 @@ jobs:
- name: Clippy
run: |
rustup component add clippy
cargo clippy --workspace --exclude affine_server_native --all-targets --all-features -- -D warnings
cargo clippy -p affine_server_native --all-targets --all-features -- -D warnings
cargo clippy --all-targets --all-features -- -D warnings
check-git-status:
name: Check Git Status
@@ -924,7 +923,7 @@ jobs:
uses: taiki-e/install-action@nextest
- name: Run tests
run: cargo nextest run --workspace --exclude affine_server_native --features use-as-lib --release --no-fail-fast
run: cargo nextest run --release --no-fail-fast
copilot-api-test:
name: Server Copilot Api Test
+23 -2
View File
@@ -117,10 +117,31 @@ jobs:
name: android
path: packages/frontend/apps/android/dist
ios:
runs-on: ${{ github.ref_name == 'canary' && 'macos-latest' || 'blaze/macos-14' }}
determine-ios-runner:
runs-on: ubuntu-latest
needs:
- build-ios-web
outputs:
RUNNER: ${{ steps.runner.outputs.RUNNER }}
steps:
- name: Determine Runner
id: runner
# Randomly pick runner with 80% chance for blaze/macos-14 and 20% chance for namespace-profile-macos
# blaze/macos-14 is free but has limited concurrency
run: |
RANDOM_NUMBER=$(( $RANDOM % 100 + 1 ))
if [ $RANDOM_NUMBER -le 20 ]; then
echo "Selected namespace-profile-macos (20% probability)"
echo "RUNNER=namespace-profile-macos" >> $GITHUB_OUTPUT
else
echo "Selected blaze/macos-14 (80% probability)"
echo "RUNNER=blaze/macos-14" >> $GITHUB_OUTPUT
fi
ios:
runs-on: ${{ github.ref_name == 'canary' && 'macos-latest' || needs.determine-ios-runner.outputs.RUNNER }}
needs:
- determine-ios-runner
steps:
- uses: actions/checkout@v4
- name: Download mobile artifact
Generated
+226 -158
View File
@@ -281,12 +281,12 @@ dependencies = [
[[package]]
name = "anstyle-wincon"
version = "3.0.8"
version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
dependencies = [
"anstyle",
"once_cell_polyfill",
"once_cell",
"windows-sys 0.59.0",
]
@@ -496,7 +496,7 @@ version = "0.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.0",
"cexpr",
"clang-sys",
"itertools 0.13.0",
@@ -546,9 +546,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.1"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
dependencies = [
"serde",
]
@@ -603,9 +603,9 @@ dependencies = [
[[package]]
name = "bpaf"
version = "0.9.20"
version = "0.9.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "473976d7a8620bb1e06dcdd184407c2363fe4fec8e983ee03ed9197222634a31"
checksum = "4848ed5727d39a7573551c205bcb1ccd88c8cad4ed2c80f62e2316f208196b8d"
[[package]]
name = "bstr"
@@ -697,9 +697,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.24"
version = "1.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7"
checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1"
dependencies = [
"shlex",
]
@@ -815,9 +815,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.39"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f"
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
dependencies = [
"clap_builder",
"clap_derive",
@@ -825,9 +825,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.39"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51"
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
dependencies = [
"anstream",
"anstyle",
@@ -901,9 +901,9 @@ dependencies = [
[[package]]
name = "core-foundation"
version = "0.10.1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
dependencies = [
"core-foundation-sys",
"libc",
@@ -922,7 +922,7 @@ version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.0",
"core-foundation",
"core-graphics-types",
"foreign-types",
@@ -935,7 +935,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.0",
"core-foundation",
"libc",
]
@@ -1241,7 +1241,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.0",
"block2",
"libc",
"objc2",
@@ -1272,9 +1272,9 @@ dependencies = [
[[package]]
name = "docx-rust"
version = "0.1.10"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75463cd42bd579fdf76cf10ff1c4fb5988568c726f8ed4c2ed3921ddb6ef5c89"
checksum = "e6a8e9647d314f66d467a705715111d045955949b5dbcf2dc7aee89e078af83d"
dependencies = [
"derive_more",
"hard-xml",
@@ -1339,9 +1339,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.12"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
dependencies = [
"libc",
"windows-sys 0.59.0",
@@ -1595,16 +1595,15 @@ dependencies = [
[[package]]
name = "generator"
version = "0.8.5"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827"
checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd"
dependencies = [
"cc",
"cfg-if",
"libc",
"log",
"rustversion",
"windows 0.61.1",
"windows 0.58.0",
]
[[package]]
@@ -1690,9 +1689,9 @@ dependencies = [
[[package]]
name = "hard-xml"
version = "1.41.0"
version = "1.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b07b8ba970e18a03dbb79f6786b6e4d6f198a0ac839aa5182017001bb8dee17"
checksum = "18b4f798dc293307dc5445b47403a317adc8131782963f3ad4700a836bd26c93"
dependencies = [
"hard-xml-derive",
"jetscii",
@@ -1703,11 +1702,11 @@ dependencies = [
[[package]]
name = "hard-xml-derive"
version = "1.41.0"
version = "1.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0c43e7c3212bd992c11b6b9796563388170950521ae8487f5cdf6f6e792f1c8"
checksum = "5ef0ad18601a1563729b311c9d49e5d46b1c987dc0353742cb8219f61f4339a6"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.0",
"proc-macro2",
"quote",
"syn 1.0.109",
@@ -1826,7 +1825,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.57.0",
"windows-core 0.61.0",
]
[[package]]
@@ -1911,9 +1910,9 @@ checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
[[package]]
name = "icu_properties"
version = "2.0.1"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a"
dependencies = [
"displaydoc",
"icu_collections 2.0.0",
@@ -1927,9 +1926,9 @@ dependencies = [
[[package]]
name = "icu_properties_data"
version = "2.0.1"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04"
[[package]]
name = "icu_provider"
@@ -2050,14 +2049,15 @@ dependencies = [
[[package]]
name = "io-surface"
version = "0.16.1"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "554b8c5d64ec09a3a520fe58e4d48a73e00ff32899cdcbe32a4877afd4968b8e"
checksum = "8283575d5f0b2e7447ec0840363879d71c0fa325d4c699d5b45208ea4a51f45e"
dependencies = [
"cgl",
"core-foundation",
"core-foundation-sys",
"leaky-cow",
"libc",
]
[[package]]
@@ -2188,12 +2188,12 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libloading"
version = "0.8.8"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.53.0",
]
[[package]]
@@ -2279,7 +2279,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59fa2559e99ba0f26a12458aabc754432c805bbb8cba516c427825a997af1fb7"
dependencies = [
"aes",
"bitflags 2.9.1",
"bitflags 2.9.0",
"cbc",
"ecb",
"encoding_rs",
@@ -2390,13 +2390,13 @@ dependencies = [
[[package]]
name = "mio"
version = "1.0.4"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -2410,12 +2410,12 @@ dependencies = [
[[package]]
name = "napi"
version = "3.0.0-beta.3"
version = "3.0.0-alpha.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a5c343e6e1fb57bf3ea3386638c4affb394ee932708128840a56aaac3d6a8ab"
checksum = "c857a2b38c994db8bec785554ab4216d45ad63469832070c86a992be0b5491ad"
dependencies = [
"anyhow",
"bitflags 2.9.1",
"bitflags 2.9.0",
"chrono",
"ctor",
"napi-build",
@@ -2426,18 +2426,17 @@ dependencies = [
[[package]]
name = "napi-build"
version = "2.2.0"
version = "2.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03acbfa4f156a32188bfa09b86dc11a431b5725253fc1fc6f6df5bed273382c4"
checksum = "e28acfa557c083f6e254a786e01ba253fc56f18ee000afcd4f79af735f73a6da"
[[package]]
name = "napi-derive"
version = "3.0.0-beta.3"
version = "3.0.0-alpha.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d23065ee795a4b1a8755fdf4a39c2a229679f01f923a8feea33f045d6d96cb"
checksum = "c7165d931d54f68115e651330d5fe0ae0081133d3f4ee3ab55b0b808f0c23f71"
dependencies = [
"convert_case 0.8.0",
"ctor",
"napi-derive-backend",
"proc-macro2",
"quote",
@@ -2446,9 +2445,9 @@ dependencies = [
[[package]]
name = "napi-derive-backend"
version = "2.0.0-beta.3"
version = "2.0.0-alpha.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "348aaac2c51b5d11cf90cf7670b470c7f4d1607d15c338efd4d3db361003e4f5"
checksum = "ce3f36354262054df8e1c3a73bdcd36ea13f130feb1e4d86b67cab9e10d6ef6d"
dependencies = [
"convert_case 0.8.0",
"proc-macro2",
@@ -2459,9 +2458,9 @@ dependencies = [
[[package]]
name = "napi-sys"
version = "3.0.0-alpha.2"
version = "3.0.0-alpha.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b443b980b2258dbaa31b99115e74da6c0866e537278309d566b4672a2f8df516"
checksum = "4cc061b99c514ad4b7abc99d4db1ca24b9542b7ff48b4760bd9f82b24611534d"
dependencies = [
"libloading",
]
@@ -2478,7 +2477,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.0",
"cfg-if",
"cfg_aliases",
"libc",
@@ -2626,7 +2625,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.0",
"dispatch2",
"objc2",
]
@@ -2643,7 +2642,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.0",
"block2",
"libc",
"objc2",
@@ -2674,12 +2673,6 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "oorandom"
version = "11.1.5"
@@ -3016,7 +3009,7 @@ checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50"
dependencies = [
"bit-set 0.8.0",
"bit-vec 0.8.0",
"bitflags 2.9.1",
"bitflags 2.9.0",
"lazy_static",
"num-traits",
"rand 0.8.5",
@@ -3045,7 +3038,7 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.0",
"memchr",
"unicase",
]
@@ -3250,7 +3243,7 @@ version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.0",
]
[[package]]
@@ -3425,7 +3418,7 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.0",
"errno",
"libc",
"linux-raw-sys",
@@ -3468,9 +3461,9 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.21"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "rusty-fork"
@@ -3724,9 +3717,9 @@ dependencies = [
[[package]]
name = "socket2"
version = "0.5.10"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
dependencies = [
"libc",
"windows-sys 0.52.0",
@@ -3753,9 +3746,9 @@ dependencies = [
[[package]]
name = "sqlx"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
checksum = "f3c3a85280daca669cfd3bcb68a337882a8bc57ec882f72c5d13a430613a738e"
dependencies = [
"sqlx-core",
"sqlx-macros",
@@ -3766,9 +3759,9 @@ dependencies = [
[[package]]
name = "sqlx-core"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
checksum = "f743f2a3cea30a58cd479013f75550e879009e3a02f616f18ca699335aa248c3"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -3803,9 +3796,9 @@ dependencies = [
[[package]]
name = "sqlx-macros"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
checksum = "7f4200e0fde19834956d4252347c12a083bdcb237d7a1a1446bffd8768417dce"
dependencies = [
"proc-macro2",
"quote",
@@ -3816,9 +3809,9 @@ dependencies = [
[[package]]
name = "sqlx-macros-core"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
checksum = "882ceaa29cade31beca7129b6beeb05737f44f82dbe2a9806ecea5a7093d00b7"
dependencies = [
"dotenvy",
"either",
@@ -3835,19 +3828,20 @@ dependencies = [
"sqlx-postgres",
"sqlx-sqlite",
"syn 2.0.101",
"tempfile",
"tokio",
"url",
]
[[package]]
name = "sqlx-mysql"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
checksum = "0afdd3aa7a629683c2d750c2df343025545087081ab5942593a5288855b1b7a7"
dependencies = [
"atoi",
"base64 0.22.1",
"bitflags 2.9.1",
"bitflags 2.9.0",
"byteorder",
"bytes",
"chrono",
@@ -3884,13 +3878,13 @@ dependencies = [
[[package]]
name = "sqlx-postgres"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
checksum = "a0bedbe1bbb5e2615ef347a5e9d8cd7680fb63e77d9dafc0f29be15e53f1ebe6"
dependencies = [
"atoi",
"base64 0.22.1",
"bitflags 2.9.1",
"bitflags 2.9.0",
"byteorder",
"chrono",
"crc",
@@ -3922,9 +3916,9 @@ dependencies = [
[[package]]
name = "sqlx-sqlite"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
checksum = "c26083e9a520e8eb87a06b12347679b142dc2ea29e6e409f805644a7a979a5bc"
dependencies = [
"atoi",
"chrono",
@@ -4438,9 +4432,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.45.1"
version = "1.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
dependencies = [
"backtrace",
"bytes",
@@ -4576,9 +4570,9 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.25.5"
version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac5fff5c47490dfdf473b5228039bfacad9d765d9b6939d26bf7cc064c1c7822"
checksum = "69aff09fea9a41fb061ae6b206cb87cac1b8db07df31be3ba271fbc26760f213"
dependencies = [
"cc",
"regex",
@@ -4941,13 +4935,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.17.0"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [
"getrandom 0.3.3",
"js-sys",
"wasm-bindgen",
]
[[package]]
@@ -5123,9 +5115,9 @@ dependencies = [
[[package]]
name = "weezl"
version = "0.1.10"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3"
checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
[[package]]
name = "whoami"
@@ -5165,7 +5157,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -5186,24 +5178,12 @@ dependencies = [
[[package]]
name = "windows"
version = "0.61.1"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
dependencies = [
"windows-collections",
"windows-core 0.61.2",
"windows-future",
"windows-link",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [
"windows-core 0.61.2",
"windows-core 0.58.0",
"windows-targets 0.52.6",
]
[[package]]
@@ -5220,26 +5200,28 @@ dependencies = [
[[package]]
name = "windows-core"
version = "0.61.2"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
"windows-implement 0.58.0",
"windows-interface 0.58.0",
"windows-result 0.2.0",
"windows-strings 0.1.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
"windows-implement 0.60.0",
"windows-interface 0.59.1",
"windows-link",
"windows-result 0.3.4",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core 0.61.2",
"windows-link",
"windows-threading",
"windows-result 0.3.2",
"windows-strings 0.4.0",
]
[[package]]
@@ -5253,6 +5235,17 @@ dependencies = [
"syn 2.0.101",
]
[[package]]
name = "windows-implement"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
@@ -5275,6 +5268,17 @@ dependencies = [
"syn 2.0.101",
]
[[package]]
name = "windows-interface"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
@@ -5292,16 +5296,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-numerics"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core 0.61.2",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.1.2"
@@ -5313,18 +5307,37 @@ dependencies = [
[[package]]
name = "windows-result"
version = "0.3.4"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result 0.2.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
dependencies = [
"windows-link",
]
@@ -5380,7 +5393,7 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
@@ -5388,12 +5401,19 @@ dependencies = [
]
[[package]]
name = "windows-threading"
version = "0.1.0"
name = "windows-targets"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
"windows_i686_gnullvm 0.53.0",
"windows_i686_msvc 0.53.0",
"windows_x86_64_gnu 0.53.0",
"windows_x86_64_gnullvm 0.53.0",
"windows_x86_64_msvc 0.53.0",
]
[[package]]
@@ -5408,6 +5428,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
@@ -5420,6 +5446,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
@@ -5432,12 +5464,24 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
@@ -5450,6 +5494,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
@@ -5462,6 +5512,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
@@ -5474,6 +5530,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
@@ -5486,6 +5548,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.7.10"
@@ -5501,7 +5569,7 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.0",
]
[[package]]
@@ -5570,7 +5638,7 @@ dependencies = [
"serde_json",
"smol_str",
"thiserror 2.0.12",
"yrs 0.23.4",
"yrs 0.23.1",
]
[[package]]
@@ -5601,7 +5669,7 @@ dependencies = [
"regex",
"y-octo",
"y-sync",
"yrs 0.23.4",
"yrs 0.23.1",
]
[[package]]
@@ -5679,9 +5747,9 @@ dependencies = [
[[package]]
name = "yrs"
version = "0.23.4"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb1d740a98b12112352f05ddfc06c1505b66fca116601f9388b84be45d21f84d"
checksum = "4a7cab84724ae7f361a8c92465f5160922cbb941a499e1a8cacd103351ab9c78"
dependencies = [
"arc-swap",
"async-lock",
@@ -5692,7 +5760,7 @@ dependencies = [
"serde_json",
"smallstr",
"smallvec",
"thiserror 2.0.12",
"thiserror 1.0.69",
]
[[package]]
+3 -3
View File
@@ -47,9 +47,9 @@ log = "0.4"
loom = { version = "0.7", features = ["checkpoint"] }
mimalloc = "0.1"
nanoid = "0.4"
napi = { version = "3.0.0-beta.3", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
napi = { version = "3.0.0-alpha.31", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
napi-build = { version = "2" }
napi-derive = { version = "3.0.0-beta.3" }
napi-derive = { version = "3.0.0-alpha.28" }
nom = "8"
notify = { version = "8", features = ["serde"] }
objc2 = "0.6"
@@ -80,7 +80,7 @@ symphonia = { version = "0.5", features = ["all", "opt-simd"] }
text-splitter = "0.25"
thiserror = "2"
tiktoken-rs = "0.6"
tokio = "1.45"
tokio = "1.37"
tree-sitter = { version = "0.25" }
tree-sitter-c = { version = "0.23" }
tree-sitter-c-sharp = { version = "0.23" }
@@ -407,7 +407,7 @@ const builtinSurfaceToolbarConfig = {
if (options?.viewType !== 'embed') return;
const { flavour, styles } = options;
let style: EmbedCardStyle = model.props.style;
let { style } = model.props;
if (!styles.includes(style)) {
style = styles[0];
@@ -482,26 +482,24 @@ const builtinSurfaceToolbarConfig = {
} satisfies ToolbarActionGroup<ToolbarAction>,
{
id: 'b.style',
actions: (
[
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
{
id: 'vertical',
label: 'Large vertical style',
},
{
id: 'cube',
label: 'Small vertical style',
},
] as const
).filter(action => BookmarkStyles.includes(action.id)),
actions: [
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
{
id: 'vertical',
label: 'Large vertical style',
},
{
id: 'cube',
label: 'Small vertical style',
},
].filter(action => BookmarkStyles.includes(action.id as EmbedCardStyle)),
content(ctx) {
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
if (!model) return null;
@@ -259,18 +259,18 @@ const builtinToolbarConfig = {
conversionsActionGroup,
{
id: 'c.style',
actions: (
[
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
] as const
).filter(action => EmbedLinkedDocStyles.includes(action.id)),
actions: [
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
].filter(action =>
EmbedLinkedDocStyles.includes(action.id as EmbedCardStyle)
),
content(ctx) {
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
if (!model) return null;
@@ -368,26 +368,26 @@ const builtinSurfaceToolbarConfig = {
conversionsActionGroup,
{
id: 'c.style',
actions: (
[
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
{
id: 'vertical',
label: 'Large vertical style',
},
{
id: 'cube',
label: 'Small vertical style',
},
] as const
).filter(action => EmbedLinkedDocStyles.includes(action.id)),
actions: [
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
{
id: 'vertical',
label: 'Large vertical style',
},
{
id: 'cube',
label: 'Small vertical style',
},
].filter(action =>
EmbedLinkedDocStyles.includes(action.id as EmbedCardStyle)
),
content(ctx) {
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
if (!model) return null;
@@ -153,7 +153,7 @@ function createBuiltinToolbarConfigForExternal(
.get(EmbedOptionProvider)
.getEmbedBlockOptions(url);
let style: EmbedCardStyle = model.props.style;
let { style } = model.props;
let flavour = 'affine:bookmark';
if (options?.viewType === 'card') {
@@ -227,7 +227,7 @@ function createBuiltinToolbarConfigForExternal(
if (options?.viewType !== 'embed') return;
const { flavour, styles } = options;
let style: EmbedCardStyle = model.props.style;
let { style } = model.props;
if (!styles.includes(style)) {
style =
@@ -441,11 +441,7 @@ const createBuiltinSurfaceToolbarConfigForExternal = (
let { style } = model.props;
let flavour = 'affine:bookmark';
if (
!BookmarkStyles.includes(
style as (typeof BookmarkStyles)[number]
)
) {
if (!BookmarkStyles.includes(style)) {
style = BookmarkStyles[0];
}
@@ -521,26 +517,26 @@ const createBuiltinSurfaceToolbarConfigForExternal = (
} satisfies ToolbarActionGroup<ToolbarAction>,
{
id: 'c.style',
actions: (
[
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
{
id: 'vertical',
label: 'Large vertical style',
},
{
id: 'cube',
label: 'Small vertical style',
},
] as const
).filter(action => EmbedGithubStyles.includes(action.id)),
actions: [
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
{
id: 'vertical',
label: 'Large vertical style',
},
{
id: 'cube',
label: 'Small vertical style',
},
].filter(action =>
EmbedGithubStyles.includes(action.id as EmbedCardStyle)
),
when(ctx) {
return Boolean(ctx.getCurrentModelByType(EmbedGithubModel));
},
@@ -16,7 +16,6 @@ import {
import { cssVarV2 } from '@toeverything/theme/v2';
import { html } from 'lit';
import { state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import {
@@ -88,12 +87,6 @@ export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
this.gfx.tool.currentToolName$.value === 'frameNavigator';
const frameIndex = this.gfx.layer.getZIndex(model);
const widgets = html`${repeat(
Object.entries(this.widgets),
([id]) => id,
([_, widget]) => widget
)}`;
return html`
<div
class="affine-frame-container"
@@ -109,7 +102,6 @@ export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
: `1px solid ${cssVarV2('edgeless/frame/border/default')}`,
})}
></div>
${widgets}
`;
}
@@ -186,22 +178,11 @@ export const FrameBlockInteraction =
selectable(context) {
const { model } = context;
const onTitle =
model.externalBound?.containsPoint([
context.position.x,
context.position.y,
]) ?? false;
return (
context.default(context) &&
(model.isLocked() ||
!isTransparent(model.props.background) ||
onTitle)
(model.isLocked() || !isTransparent(model.props.background))
);
},
onSelect(context) {
return context.default(context);
},
};
},
}
@@ -241,35 +241,20 @@ export class EdgelessFrameManager extends GfxExtension {
surfaceModel.elementAdded.subscribe(({ id, local }) => {
const element = surfaceModel.getElementById(id);
if (element && local) {
// The entire frame detection logic must be in microtask for timing reasons:
//
// 1. For connectors: When elementAdded fires, connectors have invalid bounds [0,0,0,0]
// because their path/bounds are calculated in a separate microtask of updateConnectorPath by connector-watcher.
// We need to wait for that calculation to complete before frame detection.
//
// 2. For shapes: Although they have valid bounds immediately, processing them in microtask
// ensures consistent timing and allows other initialization to complete first.
//
// 3. Group compatibility: Some elements may need to establish their group relationships
// before being considered for frame membership.
//
// By embedding the entire logic in microtask, we ensure:
// - Connectors have proper bounds calculated (not [0,0,0,0])
// - getFrameFromPoint() works correctly with valid element centers
// - All element initialization is complete before frame detection
const frame = this.getFrameFromPoint(element.elementBound.center);
// if the container created with a frame, skip it.
if (
isGfxGroupCompatibleModel(element) &&
frame &&
element.hasChild(frame)
) {
return;
}
// new element may intended to be added to other group
// so we need to wait for the next microtask to check if the element can be added to the frame
queueMicrotask(() => {
const frame = this.getFrameFromPoint(element.elementBound.center);
// if the container created with a frame, skip it.
if (
isGfxGroupCompatibleModel(element) &&
frame &&
element.hasChild(frame)
) {
return;
}
// Only add elements that aren't already grouped and have a valid frame
if (!element.group && frame) {
this.addElementsToFrame(frame, [element]);
}
@@ -9,10 +9,7 @@ import {
getSurfaceComponent,
} from '@blocksuite/affine-block-surface';
import { splitIntoLines } from '@blocksuite/affine-gfx-text';
import type {
EmbedCardStyle,
ShapeElementModel,
} from '@blocksuite/affine-model';
import type { ShapeElementModel } from '@blocksuite/affine-model';
import {
BookmarkStyles,
DEFAULT_NOTE_HEIGHT,
@@ -239,7 +236,7 @@ export class EdgelessClipboardController extends PageClipboard {
const options: Record<string, unknown> = {};
let flavour = 'affine:bookmark';
let style: EmbedCardStyle = BookmarkStyles[0];
let style = BookmarkStyles[0];
let isInternalLink = false;
let isLinkedBlock = false;
@@ -129,7 +129,7 @@ export class EdgelessRootBlockComponent extends BlockComponent<
) as SurfaceBlockModel;
}
get viewportElement(): HTMLElement {
private get _viewportElement(): HTMLElement {
return this.std.get(ViewportElementProvider).viewportElement;
}
@@ -267,7 +267,7 @@ export class EdgelessRootBlockComponent extends BlockComponent<
this.gfx.viewport.onResize();
});
resizeObserver.observe(this.viewportElement);
resizeObserver.observe(this._viewportElement);
this._resizeObserver = resizeObserver;
}
@@ -43,16 +43,14 @@ type RendererOptions = {
surfaceModel: SurfaceBlockModel;
};
const UpdateType = {
ELEMENT_ADDED: 'element-added',
ELEMENT_REMOVED: 'element-removed',
ELEMENT_UPDATED: 'element-updated',
VIEWPORT_CHANGED: 'viewport-changed',
SIZE_CHANGED: 'size-changed',
ZOOM_STATE_CHANGED: 'zoom-state-changed',
} as const;
type UpdateType = (typeof UpdateType)[keyof typeof UpdateType];
enum UpdateType {
ELEMENT_ADDED = 'element-added',
ELEMENT_REMOVED = 'element-removed',
ELEMENT_UPDATED = 'element-updated',
VIEWPORT_CHANGED = 'viewport-changed',
SIZE_CHANGED = 'size-changed',
ZOOM_STATE_CHANGED = 'zoom-state-changed',
}
interface IncrementalUpdateState {
dirtyElementIds: Set<string>;
@@ -386,21 +384,21 @@ export class DomRenderer {
);
}
addOverlay = (overlay: Overlay) => {
addOverlay(overlay: Overlay) {
overlay.setRenderer(null);
this._overlays.add(overlay);
this.refresh();
};
}
attach = (container: HTMLElement) => {
attach(container: HTMLElement) {
this._container = container;
container.append(this.rootElement);
this._resetSize();
this.refresh();
};
}
dispose = () => {
dispose(): void {
this._overlays.forEach(overlay => overlay.dispose());
this._overlays.clear();
this._disposables.dispose();
@@ -416,65 +414,65 @@ export class DomRenderer {
this.rootElement.remove();
this._elementsMap.clear();
};
}
generateColorProperty = (color: Color, fallback?: Color) => {
generateColorProperty(color: Color, fallback?: Color) {
return (
this.provider.generateColorProperty?.(color, fallback) ?? 'transparent'
);
};
}
getColorScheme = () => {
getColorScheme() {
return this.provider.getColorScheme?.() ?? ColorScheme.Light;
};
}
getColorValue = (color: Color, fallback?: Color, real?: boolean) => {
getColorValue(color: Color, fallback?: Color, real?: boolean) {
return (
this.provider.getColorValue?.(color, fallback, real) ?? 'transparent'
);
};
}
getPropertyValue = (property: string) => {
getPropertyValue(property: string) {
return this.provider.getPropertyValue?.(property) ?? '';
};
}
refresh = () => {
refresh() {
if (this._refreshRafId !== null) return;
this._refreshRafId = requestConnectedFrame(() => {
this._refreshRafId = null;
this._render();
}, this._container);
};
}
removeOverlay = (overlay: Overlay) => {
removeOverlay(overlay: Overlay) {
if (!this._overlays.has(overlay)) {
return;
}
this._overlays.delete(overlay);
this.refresh();
};
}
/**
* Mark a specific element as dirty for incremental updates
* @param elementId - The ID of the element to mark as dirty
* @param updateType - The type of update (optional, defaults to ELEMENT_UPDATED)
*/
markElementDirty = (
markElementDirty(
elementId: string,
updateType: UpdateType = UpdateType.ELEMENT_UPDATED
) => {
) {
this._markElementDirty(elementId, updateType);
};
}
/**
* Force a full re-render of all elements
*/
forceFullRender = () => {
forceFullRender() {
this._updateState.viewportDirty = true;
this.refresh();
};
}
private _markElementDirty(elementId: string, updateType: UpdateType) {
this._updateState.dirtyElementIds.add(elementId);
@@ -29,6 +29,12 @@ export class OpenDocDropdownMenu extends SignalWatcher(
gap: unset !important;
}
editor-icon-button {
.label {
font-weight: 400;
}
}
div[data-orientation] {
width: 264px;
gap: 4px;
@@ -9,7 +9,6 @@ const toolbarColorKeys: Array<keyof AffineCssVariables> = [
'--affine-background-overlay-panel-color',
'--affine-v2-layer-background-overlayPanel' as never,
'--affine-v2-layer-insideBorder-blackBorder' as never,
'--affine-v2-icon-primary' as never,
'--affine-background-error-color',
'--affine-background-primary-color',
'--affine-background-tertiary-color',
@@ -16,6 +16,5 @@ export const renderFilterBar = (props: DataViewWidgetProps) => {
.vars="${filterTrait.view.vars$}"
.filterGroup="${filterTrait.filter$}"
.onChange="${filterTrait.filterSet}"
.dataViewLogic="${props.dataViewLogic}"
></filter-bar>`;
};
@@ -16,7 +16,6 @@ import { property } from 'lit/decorators.js';
import type { Variable } from '../../../core/expression/types.js';
import type { Filter, FilterGroup } from '../../../core/filter/types.js';
import { popCreateFilter } from '../../../core/index.js';
import type { DataViewUILogicBase } from '../../../core/view/data-view-base.js';
import { popFilterGroup } from './group-panel-view.js';
export class FilterBar extends SignalWatcher(ShadowlessElement) {
@@ -100,7 +99,6 @@ export class FilterBar extends SignalWatcher(ShadowlessElement) {
requestAnimationFrame(() => {
this.expandGroup(element, index);
});
this.dataViewLogic.eventTrace('CreateDatabaseFilter', {});
},
});
};
@@ -208,9 +206,6 @@ export class FilterBar extends SignalWatcher(ShadowlessElement) {
@property({ attribute: false })
accessor vars!: ReadonlySignal<Variable[]>;
@property({ attribute: false })
accessor dataViewLogic!: DataViewUILogicBase;
}
declare global {
@@ -26,10 +26,7 @@ import { repeat } from 'lit/directives/repeat.js';
import type { Variable } from '../../../core/expression/types.js';
import type { FilterTrait } from '../../../core/filter/trait.js';
import type { Filter, FilterGroup } from '../../../core/filter/types.js';
import {
type DataViewUILogicBase,
popCreateFilter,
} from '../../../core/index.js';
import { popCreateFilter } from '../../../core/index.js';
import {
type FilterGroupView,
getDepth,
@@ -378,7 +375,6 @@ export const popFilterRoot = (
props: {
filterTrait: FilterTrait;
onBack: () => void;
dataViewLogic: DataViewUILogicBase;
}
) => {
const filterTrait = props.filterTrait;
@@ -418,10 +414,6 @@ export const popFilterRoot = (
...value,
conditions: [...value.conditions, filter],
});
props.dataViewLogic.eventTrace(
'CreateDatabaseFilter',
{}
);
},
},
{ middleware: subMenuMiddleware }
@@ -75,7 +75,6 @@ export class DataViewHeaderToolsFilter extends WidgetBase {
conditions: [filter],
};
this.toggleShowFilter(true);
this.dataViewLogic.eventTrace('CreateDatabaseFilter', {});
},
}
);
@@ -145,16 +145,13 @@ const createSettingMenus = (
popFilterRoot(target, {
filterTrait: filterTrait,
onBack: reopen,
dataViewLogic: dataViewLogic,
});
dataViewLogic.eventTrace('CreateDatabaseFilter', {});
},
});
} else {
popFilterRoot(target, {
filterTrait: filterTrait,
onBack: reopen,
dataViewLogic: dataViewLogic,
});
}
},
@@ -192,14 +192,10 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
this._updateTitleInMeta();
this.requestUpdate();
};
if (this._rootModel) {
const rootModel = this._rootModel;
rootModel.props.title.yText.observe(updateMetaTitle);
this._disposables.add(() => {
rootModel.props.title.yText.unobserve(updateMetaTitle);
});
}
this._rootModel?.props.title.yText.observe(updateMetaTitle);
this._disposables.add(() => {
this._rootModel?.props.title.yText.unobserve(updateMetaTitle);
});
}
override render() {
@@ -0,0 +1,11 @@
import { DomElementRendererExtension } from '@blocksuite/affine-block-surface';
import { brushDomRenderer } from './brush-dom/index.js';
/**
* Extension to register the DOM-based renderer for 'brush' elements.
*/
export const BrushDomRendererExtension = DomElementRendererExtension(
'brush',
brushDomRenderer
);
@@ -0,0 +1,63 @@
import type { DomRenderer } from '@blocksuite/affine-block-surface';
import type { BrushElementModel } from '@blocksuite/affine-model';
import { DefaultTheme } from '@blocksuite/affine-model';
/**
* Renders a BrushElementModel to a given HTMLElement using DOM properties.
* This function is intended to be registered via the DomElementRendererExtension.
*
* @param model - The brush element model containing rendering properties.
* @param element - The HTMLElement to apply the brush's styles to.
* @param renderer - The main DOMRenderer instance, providing access to viewport and color utilities.
*/
export const brushDomRenderer = (
model: BrushElementModel,
element: HTMLElement,
renderer: DomRenderer
): void => {
const { zoom } = renderer.viewport;
const unscaledWidth = model.w;
const unscaledHeight = model.h;
const color = renderer.getColorValue(model.color, DefaultTheme.black, true);
element.style.width = `${unscaledWidth * zoom}px`;
element.style.height = `${unscaledHeight * zoom}px`;
element.style.boxSizing = 'border-box';
element.style.overflow = 'hidden';
// Clear any existing content
element.replaceChildren();
// Create SVG element to render the brush stroke
const SVG_NS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(SVG_NS, 'svg');
svg.setAttribute('width', '100%');
svg.setAttribute('height', '100%');
svg.setAttribute('viewBox', `0 0 ${unscaledWidth} ${unscaledHeight}`);
svg.setAttribute('preserveAspectRatio', 'none');
// Create path element for the brush stroke
const path = document.createElementNS(SVG_NS, 'path');
path.setAttribute('d', model.commands);
path.setAttribute('fill', color);
path.setAttribute('stroke', 'none');
svg.append(path);
element.append(svg);
// Apply rotation if needed
if (model.rotate) {
element.style.transform = `rotate(${model.rotate}deg)`;
element.style.transformOrigin = 'center';
}
// Apply opacity
element.style.opacity = `${model.opacity ?? 1}`;
// Set z-index
element.style.zIndex = renderer.layerManager.getZIndex(model).toString();
// Add brush-specific class for styling
element.classList.add('brush-element');
};
+1
View File
@@ -1,6 +1,7 @@
export * from './adapter';
export * from './brush-tool';
export * from './element-renderer';
export * from './element-renderer/brush-dom';
export * from './eraser-tool';
export * from './highlighter-tool';
export * from './toolbar/configs';
+2
View File
@@ -6,6 +6,7 @@ import {
import { BrushTool } from './brush-tool';
import { effects } from './effects';
import { BrushElementRendererExtension } from './element-renderer';
import { BrushDomRendererExtension } from './element-renderer/brush-dom';
import { EraserTool } from './eraser-tool';
import { HighlighterTool } from './highlighter-tool';
import {
@@ -30,6 +31,7 @@ export class BrushViewExtension extends ViewExtensionProvider {
context.register(HighlighterTool);
context.register(BrushElementRendererExtension);
context.register(BrushDomRendererExtension);
context.register(brushToolbarExtension);
context.register(highlighterToolbarExtension);
@@ -0,0 +1,11 @@
import { DomElementRendererExtension } from '@blocksuite/affine-block-surface';
import { connectorDomRenderer } from './connector-dom/index.js';
/**
* Extension to register the DOM-based renderer for 'connector' elements.
*/
export const ConnectorDomRendererExtension = DomElementRendererExtension(
'connector',
connectorDomRenderer
);
@@ -0,0 +1,367 @@
import type { DomRenderer } from '@blocksuite/affine-block-surface';
import {
type ConnectorElementModel,
ConnectorMode,
DefaultTheme,
type PointStyle,
} from '@blocksuite/affine-model';
import { PointLocation, SVGPathBuilder } from '@blocksuite/global/gfx';
import { isConnectorWithLabel } from '../../connector-manager.js';
import { DEFAULT_ARROW_SIZE } from '../utils.js';
interface PathBounds {
minX: number;
minY: number;
maxX: number;
maxY: number;
}
function calculatePathBounds(path: PointLocation[]): PathBounds {
if (path.length === 0) {
return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
}
let minX = path[0][0];
let minY = path[0][1];
let maxX = path[0][0];
let maxY = path[0][1];
for (const point of path) {
minX = Math.min(minX, point[0]);
minY = Math.min(minY, point[1]);
maxX = Math.max(maxX, point[0]);
maxY = Math.max(maxY, point[1]);
}
return { minX, minY, maxX, maxY };
}
function createConnectorPath(
points: PointLocation[],
mode: ConnectorMode
): string {
if (points.length < 2) return '';
const pathBuilder = new SVGPathBuilder();
pathBuilder.moveTo(points[0][0], points[0][1]);
if (mode === ConnectorMode.Curve) {
// Use bezier curves
for (let i = 1; i < points.length; i++) {
const prev = points[i - 1];
const curr = points[i];
pathBuilder.curveTo(
prev.absOut[0],
prev.absOut[1],
curr.absIn[0],
curr.absIn[1],
curr[0],
curr[1]
);
}
} else {
// Use straight lines
for (let i = 1; i < points.length; i++) {
pathBuilder.lineTo(points[i][0], points[i][1]);
}
}
return pathBuilder.build();
}
function createArrowMarker(
id: string,
style: PointStyle,
color: string,
strokeWidth: number,
isStart: boolean = false
): SVGMarkerElement {
const marker = document.createElementNS(
'http://www.w3.org/2000/svg',
'marker'
);
const size = DEFAULT_ARROW_SIZE * (strokeWidth / 2);
marker.id = id;
marker.setAttribute('viewBox', '0 0 20 20');
marker.setAttribute('refX', isStart ? '20' : '0');
marker.setAttribute('refY', '10');
marker.setAttribute('markerWidth', String(size));
marker.setAttribute('markerHeight', String(size));
marker.setAttribute('orient', 'auto');
marker.setAttribute('markerUnits', 'strokeWidth');
switch (style) {
case 'Arrow': {
const path = document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
path.setAttribute(
'd',
isStart ? 'M 20 5 L 10 10 L 20 15 Z' : 'M 0 5 L 10 10 L 0 15 Z'
);
path.setAttribute('fill', color);
path.setAttribute('stroke', color);
marker.append(path);
break;
}
case 'Triangle': {
const path = document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
path.setAttribute(
'd',
isStart ? 'M 20 7 L 12 10 L 20 13 Z' : 'M 0 7 L 8 10 L 0 13 Z'
);
path.setAttribute('fill', color);
path.setAttribute('stroke', color);
marker.append(path);
break;
}
case 'Circle': {
const circle = document.createElementNS(
'http://www.w3.org/2000/svg',
'circle'
);
circle.setAttribute('cx', '10');
circle.setAttribute('cy', '10');
circle.setAttribute('r', '4');
circle.setAttribute('fill', color);
circle.setAttribute('stroke', color);
marker.append(circle);
break;
}
case 'Diamond': {
const path = document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
path.setAttribute('d', 'M 10 6 L 14 10 L 10 14 L 6 10 Z');
path.setAttribute('fill', color);
path.setAttribute('stroke', color);
marker.append(path);
break;
}
}
return marker;
}
function renderConnectorLabel(
model: ConnectorElementModel,
container: HTMLElement,
renderer: DomRenderer,
zoom: number
) {
if (!isConnectorWithLabel(model) || !model.labelXYWH) {
return;
}
const [lx, ly, lw, lh] = model.labelXYWH;
const {
labelStyle: {
color,
fontSize,
fontWeight,
fontStyle,
fontFamily,
textAlign,
},
} = model;
// Create label element
const labelElement = document.createElement('div');
labelElement.style.position = 'absolute';
labelElement.style.left = `${lx * zoom}px`;
labelElement.style.top = `${ly * zoom}px`;
labelElement.style.width = `${lw * zoom}px`;
labelElement.style.height = `${lh * zoom}px`;
labelElement.style.pointerEvents = 'none';
labelElement.style.overflow = 'hidden';
labelElement.style.display = 'flex';
labelElement.style.alignItems = 'center';
labelElement.style.justifyContent =
textAlign === 'center'
? 'center'
: textAlign === 'right'
? 'flex-end'
: 'flex-start';
// Style the text
labelElement.style.color = renderer.getColorValue(
color,
DefaultTheme.black,
true
);
labelElement.style.fontSize = `${fontSize * zoom}px`;
labelElement.style.fontWeight = fontWeight;
labelElement.style.fontStyle = fontStyle;
labelElement.style.fontFamily = fontFamily;
labelElement.style.textAlign = textAlign;
labelElement.style.lineHeight = '1.2';
labelElement.style.whiteSpace = 'pre-wrap';
labelElement.style.wordWrap = 'break-word';
// Add text content
if (model.text) {
labelElement.textContent = model.text.toString();
}
container.append(labelElement);
}
/**
* Renders a ConnectorElementModel to a given HTMLElement using DOM/SVG.
* This function is intended to be registered via the DomElementRendererExtension.
*
* @param model - The connector element model containing rendering properties.
* @param element - The HTMLElement to apply the connector's styles to.
* @param renderer - The main DOMRenderer instance, providing access to viewport and color utilities.
*/
export const connectorDomRenderer = (
model: ConnectorElementModel,
element: HTMLElement,
renderer: DomRenderer
): void => {
const { zoom } = renderer.viewport;
const {
mode,
path: points,
strokeStyle,
frontEndpointStyle,
rearEndpointStyle,
strokeWidth,
stroke,
} = model;
// Clear previous content
element.innerHTML = '';
// Early return if no path points
if (!points || points.length < 2) {
return;
}
// Calculate bounds for the SVG viewBox
const pathBounds = calculatePathBounds(points);
const padding = Math.max(strokeWidth * 2, 20); // Add padding for arrows
const svgWidth = (pathBounds.maxX - pathBounds.minX + padding * 2) * zoom;
const svgHeight = (pathBounds.maxY - pathBounds.minY + padding * 2) * zoom;
const offsetX = pathBounds.minX - padding;
const offsetY = pathBounds.minY - padding;
// Create SVG element
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.style.position = 'absolute';
svg.style.left = `${offsetX * zoom}px`;
svg.style.top = `${offsetY * zoom}px`;
svg.style.width = `${svgWidth}px`;
svg.style.height = `${svgHeight}px`;
svg.style.overflow = 'visible';
svg.style.pointerEvents = 'none';
svg.setAttribute('viewBox', `0 0 ${svgWidth / zoom} ${svgHeight / zoom}`);
// Create defs for markers
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
svg.append(defs);
const strokeColor = renderer.getColorValue(
stroke,
DefaultTheme.connectorColor,
true
);
// Create markers for endpoints
let startMarkerId = '';
let endMarkerId = '';
if (frontEndpointStyle !== 'None') {
startMarkerId = `start-marker-${model.id}`;
const startMarker = createArrowMarker(
startMarkerId,
frontEndpointStyle,
strokeColor,
strokeWidth,
true
);
defs.append(startMarker);
}
if (rearEndpointStyle !== 'None') {
endMarkerId = `end-marker-${model.id}`;
const endMarker = createArrowMarker(
endMarkerId,
rearEndpointStyle,
strokeColor,
strokeWidth,
false
);
defs.append(endMarker);
}
// Create path element
const pathElement = document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
// Adjust points relative to the SVG coordinate system
const adjustedPoints = points.map(point => {
const adjustedPoint = new PointLocation([
point[0] - offsetX,
point[1] - offsetY,
]);
if (point.absIn) {
adjustedPoint.in = [
point.absIn[0] - offsetX - adjustedPoint[0],
point.absIn[1] - offsetY - adjustedPoint[1],
];
}
if (point.absOut) {
adjustedPoint.out = [
point.absOut[0] - offsetX - adjustedPoint[0],
point.absOut[1] - offsetY - adjustedPoint[1],
];
}
return adjustedPoint;
});
const pathData = createConnectorPath(adjustedPoints, mode);
pathElement.setAttribute('d', pathData);
pathElement.setAttribute('stroke', strokeColor);
pathElement.setAttribute('stroke-width', String(strokeWidth));
pathElement.setAttribute('fill', 'none');
pathElement.setAttribute('stroke-linecap', 'round');
pathElement.setAttribute('stroke-linejoin', 'round');
// Apply stroke style
if (strokeStyle === 'dash') {
pathElement.setAttribute('stroke-dasharray', '12,12');
}
// Apply markers
if (startMarkerId) {
pathElement.setAttribute('marker-start', `url(#${startMarkerId})`);
}
if (endMarkerId) {
pathElement.setAttribute('marker-end', `url(#${endMarkerId})`);
}
svg.append(pathElement);
element.append(svg);
// Set element size and position
element.style.width = `${model.w * zoom}px`;
element.style.height = `${model.h * zoom}px`;
element.style.overflow = 'visible';
element.style.pointerEvents = 'none';
// Set z-index for layering
element.style.zIndex = renderer.layerManager.getZIndex(model).toString();
// Render label if present
renderConnectorLabel(model, element, renderer, zoom);
};
@@ -2,6 +2,7 @@ export * from './adapter';
export * from './connector-manager';
export * from './connector-tool';
export * from './element-renderer';
export { ConnectorDomRendererExtension } from './element-renderer/connector-dom';
export * from './element-transform';
export * from './text';
export * from './toolbar/config';
@@ -7,6 +7,7 @@ import { ConnectionOverlay } from './connector-manager';
import { ConnectorTool } from './connector-tool';
import { effects } from './effects';
import { ConnectorElementRendererExtension } from './element-renderer';
import { ConnectorDomRendererExtension } from './element-renderer/connector-dom';
import { ConnectorFilter } from './element-transform';
import { connectorToolbarExtension } from './toolbar/config';
import { connectorQuickTool } from './toolbar/quick-tool';
@@ -24,6 +25,7 @@ export class ConnectorViewExtension extends ViewExtensionProvider {
super.setup(context);
context.register(ConnectorElementView);
context.register(ConnectorElementRendererExtension);
context.register(ConnectorDomRendererExtension);
if (this.isEdgeless(context.scope)) {
context.register(ConnectorTool);
context.register(ConnectorFilter);
@@ -1,6 +1,6 @@
import { OverlayIdentifier } from '@blocksuite/affine-block-surface';
import { MindmapElementModel } from '@blocksuite/affine-model';
import { type Bound } from '@blocksuite/global/gfx';
import { Bound } from '@blocksuite/global/gfx';
import {
type DragExtensionInitializeContext,
type ExtensionDragMoveContext,
@@ -74,63 +74,47 @@ export class SnapExtension extends InteractivityExtension {
return {};
}
let alignBound: Bound | null = null;
return {
onResizeStart(context) {
snapOverlay.setMovingElements(context.elements);
alignBound = snapOverlay.setMovingElements(context.elements);
},
onResizeMove(context) {
const {
handle,
originalBound,
scaleX,
scaleY,
handleSign,
currentHandlePos,
elements,
} = context;
const rotate = elements.length > 1 ? 0 : elements[0].rotate;
const alignDirection: ('vertical' | 'horizontal')[] = [];
let switchDirection = false;
let nx = handleSign.x;
let ny = handleSign.y;
if (handle.length > 6) {
alignDirection.push('vertical', 'horizontal');
} else if (rotate % 90 === 0) {
nx =
handleSign.x * Math.cos((rotate / 180) * Math.PI) -
handleSign.y * Math.sin((rotate / 180) * Math.PI);
ny =
handleSign.x * Math.sin((rotate / 180) * Math.PI) +
handleSign.y * Math.cos((rotate / 180) * Math.PI);
if (Math.abs(nx) > Math.abs(ny)) {
alignDirection.push('horizontal');
} else {
alignDirection.push('vertical');
}
if (rotate % 180 !== 0) {
switchDirection = true;
}
if (!alignBound || alignBound.w === 0 || alignBound.h === 0) {
return;
}
if (alignDirection.length > 0) {
const rst = snapOverlay.alignResize(
currentHandlePos,
alignDirection
const { handle, handleSign, lockRatio } = context;
let { dx, dy } = context;
if (lockRatio) {
const min = Math.min(
Math.abs(dx / alignBound.w),
Math.abs(dy / alignBound.h)
);
const dx = switchDirection ? ny * rst.dy : nx * rst.dx;
const dy = switchDirection ? nx * rst.dx : ny * rst.dy;
context.suggest({
scaleX: scaleX + dx / originalBound.w,
scaleY: scaleY + dy / originalBound.h,
});
dx = min * Math.sign(dx) * alignBound.w;
dy = min * Math.sign(dy) * alignBound.h;
}
const currentBound = new Bound(
alignBound.x +
(handle.includes('left') ? -dx * handleSign.xSign : 0),
alignBound.y +
(handle.includes('top') ? -dy * handleSign.ySign : 0),
Math.abs(alignBound.w + dx * handleSign.xSign),
Math.abs(alignBound.h + dy * handleSign.ySign)
);
const alignRst = snapOverlay.align(currentBound);
context.suggest({
dx: alignRst.dx + context.dx,
dy: alignRst.dy + context.dy,
});
},
onResizeEnd() {
alignBound = null;
snapOverlay.clear();
},
};
@@ -3,7 +3,7 @@ import {
ConnectorElementModel,
MindmapElementModel,
} from '@blocksuite/affine-model';
import { almostEqual, Bound, type IVec, Point } from '@blocksuite/global/gfx';
import { almostEqual, Bound, Point } from '@blocksuite/global/gfx';
import type { GfxModel } from '@blocksuite/std/gfx';
interface Distance {
@@ -586,60 +586,6 @@ export class SnapOverlay extends Overlay {
);
}
alignResize(position: IVec, direction: ('vertical' | 'horizontal')[]) {
const rst = { dx: 0, dy: 0 };
const { viewport } = this.gfx;
const threshold = ALIGN_THRESHOLD / viewport.zoom;
const searchBound = new Bound(
position[0] - threshold / 2,
position[1] - threshold / 2,
threshold,
threshold
);
const alignBound = new Bound(position[0], position[1], 0, 0);
this._intraGraphicAlignLines = {
horizontal: [],
vertical: [],
};
this._distributedAlignLines = [];
this._updateAlignCandidates(searchBound);
for (const other of this._referenceBounds.all) {
const closestDistances = this._calculateClosestDistances(
alignBound,
other
);
if (
direction.includes('horizontal') &&
closestDistances.horiz &&
(!this._intraGraphicAlignLines.horizontal.length ||
Math.abs(closestDistances.horiz.distance) < Math.abs(rst.dx))
) {
this._updateXAlignPoint(rst, alignBound, other, closestDistances);
}
if (
direction.includes('vertical') &&
closestDistances.vert &&
(!this._intraGraphicAlignLines.vertical.length ||
Math.abs(closestDistances.vert.distance) < Math.abs(rst.dy))
) {
this._updateYAlignPoint(rst, alignBound, other, closestDistances);
}
}
this._intraGraphicAlignLines.horizontal =
this._intraGraphicAlignLines.horizontal.slice(0, 1);
this._intraGraphicAlignLines.vertical =
this._intraGraphicAlignLines.vertical.slice(0, 1);
this._renderer?.refresh();
return rst;
}
align(bound: Bound): { dx: number; dy: number } {
const rst = { dx: 0, dy: 0 };
const threshold = ALIGN_THRESHOLD / this.gfx.viewport.zoom;
@@ -1,6 +1,7 @@
import type { DomRenderer } from '@blocksuite/affine-block-surface';
import type { ShapeElementModel } from '@blocksuite/affine-model';
import { DefaultTheme } from '@blocksuite/affine-model';
import { SVGShapeBuilder } from '@blocksuite/global/gfx';
import { manageClassNames, setStyles } from './utils';
@@ -122,25 +123,22 @@ export const shapeDomRenderer = (
element.style.backgroundColor = 'transparent'; // Host element is transparent
const strokeW = model.strokeWidth;
const halfStroke = strokeW / 2; // Calculate half stroke width for point adjustment
let svgPoints = '';
if (model.shapeType === 'diamond') {
// Adjusted points for diamond
svgPoints = [
`${unscaledWidth / 2},${halfStroke}`,
`${unscaledWidth - halfStroke},${unscaledHeight / 2}`,
`${unscaledWidth / 2},${unscaledHeight - halfStroke}`,
`${halfStroke},${unscaledHeight / 2}`,
].join(' ');
// Generate diamond points using shared utility
svgPoints = SVGShapeBuilder.diamond(
unscaledWidth,
unscaledHeight,
strokeW
);
} else {
// triangle
// Adjusted points for triangle
svgPoints = [
`${unscaledWidth / 2},${halfStroke}`,
`${unscaledWidth - halfStroke},${unscaledHeight - halfStroke}`,
`${halfStroke},${unscaledHeight - halfStroke}`,
].join(' ');
// triangle - generate triangle points using shared utility
svgPoints = SVGShapeBuilder.triangle(
unscaledWidth,
unscaledHeight,
strokeW
);
}
// Determine if stroke should be visible and its color
@@ -188,8 +188,6 @@ export class AffineLatexNode extends SignalWatcher(
this._editorAbortController?.abort();
this._editorAbortController = new AbortController();
blockComponent.selection.setGroup('note', []);
const portal = createLitPortal({
template: html`<latex-editor-menu
.std=${this.std}
@@ -30,12 +30,11 @@ import { AttachmentBlockTransformer } from './attachment-transformer.js';
*/
type BackwardCompatibleUndefined = undefined;
export const AttachmentBlockStyles = [
export const AttachmentBlockStyles: EmbedCardStyle[] = [
'cubeThick',
'horizontalThin',
'pdf',
'citation',
] as const satisfies EmbedCardStyle[];
] as const;
export type AttachmentBlockProps = {
name: string;
@@ -15,13 +15,13 @@ import type {
LinkPreviewData,
} from '../../utils/index.js';
export const BookmarkStyles = [
export const BookmarkStyles: EmbedCardStyle[] = [
'vertical',
'horizontal',
'list',
'cube',
'citation',
] as const satisfies EmbedCardStyle[];
] as const;
export type BookmarkBlockProps = {
style: (typeof BookmarkStyles)[number];
@@ -8,7 +8,7 @@ export type EmbedFigmaBlockUrlData = {
description: string | null;
};
export const EmbedFigmaStyles = ['figma'] as const satisfies EmbedCardStyle[];
export const EmbedFigmaStyles: EmbedCardStyle[] = ['figma'] as const;
export type EmbedFigmaBlockProps = {
style: (typeof EmbedFigmaStyles)[number];
@@ -13,12 +13,12 @@ export type EmbedGithubBlockUrlData = {
assignees: string[] | null;
};
export const EmbedGithubStyles = [
export const EmbedGithubStyles: EmbedCardStyle[] = [
'vertical',
'horizontal',
'list',
'cube',
] as const satisfies EmbedCardStyle[];
] as const;
export type EmbedGithubBlockProps = {
style: (typeof EmbedGithubStyles)[number];
@@ -3,7 +3,7 @@ import { BlockModel } from '@blocksuite/store';
import type { EmbedCardStyle } from '../../../utils/index.js';
import { defineEmbedModel } from '../../../utils/index.js';
export const EmbedHtmlStyles = ['html'] as const satisfies EmbedCardStyle[];
export const EmbedHtmlStyles: EmbedCardStyle[] = ['html'] as const;
export type EmbedHtmlBlockProps = {
style: (typeof EmbedHtmlStyles)[number];
@@ -7,7 +7,7 @@ import { BlockModel } from '@blocksuite/store';
import { type EmbedCardStyle } from '../../../utils/index.js';
export const EmbedIframeStyles = ['figma'] as const satisfies EmbedCardStyle[];
export const EmbedIframeStyles: EmbedCardStyle[] = ['figma'] as const;
export type EmbedIframeBlockProps = {
url: string; // the original url that user input
@@ -4,17 +4,17 @@ import type { ReferenceInfo } from '../../../consts/doc.js';
import type { EmbedCardStyle } from '../../../utils/index.js';
import { defineEmbedModel } from '../../../utils/index.js';
export const EmbedLinkedDocStyles = [
export const EmbedLinkedDocStyles: EmbedCardStyle[] = [
'vertical',
'horizontal',
'list',
'cube',
'horizontalThin',
'citation',
] as const satisfies EmbedCardStyle[];
];
export type EmbedLinkedDocBlockProps = {
style: (typeof EmbedLinkedDocStyles)[number];
style: EmbedCardStyle;
caption: string | null;
footnoteIdentifier: string | null;
} & ReferenceInfo;
@@ -10,7 +10,7 @@ export type EmbedLoomBlockUrlData = {
description: string | null;
};
export const EmbedLoomStyles = ['video'] as const satisfies EmbedCardStyle[];
export const EmbedLoomStyles: EmbedCardStyle[] = ['video'] as const;
export type EmbedLoomBlockProps = {
style: (typeof EmbedLoomStyles)[number];
@@ -5,9 +5,7 @@ import type { ReferenceInfo } from '../../../consts/doc.js';
import type { EmbedCardStyle } from '../../../utils/index.js';
import { defineEmbedModel } from '../../../utils/index.js';
export const EmbedSyncedDocStyles = [
'syncedDoc',
] as const satisfies EmbedCardStyle[];
export const EmbedSyncedDocStyles: EmbedCardStyle[] = ['syncedDoc'];
export type EmbedSyncedDocBlockProps = {
style: EmbedCardStyle;
@@ -1,7 +1,6 @@
import type { GfxModel } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import type { BookmarkBlockModel } from '../bookmark';
import { EmbedFigmaModel } from './figma';
import { EmbedGithubModel } from './github';
import type { EmbedHtmlModel } from './html';
@@ -31,10 +30,7 @@ export type EmbedCardModel = InstanceType<
ExternalEmbedModel | InternalEmbedModel
>;
export type LinkableEmbedModel =
| EmbedCardModel
| EmbedIframeBlockModel
| BookmarkBlockModel;
export type LinkableEmbedModel = EmbedCardModel | EmbedIframeBlockModel;
export type BuiltInEmbedModel = EmbedCardModel | EmbedHtmlModel;
@@ -13,7 +13,7 @@ export type EmbedYoutubeBlockUrlData = {
creatorImage: string | null;
};
export const EmbedYoutubeStyles = ['video'] as const satisfies EmbedCardStyle[];
export const EmbedYoutubeStyles: EmbedCardStyle[] = ['video'] as const;
export type EmbedYoutubeBlockProps = {
style: (typeof EmbedYoutubeStyles)[number];
@@ -80,8 +80,4 @@ describe('isValidUrl: determining whether a URL is valid is very complicated', (
// See also https://stackoverflow.com/questions/9238640/how-long-can-a-tld-possibly-be#:~:text=Longest%20TLD%20up%20to%20date,17%20when%20decoded%20%5Bverm%C3%B6gensberatung%5D.
expect(isValidUrl('example.xn--vermgensberatung-pwb')).toEqual(false);
});
test('should allow ip address url when origin is same', () => {
expect(isValidUrl('http://127.0.0.1', 'http://127.0.0.1')).toEqual(true);
});
});
@@ -37,7 +37,7 @@ const handlePoint = (
};
const sliceText = (slots: TransformerSlots, std: EditorHost['std']) => {
const afterExportSubscription = slots.afterExport.subscribe(payload => {
slots.afterExport.subscribe(payload => {
if (payload.type === 'block') {
const snapshot = payload.snapshot;
@@ -53,14 +53,10 @@ const sliceText = (slots: TransformerSlots, std: EditorHost['std']) => {
}
}
});
return () => {
afterExportSubscription.unsubscribe();
};
};
export const copyMiddleware = (std: BlockStdScope): TransformerMiddleware => {
return ({ slots }) => {
return sliceText(slots, std);
sliceText(slots, std);
};
};
@@ -3,7 +3,7 @@ import type { TransformerMiddleware } from '@blocksuite/store';
export const fileNameMiddleware =
(fileName?: string): TransformerMiddleware =>
({ slots }) => {
const beforeImportSubscription = slots.beforeImport.subscribe(payload => {
slots.beforeImport.subscribe(payload => {
if (payload.type !== 'page') {
return;
}
@@ -20,8 +20,4 @@ export const fileNameMiddleware =
],
};
});
return () => {
beforeImportSubscription.unsubscribe();
};
};
@@ -528,7 +528,7 @@ export const pasteMiddleware = (
): TransformerMiddleware => {
return ({ slots }) => {
let tr: PasteTr | undefined;
const beforeImportSubscription = slots.beforeImport.subscribe(payload => {
slots.beforeImport.subscribe(payload => {
if (payload.type === 'slice') {
const { snapshot } = payload;
flatNote(snapshot);
@@ -543,18 +543,13 @@ export const pasteMiddleware = (
}
}
});
const afterImportSubscription = slots.afterImport.subscribe(payload => {
slots.afterImport.subscribe(payload => {
if (tr && payload.type === 'slice') {
tr.pasted();
tr.focusPasted();
tr.convertToLinkedDoc();
}
});
return () => {
beforeImportSubscription.unsubscribe();
afterImportSubscription.unsubscribe();
};
};
};
@@ -32,7 +32,7 @@ export const replaceIdMiddleware =
map(({ model }) => model)
);
const afterImportBlockSubscription = afterImportBlock$
afterImportBlock$
.pipe(filter(model => matchModels(model, [DatabaseBlockModel])))
.subscribe(model => {
Object.keys(model.props.cells).forEach(cellId => {
@@ -44,7 +44,7 @@ export const replaceIdMiddleware =
});
// replace LinkedPage pageId with new id in paragraph blocks
const replaceLinkedPageIdSubscription = afterImportBlock$
afterImportBlock$
.pipe(
filter(model =>
matchModels(model, [ParagraphBlockModel, ListBlockModel])
@@ -84,7 +84,7 @@ export const replaceIdMiddleware =
}
});
const replaceSurfaceRefIdSubscription = afterImportBlock$
afterImportBlock$
.pipe(filter(model => matchModels(model, [SurfaceRefBlockModel])))
.subscribe(model => {
const original = model.props.reference;
@@ -105,7 +105,7 @@ export const replaceIdMiddleware =
});
// TODO(@fundon): process linked block/element
const replaceLinkedDocIdSubscription = afterImportBlock$
afterImportBlock$
.pipe(
filter(model =>
matchModels(model, [EmbedLinkedDocModel, EmbedSyncedDocModel])
@@ -128,7 +128,7 @@ export const replaceIdMiddleware =
// Before Import
const beforeImportPageSubscription = slots.beforeImport
slots.beforeImport
.pipe(filter(payload => payload.type === 'page'))
.subscribe(payload => {
if (idMap.has(payload.snapshot.meta.id)) {
@@ -140,7 +140,7 @@ export const replaceIdMiddleware =
payload.snapshot.meta.id = newId;
});
const beforeImportBlockSubscription = slots.beforeImport
slots.beforeImport
.pipe(
filter(
(payload): payload is BeforeImportBlockPayload =>
@@ -244,13 +244,4 @@ export const replaceIdMiddleware =
});
}
});
return () => {
afterImportBlockSubscription.unsubscribe();
replaceLinkedPageIdSubscription.unsubscribe();
replaceSurfaceRefIdSubscription.unsubscribe();
replaceLinkedDocIdSubscription.unsubscribe();
beforeImportPageSubscription.unsubscribe();
beforeImportBlockSubscription.unsubscribe();
};
};
@@ -5,42 +5,33 @@ export const surfaceRefToEmbed =
(std: BlockStdScope): TransformerMiddleware =>
({ slots }) => {
let pageId: string | null = null;
const beforeImportSliceSubscription = slots.beforeImport.subscribe(
payload => {
if (payload.type === 'slice') {
pageId = payload.snapshot.pageId;
}
slots.beforeImport.subscribe(payload => {
if (payload.type === 'slice') {
pageId = payload.snapshot.pageId;
}
);
const beforeImportBlockSubscription = slots.beforeImport.subscribe(
payload => {
// only handle surface-ref block snapshot
if (
payload.type !== 'block' ||
payload.snapshot.flavour !== 'affine:surface-ref'
)
return;
});
slots.beforeImport.subscribe(payload => {
// only handle surface-ref block snapshot
if (
payload.type !== 'block' ||
payload.snapshot.flavour !== 'affine:surface-ref'
)
return;
// turn into embed-linked-doc if the current doc is different from the pageId of the surface-ref block
const isNotSameDoc = pageId !== std.store.doc.id;
if (pageId && isNotSameDoc) {
// The blockId of the original surface-ref block
const blockId = payload.snapshot.id;
payload.snapshot.id = std.workspace.idGenerator();
payload.snapshot.flavour = 'affine:embed-linked-doc';
payload.snapshot.props = {
pageId,
params: {
mode: 'page',
blockIds: [blockId],
},
};
}
// turn into embed-linked-doc if the current doc is different from the pageId of the surface-ref block
const isNotSameDoc = pageId !== std.store.doc.id;
if (pageId && isNotSameDoc) {
// The blockId of the original surface-ref block
const blockId = payload.snapshot.id;
payload.snapshot.id = std.workspace.idGenerator();
payload.snapshot.flavour = 'affine:embed-linked-doc';
payload.snapshot.props = {
pageId,
params: {
mode: 'page',
blockIds: [blockId],
},
};
}
);
return () => {
beforeImportSliceSubscription.unsubscribe();
beforeImportBlockSubscription.unsubscribe();
};
});
};
@@ -3,13 +3,9 @@ import type { DocMeta, TransformerMiddleware } from '@blocksuite/store';
export const titleMiddleware =
(metas: DocMeta[]): TransformerMiddleware =>
({ slots, adapterConfigs }) => {
const beforeExportSubscription = slots.beforeExport.subscribe(() => {
slots.beforeExport.subscribe(() => {
for (const meta of metas) {
adapterConfigs.set('title:' + meta.id, meta.title);
}
});
return () => {
beforeExportSubscription.unsubscribe();
};
};
@@ -79,7 +79,7 @@ export const uploadMiddleware = (
}
}
const blockViewSubscription = blockView$
blockView$
.pipe(
map(payload => {
if (assetsManager.uploadingAssetsMap.size === 0) return null;
@@ -110,9 +110,5 @@ export const uploadMiddleware = (
)
)
.subscribe();
return () => {
blockViewSubscription.unsubscribe();
};
};
};
+5 -12
View File
@@ -11,9 +11,6 @@ const ALLOWED_SCHEMES = new Set([
// https://publicsuffix.org/
const TLD_REGEXP = /(?:\.[a-zA-Z]+)?(\.[a-zA-Z]{2,})$/;
const IPV4_ADDR_REGEXP =
/^(25[0-5]|2[0-4]\d|[01]?\d\d?)(\.(25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$/;
const toURL = (str: string) => {
try {
if (!URL.canParse(str)) return null;
@@ -24,20 +21,16 @@ const toURL = (str: string) => {
}
};
function resolveURL(str: string, baseUrl: string, padded = false) {
function resolveURL(str: string) {
const url = toURL(str);
if (!url) return null;
const protocol = url.protocol.substring(0, url.protocol.length - 1);
const hostname = url.hostname;
const origin = url.origin;
let allowed = ALLOWED_SCHEMES.has(protocol);
if (allowed && hostname.includes('.')) {
allowed =
origin === baseUrl ||
TLD_REGEXP.test(hostname) ||
(padded ? false : IPV4_ADDR_REGEXP.test(hostname));
allowed = TLD_REGEXP.test(hostname);
}
return { url, allowed };
@@ -75,10 +68,10 @@ export function normalizeUrl(str: string) {
*
* For more detail see https://www.ietf.org/rfc/rfc1738.txt
*/
export function isValidUrl(str: string, baseUrl = location.origin) {
export function isValidUrl(str: string) {
str = str.trim();
let result = resolveURL(str, baseUrl);
let result = resolveURL(str);
if (result && !result.allowed) return false;
@@ -87,7 +80,7 @@ export function isValidUrl(str: string, baseUrl = location.origin) {
if (!hasScheme) {
const dotIdx = str.indexOf('.');
if (dotIdx > 0 && dotIdx < str.length - 1) {
result = resolveURL(`https://${str}`, baseUrl, true);
result = resolveURL(`https://${str}`);
}
}
}
@@ -40,7 +40,7 @@ export const gfxBlocksFilter = (
}
return ({ slots, transformerConfigs }) => {
const beforeExportSubscription = slots.beforeExport.subscribe(payload => {
slots.beforeExport.subscribe(payload => {
if (payload.type !== 'block') {
return;
}
@@ -54,7 +54,7 @@ export const gfxBlocksFilter = (
}
});
const afterExportSubscription = slots.afterExport.subscribe(payload => {
slots.afterExport.subscribe(payload => {
if (payload.type !== 'block') {
return;
}
@@ -110,10 +110,5 @@ export const gfxBlocksFilter = (
});
}
});
return () => {
beforeExportSubscription.unsubscribe();
afterExportSubscription.unsubscribe();
};
};
};
@@ -1,56 +0,0 @@
import {
AttachmentBlockModel,
BookmarkBlockModel,
EmbedGithubModel,
EmbedLinkedDocModel,
NoteBlockModel,
} from '@blocksuite/affine-model';
import { matchModels } from '@blocksuite/affine-shared/utils';
import type { BlockStdScope } from '@blocksuite/std';
import type { TransformerMiddleware } from '@blocksuite/store';
export const cardStyleUpdater =
(std: BlockStdScope): TransformerMiddleware =>
({ slots }) => {
slots.beforeImport.subscribe(payload => {
if (payload.type !== 'block' || !payload.parent) return;
const parentModel = std.store.getModelById(payload.parent);
if (!matchModels(parentModel, [NoteBlockModel])) return;
// TODO(@L-Sun): Refactor this after refactor `store.moveBlocks`
// Currently, drag a block will use store.moveBlocks to update the tree of blocks
// but the instance of it is not changed.
// So change the style of snapshot.props in the middleware is not working.
// Instead, we can change the style of the model instance in the middleware,
const model = std.store.getModelById(payload.snapshot.id);
if (!model) return;
if (model instanceof AttachmentBlockModel) {
std.store.updateBlock(model, {
style: 'horizontalThin',
});
return;
}
if (model instanceof BookmarkBlockModel) {
std.store.updateBlock(model, {
style: 'horizontal',
});
return;
}
if (model instanceof EmbedGithubModel) {
std.store.updateBlock(model, {
style: 'horizontal',
});
return;
}
if (model instanceof EmbedLinkedDocModel) {
std.store.updateBlock(model, {
style: 'horizontal',
});
return;
}
});
};
@@ -9,45 +9,36 @@ export const newIdCrossDoc =
let samePage = false;
const oldToNewIdMap = new Map<string, string>();
const beforeImportSliceSubscription = slots.beforeImport.subscribe(
payload => {
if (payload.type === 'slice') {
samePage = payload.snapshot.pageId === std.store.id;
}
if (payload.type === 'block' && !samePage) {
const newId = std.workspace.idGenerator();
oldToNewIdMap.set(payload.snapshot.id, newId);
payload.snapshot.id = newId;
}
slots.beforeImport.subscribe(payload => {
if (payload.type === 'slice') {
samePage = payload.snapshot.pageId === std.store.id;
}
);
if (payload.type === 'block' && !samePage) {
const newId = std.workspace.idGenerator();
const afterImportBlockSubscription = slots.afterImport.subscribe(
payload => {
if (
!samePage &&
payload.type === 'block' &&
matchModels(payload.model, [DatabaseBlockModel])
) {
const originalCells = payload.model.props.cells;
const newCells = {
...originalCells,
};
Object.keys(originalCells).forEach(cellId => {
if (oldToNewIdMap.has(cellId)) {
newCells[oldToNewIdMap.get(cellId)!] = originalCells[cellId];
}
});
payload.model.props.cells$.value = newCells;
}
oldToNewIdMap.set(payload.snapshot.id, newId);
payload.snapshot.id = newId;
}
);
});
return () => {
beforeImportSliceSubscription.unsubscribe();
afterImportBlockSubscription.unsubscribe();
};
slots.afterImport.subscribe(payload => {
if (
!samePage &&
payload.type === 'block' &&
matchModels(payload.model, [DatabaseBlockModel])
) {
const originalCells = payload.model.props.cells;
const newCells = {
...originalCells,
};
Object.keys(originalCells).forEach(cellId => {
if (oldToNewIdMap.has(cellId)) {
newCells[oldToNewIdMap.get(cellId)!] = originalCells[cellId];
}
});
payload.model.props.cells$.value = newCells;
}
});
};
@@ -7,25 +7,19 @@ import type { TransformerMiddleware } from '@blocksuite/store';
export const reorderList =
(std: BlockStdScope): TransformerMiddleware =>
({ slots }) => {
const afterImportBlockSubscription = slots.afterImport.subscribe(
payload => {
if (payload.type === 'block') {
const model = payload.model;
if (
matchModels(model, [ListBlockModel]) &&
model.props.type === 'numbered'
) {
const next = std.store.getNext(model);
correctNumberedListsOrderToPrev(std.store, model);
if (next) {
correctNumberedListsOrderToPrev(std.store, next);
}
slots.afterImport.subscribe(payload => {
if (payload.type === 'block') {
const model = payload.model;
if (
matchModels(model, [ListBlockModel]) &&
model.props.type === 'numbered'
) {
const next = std.store.getNext(model);
correctNumberedListsOrderToPrev(std.store, model);
if (next) {
correctNumberedListsOrderToPrev(std.store, next);
}
}
}
);
return () => {
afterImportBlockSubscription.unsubscribe();
};
});
};
@@ -76,7 +76,6 @@ import last from 'lodash-es/last';
import type { AffineDragHandleWidget } from '../drag-handle.js';
import { PreviewHelper } from '../helpers/preview-helper.js';
import { gfxBlocksFilter } from '../middleware/blocks-filter.js';
import { cardStyleUpdater } from '../middleware/card-style-updater.js';
import { newIdCrossDoc } from '../middleware/new-id-cross-doc.js';
import { reorderList } from '../middleware/reorder-list';
import {
@@ -1434,7 +1433,6 @@ export class DragEventWatcher {
newIdCrossDoc(std),
reorderList(std),
surfaceRefToEmbed(std),
cardStyleUpdater(std),
];
if (selectedIds) {
@@ -374,8 +374,6 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
type: 'resize' | 'rotate';
angle: number;
handle: ResizeHandle;
flipX?: boolean;
flipY?: boolean;
pure?: boolean;
}) => {
if (!options) {
@@ -383,25 +381,8 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
return 'default';
}
const { type, angle, flipX, flipY } = options;
const { type, angle, handle } = options;
let cursor: CursorType = 'default';
let handle: ResizeHandle = options.handle;
if (flipX) {
handle = (
handle.includes('left')
? handle.replace('left', 'right')
: handle.replace('right', 'left')
) as ResizeHandle;
}
if (flipY) {
handle = (
handle.includes('top')
? handle.replace('top', 'bottom')
: handle.replace('bottom', 'top')
) as ResizeHandle;
}
if (type === 'rotate') {
cursor = generateCursorUrl(angle, handle);
@@ -645,7 +626,7 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
onResizeStart: () => {
this._mode = 'resize';
},
onResizeUpdate: ({ lockRatio, scaleX, scaleY, exceed }) => {
onResizeUpdate: ({ lockRatio, scaleX, exceed }) => {
if (lockRatio) {
this._scaleDirection = handle;
this._scalePercent = `${Math.round(scaleX * 100)}%`;
@@ -661,8 +642,6 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
type: 'resize',
angle: elements.length > 1 ? 0 : (elements[0]?.rotate ?? 0),
handle,
flipX: scaleX < 0,
flipY: scaleY < 0,
});
},
onResizeEnd: () => {
@@ -673,14 +652,6 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
}
},
option => {
if (
['resize', 'rotate'].includes(
interaction.activeInteraction$.value?.type ?? ''
)
) {
return '';
}
return this._updateCursor({
...option,
angle: elements.length > 1 ? 0 : (elements[0]?.rotate ?? 0),
@@ -1,21 +1,43 @@
import { type FrameBlockModel } from '@blocksuite/affine-model';
import { FrameBlockModel, type RootBlockModel } from '@blocksuite/affine-model';
import { WidgetComponent, WidgetViewExtension } from '@blocksuite/std';
import { html } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
import { literal, unsafeStatic } from 'lit/static-html.js';
import type { AffineFrameTitle } from './frame-title.js';
export const AFFINE_FRAME_TITLE_WIDGET = 'affine-frame-title-widget';
export class AffineFrameTitleWidget extends WidgetComponent<FrameBlockModel> {
export class AffineFrameTitleWidget extends WidgetComponent<RootBlockModel> {
private get _frames() {
return Object.values(this.store.blocks.value)
.map(({ model }) => model)
.filter(model => model instanceof FrameBlockModel);
}
getFrameTitle(frame: FrameBlockModel | string) {
const id = typeof frame === 'string' ? frame : frame.id;
const frameTitle = this.shadowRoot?.querySelector(
`affine-frame-title[data-id="${id}"]`
) as AffineFrameTitle | null;
return frameTitle;
}
override render() {
return html`<affine-frame-title
.model=${this.model}
data-id=${this.model.id}
></affine-frame-title>`;
return repeat(
this._frames,
({ id }) => id,
frame =>
html`<affine-frame-title
.model=${frame}
data-id=${frame.id}
></affine-frame-title>`
);
}
}
export const frameTitleWidget = WidgetViewExtension(
'affine:frame',
'affine:page',
AFFINE_FRAME_TITLE_WIDGET,
literal`${unsafeStatic(AFFINE_FRAME_TITLE_WIDGET)}`
);
@@ -14,7 +14,6 @@ import {
AFFINE_FRAME_TITLE_WIDGET,
type AffineFrameTitleWidget,
} from './affine-frame-title-widget';
import type { AffineFrameTitle } from './frame-title';
import { frameTitleStyleVars } from './styles';
export class EdgelessFrameTitleEditor extends WithDisposable(
@@ -136,13 +135,12 @@ export class EdgelessFrameTitleEditor extends WithDisposable(
const frameTitleWidget = this.edgeless.std.view.getWidget(
AFFINE_FRAME_TITLE_WIDGET,
this.frameModel.id
rootBlockId
) as AffineFrameTitleWidget | null;
if (!frameTitleWidget) return nothing;
const frameTitle =
frameTitleWidget.querySelector<AffineFrameTitle>('affine-frame-title');
const frameTitle = frameTitleWidget.getFrameTitle(this.frameModel);
const colors = frameTitle?.colors ?? {
background: cssVarV2('edgeless/frame/background/white'),
@@ -142,10 +142,12 @@ export class AffineFrameTitle extends SignalWatcher(
}px)`,
];
const anchor = this.gfx.viewport.toViewCoord(bound.x, bound.y);
this.style.display = '';
this.style.setProperty('--bg-color', this.colors.background);
this.style.left = '0px';
this.style.top = '0px';
this.style.left = `${anchor[0]}px`;
this.style.top = `${anchor[1]}px`;
this.style.display = hidden ? 'none' : 'flex';
this.style.transform = transformOperation.join(' ');
this.style.maxWidth = `${maxWidth}px`;
@@ -203,6 +205,18 @@ export class AffineFrameTitle extends SignalWatcher(
})
);
_disposables.add(
on(this, 'click', evt => {
if (evt.shiftKey) {
this.gfx.selection.toggle(this.model);
} else {
this.gfx.selection.set({
elements: [this.model.id],
});
}
})
);
_disposables.add(
on(this, 'dblclick', () => {
const edgeless = this.std.view.getBlock(this.std.store.root?.id || '');
@@ -566,29 +566,23 @@ Optional flag to insert before sibling
### updateBlock()
> **updateBlock**\<`T`\>(`modelOrId`, `callBackOrProps`): `void`
> **updateBlock**(`modelOrId`, `callBackOrProps`): `void`
Updates a block's properties or executes a callback in a transaction
#### Type Parameters
##### T
`T` *extends* `BlockModel`\<`object`\> = `BlockModel`\<`object`\>
#### Parameters
##### modelOrId
The block model or block ID to update
`string` | `T`
`string` | `BlockModel`\<`object`\>
##### callBackOrProps
Either a callback function to execute or properties to update
() => `void` | `Partial`\<`BlockProps` \| `PropsOfModel`\<`T`\> & `BlockSysProps`\>
`Partial`\<`BlockProps`\> | () => `void`
#### Returns
@@ -0,0 +1,73 @@
import { describe, expect, test } from 'vitest';
import { SVGPathBuilder, SVGShapeBuilder } from '../gfx/svg-path.js';
describe('SVGPathBuilder', () => {
test('should build a simple path', () => {
const pathBuilder = new SVGPathBuilder();
const result = pathBuilder.moveTo(10, 20).lineTo(30, 40).build();
expect(result).toBe('M 10 20 L 30 40');
});
test('should build a path with curves', () => {
const pathBuilder = new SVGPathBuilder();
const result = pathBuilder
.moveTo(0, 0)
.curveTo(10, 0, 10, 10, 20, 10)
.build();
expect(result).toBe('M 0 0 C 10 0 10 10 20 10');
});
test('should build a closed path', () => {
const pathBuilder = new SVGPathBuilder();
const result = pathBuilder
.moveTo(0, 0)
.lineTo(10, 0)
.lineTo(5, 10)
.closePath()
.build();
expect(result).toBe('M 0 0 L 10 0 L 5 10 Z');
});
test('should clear commands', () => {
const pathBuilder = new SVGPathBuilder();
pathBuilder.moveTo(10, 20).lineTo(30, 40);
pathBuilder.clear();
const result = pathBuilder.moveTo(0, 0).build();
expect(result).toBe('M 0 0');
});
});
describe('SVGShapeBuilder', () => {
test('should generate diamond polygon points', () => {
const result = SVGShapeBuilder.diamond(100, 80, 2);
expect(result).toBe('50,1 99,40 50,79 1,40');
});
test('should generate triangle polygon points', () => {
const result = SVGShapeBuilder.triangle(100, 80, 2);
expect(result).toBe('50,1 99,79 1,79');
});
test('should generate diamond path', () => {
const result = SVGShapeBuilder.diamondPath(100, 80, 2);
expect(result).toBe('M 50 1 L 99 40 L 50 79 L 1 40 Z');
});
test('should generate triangle path', () => {
const result = SVGShapeBuilder.trianglePath(100, 80, 2);
expect(result).toBe('M 50 1 L 99 79 L 1 79 Z');
});
test('should handle zero stroke width', () => {
const diamondResult = SVGShapeBuilder.diamond(100, 80, 0);
expect(diamondResult).toBe('50,0 100,40 50,80 0,40');
const triangleResult = SVGShapeBuilder.triangle(100, 80, 0);
expect(triangleResult).toBe('50,0 100,80 0,80');
});
});
@@ -4,4 +4,5 @@ export * from './math.js';
export * from './model/index.js';
export * from './perfect-freehand/index.js';
export * from './polyline.js';
export * from './svg-path.js';
export * from './xywh.js';
@@ -0,0 +1,160 @@
interface PathCommand {
command: string;
coordinates: number[];
}
/**
* A utility class for building SVG path strings using command-based API.
* Supports moveTo, lineTo, curveTo operations and can build complete path strings.
*/
export class SVGPathBuilder {
private commands: PathCommand[] = [];
/**
* Move to a specific point without drawing
*/
moveTo(x: number, y: number): this {
this.commands.push({
command: 'M',
coordinates: [x, y],
});
return this;
}
/**
* Draw a line to a specific point
*/
lineTo(x: number, y: number): this {
this.commands.push({
command: 'L',
coordinates: [x, y],
});
return this;
}
/**
* Draw a cubic Bézier curve
*/
curveTo(
cp1x: number,
cp1y: number,
cp2x: number,
cp2y: number,
x: number,
y: number
): this {
this.commands.push({
command: 'C',
coordinates: [cp1x, cp1y, cp2x, cp2y, x, y],
});
return this;
}
/**
* Close the current path
*/
closePath(): this {
this.commands.push({
command: 'Z',
coordinates: [],
});
return this;
}
/**
* Build the complete SVG path string
*/
build(): string {
const pathSegments = this.commands.map(cmd => {
const coords = cmd.coordinates.join(' ');
return coords ? `${cmd.command} ${coords}` : cmd.command;
});
return pathSegments.join(' ');
}
/**
* Clear all commands and reset the builder
*/
clear(): this {
this.commands = [];
return this;
}
}
/**
* Create SVG polygon points string for common shapes
*/
export class SVGShapeBuilder {
/**
* Generate diamond (rhombus) polygon points
*/
static diamond(
width: number,
height: number,
strokeWidth: number = 0
): string {
const halfStroke = strokeWidth / 2;
return [
`${width / 2},${halfStroke}`,
`${width - halfStroke},${height / 2}`,
`${width / 2},${height - halfStroke}`,
`${halfStroke},${height / 2}`,
].join(' ');
}
/**
* Generate triangle polygon points
*/
static triangle(
width: number,
height: number,
strokeWidth: number = 0
): string {
const halfStroke = strokeWidth / 2;
return [
`${width / 2},${halfStroke}`,
`${width - halfStroke},${height - halfStroke}`,
`${halfStroke},${height - halfStroke}`,
].join(' ');
}
/**
* Generate diamond path using SVGPathBuilder
*/
static diamondPath(
width: number,
height: number,
strokeWidth: number = 0
): string {
const halfStroke = strokeWidth / 2;
const pathBuilder = new SVGPathBuilder();
return pathBuilder
.moveTo(width / 2, halfStroke)
.lineTo(width - halfStroke, height / 2)
.lineTo(width / 2, height - halfStroke)
.lineTo(halfStroke, height / 2)
.closePath()
.build();
}
/**
* Generate triangle path using SVGPathBuilder
*/
static trianglePath(
width: number,
height: number,
strokeWidth: number = 0
): string {
const halfStroke = strokeWidth / 2;
const pathBuilder = new SVGPathBuilder();
return pathBuilder
.moveTo(width / 2, halfStroke)
.lineTo(width - halfStroke, height - halfStroke)
.lineTo(halfStroke, height - halfStroke)
.closePath()
.build();
}
}
@@ -947,34 +947,23 @@ export class InteractivityManager extends GfxExtension {
...options,
lockRatio,
elements,
onResizeMove: ({
scaleX,
scaleY,
originalBound,
handleSign,
handlePos,
currentHandlePos,
lockRatio,
}) => {
onResizeMove: ({ dx, dy, handleSign, lockRatio }) => {
const suggested: {
scaleX: number;
scaleY: number;
dx: number;
dy: number;
priority?: number;
}[] = [];
const suggest = (distance: { scaleX: number; scaleY: number }) => {
const suggest = (distance: { dx: number; dy: number }) => {
suggested.push(distance);
};
extensionHandlers.forEach(ext => {
ext.onResizeMove?.({
scaleX,
scaleY,
dx,
dy,
elements,
handle,
handleSign,
handlePos,
originalBound,
currentHandlePos,
handle,
lockRatio,
suggest,
});
@@ -984,9 +973,9 @@ export class InteractivityManager extends GfxExtension {
return (a.priority ?? 0) - (b.priority ?? 0);
});
return last(suggested) ?? { scaleX, scaleY };
return last(suggested) ?? { dx, dy };
},
onResizeStart: ({ handleSign, handlePos, data }) => {
onResizeStart: ({ data }) => {
this.activeInteraction$.value = {
type: 'resize',
elements,
@@ -995,8 +984,6 @@ export class InteractivityManager extends GfxExtension {
ext.onResizeStart?.({
elements,
handle,
handlePos,
handleSign,
});
});
@@ -1058,15 +1045,13 @@ export class InteractivityManager extends GfxExtension {
options.onResizeUpdate?.({ scaleX, scaleY, lockRatio, exceed });
},
onResizeEnd: ({ handleSign, handlePos, data }) => {
onResizeEnd: ({ data }) => {
this.activeInteraction$.value = null;
extensionHandlers.forEach(ext => {
ext.onResizeEnd?.({
elements,
handle,
handlePos,
handleSign,
});
});
options.onResizeEnd?.();
@@ -2,7 +2,6 @@ import {
Bound,
getCommonBoundWithRotation,
type IBound,
type IPoint,
type IVec,
} from '@blocksuite/global/gfx';
@@ -30,7 +29,7 @@ export const DEFAULT_HANDLES: ResizeHandle[] = [
'bottom',
];
type ReadonlyIBound = Readonly<Required<IBound>>;
type ElementInitialSnapshot = Readonly<Required<IBound>>;
export interface OptionResize {
elements: GfxModel[];
@@ -38,18 +37,16 @@ export interface OptionResize {
lockRatio: boolean;
event: PointerEvent;
onResizeMove: (payload: {
scaleX: number;
scaleY: number;
dx: number;
dy: number;
originalBound: IBound;
handleSign: IPoint;
handlePos: IVec;
currentHandlePos: IVec;
handleSign: {
xSign: number;
ySign: number;
};
lockRatio: boolean;
}) => { scaleX: number; scaleY: number };
}) => { dx: number; dy: number };
onResizeUpdate: (payload: {
lockRatio: boolean;
scaleX: number;
@@ -62,16 +59,8 @@ export interface OptionResize {
matrix: DOMMatrix;
}[];
}) => void;
onResizeStart?: (payload: {
handlePos: IVec;
handleSign: IPoint;
data: { model: GfxModel }[];
}) => void;
onResizeEnd?: (payload: {
handlePos: IVec;
handleSign: IPoint;
data: { model: GfxModel }[];
}) => void;
onResizeStart?: (payload: { data: { model: GfxModel }[] }) => void;
onResizeEnd?: (payload: { data: { model: GfxModel }[] }) => void;
}
export type RotateOption = {
@@ -106,102 +95,11 @@ export class ResizeController {
this.gfx = option.gfx;
}
getCoordsTransform(originalBound: IBound, handle: ResizeHandle) {
const { x: xSign, y: ySign } = this.getHandleSign(handle);
const pivot = new DOMPoint(
originalBound.x + ((-xSign + 1) / 2) * originalBound.w,
originalBound.y + ((-ySign + 1) / 2) * originalBound.h
);
const toLocalM = new DOMMatrix().translate(-pivot.x, -pivot.y);
const toLocalRotatedM = new DOMMatrix()
.translate(-pivot.x, -pivot.y)
.translate(
originalBound.w / 2 + originalBound.x,
originalBound.h / 2 + originalBound.y
)
.rotate(-(originalBound.rotate ?? 0))
.translate(
-(originalBound.w / 2 + originalBound.x),
-(originalBound.h / 2 + originalBound.y)
);
const toLocal = (p: DOMPoint, withRotation: boolean = false) =>
p.matrixTransform(withRotation ? toLocalRotatedM : toLocalM);
const toModel = (p: DOMPoint) =>
p.matrixTransform(toLocalRotatedM.inverse());
const handlePos = toModel(
new DOMPoint(originalBound.w * xSign, originalBound.h * ySign)
);
return {
xSign,
ySign,
originalBound,
toLocalM,
toLocalRotatedM,
toLocal,
toModel,
handlePos: [handlePos.x, handlePos.y] as IVec,
};
}
getScaleFromDelta(
transform: ReturnType<ResizeController['getCoordsTransform']>,
delta: { dx: number; dy: number },
handleStartPos: IVec,
lockRatio: boolean
) {
const { originalBound, xSign, ySign, toModel, toLocal } = transform;
const currentPos = toLocal(
new DOMPoint(handleStartPos[0] + delta.dx, handleStartPos[1] + delta.dy),
true
);
let scaleX = xSign ? currentPos.x / (originalBound.w * xSign) : 1;
let scaleY = ySign ? currentPos.y / (originalBound.h * ySign) : 1;
if (lockRatio) {
const min = Math.min(Math.abs(scaleX), Math.abs(scaleY));
scaleX = Math.sign(scaleX) * min;
scaleY = Math.sign(scaleY) * min;
}
const finalHandlePos = toModel(
new DOMPoint(
originalBound.w * xSign * scaleX,
originalBound.h * ySign * scaleY
)
);
return {
scaleX,
scaleY,
handlePos: [finalHandlePos.x, finalHandlePos.y] as IVec,
};
}
getScaleMatrix(
{ scaleX, scaleY }: { scaleX: number; scaleY: number },
lockRatio: boolean
) {
if (lockRatio) {
const min = Math.min(Math.abs(scaleX), Math.abs(scaleY));
scaleX = Math.sign(scaleX) * min;
scaleY = Math.sign(scaleY) * min;
}
return {
scaleX,
scaleY,
scaleM: new DOMMatrix().scaleSelf(scaleX, scaleY),
};
}
startResize(options: OptionResize) {
const {
elements,
handle,
lockRatio,
onResizeStart,
onResizeMove,
onResizeUpdate,
@@ -209,32 +107,19 @@ export class ResizeController {
event,
} = options;
const originals: ReadonlyIBound[] = elements.map(el => ({
const originals: ElementInitialSnapshot[] = elements.map(el => ({
x: el.x,
y: el.y,
w: el.w,
h: el.h,
rotate: el.rotate,
}));
const originalBound: IBound =
originals.length > 1
? getCommonBoundWithRotation(originals)
: {
x: originals[0].x,
y: originals[0].y,
w: originals[0].w,
h: originals[0].h,
rotate: originals[0].rotate,
};
const originalBound = getCommonBoundWithRotation(originals);
const startPt = this.gfx.viewport.toModelCoordFromClientCoord([
event.clientX,
event.clientY,
]);
const transform = this.getCoordsTransform(originalBound, handle);
const handleSign = {
x: transform.xSign,
y: transform.ySign,
};
const handleSign = this.getHandleSign(handle);
const onPointerMove = (e: PointerEvent) => {
const currPt = this.gfx.viewport.toModelCoordFromClientCoord([
@@ -245,69 +130,45 @@ export class ResizeController {
dx: currPt[0] - startPt[0],
dy: currPt[1] - startPt[1],
};
const shouldLockRatio =
options.lockRatio || e.shiftKey || elements.length > 1;
const {
scaleX,
scaleY,
handlePos: currentHandlePos,
} = this.getScaleFromDelta(
transform,
delta,
transform.handlePos,
shouldLockRatio
);
const scale = onResizeMove({
scaleX,
scaleY,
originalBound,
const shouldLockRatio = lockRatio || e.shiftKey;
delta = onResizeMove({
dx: delta.dx,
dy: delta.dy,
handleSign,
handlePos: transform.handlePos,
currentHandlePos,
lockRatio: shouldLockRatio,
});
const scaleInfo = this.getScaleMatrix(scale, shouldLockRatio);
if (elements.length === 1) {
this.resizeSingle(
originals[0],
elements[0],
shouldLockRatio,
transform,
scaleInfo,
startPt,
delta,
handleSign,
onResizeUpdate
);
} else {
this.resizeMulti(
originalBound,
originals,
elements,
transform,
scaleInfo,
startPt,
delta,
handleSign,
onResizeUpdate
);
}
};
onResizeStart?.({
handleSign,
handlePos: transform.handlePos,
data: elements.map(model => ({ model })),
});
onResizeStart?.({ data: elements.map(model => ({ model })) });
const onPointerUp = () => {
this.host.removeEventListener('pointermove', onPointerMove);
this.host.removeEventListener('pointerup', onPointerUp);
onResizeEnd?.({
handleSign,
handlePos: transform.handlePos,
data: elements.map(model => ({ model })),
});
onResizeEnd?.({ data: elements.map(model => ({ model })) });
};
this.host.addEventListener('pointermove', onPointerMove);
@@ -315,15 +176,55 @@ export class ResizeController {
}
private resizeSingle(
orig: ReadonlyIBound,
orig: ElementInitialSnapshot,
model: GfxModel,
lockRatio: boolean,
transform: ReturnType<typeof ResizeController.prototype.getCoordsTransform>,
scale: { scaleX: number; scaleY: number; scaleM: DOMMatrix },
startPt: IVec,
delta: {
dx: number;
dy: number;
},
handleSign: { xSign: number; ySign: number },
updateCallback: OptionResize['onResizeUpdate']
) {
const { toLocalM, toLocalRotatedM, toLocal, toModel } = transform;
const { scaleX, scaleY, scaleM } = scale;
const { xSign, ySign } = handleSign;
const pivot = new DOMPoint(
orig.x + (-xSign === 1 ? orig.w : 0),
orig.y + (-ySign === 1 ? orig.h : 0)
);
const toLocalRotatedM = new DOMMatrix()
.translate(-pivot.x, -pivot.y)
.translate(orig.w / 2 + orig.x, orig.h / 2 + orig.y)
.rotate(-orig.rotate)
.translate(-(orig.w / 2 + orig.x), -(orig.h / 2 + orig.y));
const toLocalM = new DOMMatrix().translate(-pivot.x, -pivot.y);
const toLocal = (p: DOMPoint, withRotation: boolean) =>
p.matrixTransform(withRotation ? toLocalRotatedM : toLocalM);
const toModel = (p: DOMPoint) =>
p.matrixTransform(toLocalRotatedM.inverse());
const handleLocal = toLocal(new DOMPoint(startPt[0], startPt[1]), true);
const currPtLocal = toLocal(
new DOMPoint(startPt[0] + delta.dx, startPt[1] + delta.dy),
true
);
let scaleX = xSign
? (xSign * (currPtLocal.x - handleLocal.x) + orig.w) / orig.w
: 1;
let scaleY = ySign
? (ySign * (currPtLocal.y - handleLocal.y) + orig.h) / orig.h
: 1;
if (lockRatio) {
const min = Math.min(Math.abs(scaleX), Math.abs(scaleY));
scaleX = Math.sign(scaleX) * min;
scaleY = Math.sign(scaleY) * min;
}
const scaleM = new DOMMatrix().scale(scaleX, scaleY);
const [visualTopLeft, visualBottomRight] = [
new DOMPoint(orig.x, orig.y),
@@ -381,14 +282,45 @@ export class ResizeController {
}
private resizeMulti(
originals: ReadonlyIBound[],
originalBound: Bound,
originals: ElementInitialSnapshot[],
elements: GfxModel[],
transform: ReturnType<ResizeController['getCoordsTransform']>,
scale: { scaleX: number; scaleY: number; scaleM: DOMMatrix },
startPt: IVec,
delta: {
dx: number;
dy: number;
},
handleSign: { xSign: number; ySign: number },
updateCallback: OptionResize['onResizeUpdate']
) {
const { toLocalM } = transform;
const { scaleX, scaleY, scaleM } = scale;
const { xSign, ySign } = handleSign;
const pivot = new DOMPoint(
originalBound.x + ((-xSign + 1) / 2) * originalBound.w,
originalBound.y + ((-ySign + 1) / 2) * originalBound.h
);
const toLocalM = new DOMMatrix().translate(-pivot.x, -pivot.y);
const toLocal = (p: DOMPoint) => p.matrixTransform(toLocalM);
const handleLocal = toLocal(new DOMPoint(startPt[0], startPt[1]));
const currPtLocal = toLocal(
new DOMPoint(startPt[0] + delta.dx, startPt[1] + delta.dy)
);
let scaleX = xSign
? (xSign * (currPtLocal.x - handleLocal.x) + originalBound.w) /
originalBound.w
: 1;
let scaleY = ySign
? (ySign * (currPtLocal.y - handleLocal.y) + originalBound.h) /
originalBound.h
: 1;
const min = Math.max(Math.abs(scaleX), Math.abs(scaleY));
scaleX = Math.sign(scaleX) * min;
scaleY = Math.sign(scaleY) * min;
const scaleM = new DOMMatrix().scale(scaleX, scaleY);
const data = elements.map((model, i) => {
const orig = originals[i];
@@ -425,7 +357,7 @@ export class ResizeController {
startRotate(option: RotateOption) {
const { event, elements, onRotateUpdate } = option;
const originals: ReadonlyIBound[] = elements.map(el => ({
const originals: ElementInitialSnapshot[] = elements.map(el => ({
x: el.x,
y: el.y,
w: el.w,
@@ -497,7 +429,7 @@ export class ResizeController {
}
private rotateSingle(option: {
orig: ReadonlyIBound;
orig: ElementInitialSnapshot;
model: GfxModel;
startPt: IVec;
currentPt: IVec;
@@ -549,7 +481,7 @@ export class ResizeController {
}
private rotateMulti(option: {
origs: ReadonlyIBound[];
origs: ElementInitialSnapshot[];
models: GfxModel[];
startPt: IVec;
currentPt: IVec;
@@ -635,23 +567,23 @@ export class ResizeController {
private getHandleSign(handle: ResizeHandle) {
switch (handle) {
case 'top-left':
return { x: -1, y: -1 };
return { xSign: -1, ySign: -1 };
case 'top':
return { x: 0, y: -1 };
return { xSign: 0, ySign: -1 };
case 'top-right':
return { x: 1, y: -1 };
return { xSign: 1, ySign: -1 };
case 'right':
return { x: 1, y: 0 };
return { xSign: 1, ySign: 0 };
case 'bottom-right':
return { x: 1, y: 1 };
return { xSign: 1, ySign: 1 };
case 'bottom':
return { x: 0, y: 1 };
return { xSign: 0, ySign: 1 };
case 'bottom-left':
return { x: -1, y: 1 };
return { xSign: -1, ySign: 1 };
case 'left':
return { x: -1, y: 0 };
return { xSign: -1, ySign: 0 };
default:
return { x: 0, y: 0 };
return { xSign: 0, ySign: 0 };
}
}
}
@@ -1,5 +1,3 @@
import type { IBound, IPoint, IVec } from '@blocksuite/global/gfx';
import type { GfxModel } from '../../model/model';
import type { ResizeHandle } from '../resize/manager';
@@ -10,16 +8,6 @@ export type ExtensionElementResizeContext = {
export type ExtensionElementResizeStartContext = {
elements: GfxModel[];
/**
* The position of the handle in the browser coordinate space.
*/
handlePos: IVec;
/**
* The sign (or normal vector) of the handle.
*/
handleSign: IPoint;
handle: ResizeHandle;
};
@@ -28,14 +16,15 @@ export type ExtensionElementResizeEndContext =
export type ExtensionElementResizeMoveContext =
ExtensionElementResizeStartContext & {
scaleX: number;
scaleY: number;
originalBound: IBound;
currentHandlePos: IVec;
dx: number;
dy: number;
lockRatio: boolean;
suggest: (distance: { scaleX: number; scaleY: number }) => void;
handleSign: {
xSign: number;
ySign: number;
};
suggest: (distance: { dx: number; dy: number }) => void;
};
+7 -16
View File
@@ -825,7 +825,7 @@ export class LayerManager extends GfxExtension {
const block = store.getModelById(payload.id);
if (block instanceof GfxBlockElementModel) {
this.delete(block);
this.delete(block as GfxBlockElementModel);
}
}
})
@@ -834,29 +834,20 @@ export class LayerManager extends GfxExtension {
const watchSurface = (surface: SurfaceBlockModel) => {
let lastChildMap = new Map(surface.childMap.peek());
this._disposable.add(
surface.childMap.subscribe(currentChildMap => {
currentChildMap.forEach((_, id) => {
surface.childMap.subscribe(val => {
val.forEach((_, id) => {
if (lastChildMap.has(id)) {
lastChildMap.delete(id);
return;
}
});
lastChildMap.forEach((_, id) => {
const model = this._doc.getModelById(id);
if (model instanceof GfxBlockElementModel) {
this.delete(model);
const block = this._doc.getBlock(id);
if (block?.model) {
this.delete(block.model as GfxBlockElementModel);
}
});
currentChildMap.forEach((_, id) => {
const model = store.getModelById(id);
if (
model instanceof GfxBlockElementModel &&
!this.blocks.includes(model)
) {
this.add(model);
}
});
lastChildMap = new Map(currentChildMap);
lastChildMap = new Map(val);
})
);
@@ -19,5 +19,3 @@ export type BlockSysProps = {
children?: BlockModel[];
};
export type BlockProps = BlockSysProps & Record<string, unknown>;
export type PropsOfModel<T> = T extends BlockModel<infer P> ? P : never;
@@ -22,8 +22,6 @@ import {
type BlockModel,
type BlockOptions,
type BlockProps,
type BlockSysProps,
type PropsOfModel,
type YBlock,
} from '../block/index.js';
import { DocCRUD } from './crud.js';
@@ -854,12 +852,9 @@ export class Store {
*
* @category Block CRUD
*/
updateBlock<T extends BlockModel = BlockModel>(
modelOrId: T | string,
callBackOrProps:
| (() => void)
| Partial<(PropsOfModel<T> & BlockSysProps) | BlockProps>
updateBlock(
modelOrId: BlockModel | string,
callBackOrProps: (() => void) | Partial<BlockProps>
) {
if (this.readonly) {
console.error('cannot modify data in readonly mode');
@@ -113,8 +113,6 @@ type TransformerMiddlewareOptions = {
transformerConfigs: Map<string, unknown>;
};
type TransformerMiddlewareCleanup = () => void;
export type TransformerMiddleware = (
options: TransformerMiddlewareOptions
) => void | TransformerMiddlewareCleanup;
) => void;
@@ -1,4 +1,3 @@
import { DisposableGroup } from '@blocksuite/global/disposable';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { nextTick } from '@blocksuite/global/utils';
import { Subject } from 'rxjs';
@@ -68,8 +67,6 @@ export class Transformer {
private readonly _docCRUD: DocCRUD;
private readonly _disposables: DisposableGroup = new DisposableGroup();
private readonly _slots: TransformerSlots = {
beforeImport: new Subject<BeforeImportPayload>(),
afterImport: new Subject<AfterImportPayload>(),
@@ -369,16 +366,13 @@ export class Transformer {
this._docCRUD = docCRUD;
middlewares.forEach(middleware => {
const cleanup = middleware({
middleware({
slots: this._slots,
docCRUD: this._docCRUD,
assetsManager: this._assetsManager,
adapterConfigs: this._adapterConfigs,
transformerConfigs: this._transformerConfigs,
});
if (cleanup) {
this._disposables.add(cleanup);
}
});
}
@@ -652,9 +646,4 @@ export class Transformer {
reset() {
this._assetsManager.cleanup();
}
[Symbol.dispose]() {
this._disposables.dispose();
this._assetsManager.cleanup();
}
}
+12 -16
View File
@@ -132,13 +132,12 @@ export class DocEngine {
this.logger
);
const subscriber = state.mainPeer.onStatusChange.subscribe(() => {
if (!signal.aborted)
this.updateSyncingState(state.mainPeer, state.shadowPeers);
});
cleanUp.push(() => {
subscriber.unsubscribe();
});
cleanUp.push(
state.mainPeer.onStatusChange.subscribe(() => {
if (!signal.aborted)
this.updateSyncingState(state.mainPeer, state.shadowPeers);
}).unsubscribe
);
this.updateSyncingState(state.mainPeer, state.shadowPeers);
@@ -153,15 +152,12 @@ export class DocEngine {
this.priorityTarget,
this.logger
);
const subscriber = peer.onStatusChange.subscribe(() => {
if (!signal.aborted)
this.updateSyncingState(state.mainPeer, state.shadowPeers);
});
cleanUp.push(() => {
subscriber.unsubscribe();
});
cleanUp.push(
peer.onStatusChange.subscribe(() => {
if (!signal.aborted)
this.updateSyncingState(state.mainPeer, state.shadowPeers);
}).unsubscribe
);
return peer;
});
@@ -0,0 +1,268 @@
import { DomRenderer } from '@blocksuite/affine-block-surface';
import { beforeEach, describe, expect, test } from 'vitest';
import { wait } from '../utils/common.js';
import { getSurface } from '../utils/edgeless.js';
import { setupEditor } from '../utils/setup.js';
/**
* Tests for brush element rendering with DOM renderer.
* These tests verify that brush elements are correctly rendered as DOM nodes
* when the DOM renderer is enabled, similar to connector element tests.
*/
describe('Brush rendering with DOM renderer', () => {
beforeEach(async () => {
const cleanup = await setupEditor('edgeless', [], {
enableDomRenderer: true,
});
return cleanup;
});
test('should use DomRenderer when enable_dom_renderer flag is true', async () => {
const surface = getSurface(doc, editor);
expect(surface).not.toBeNull();
expect(surface?.renderer).toBeInstanceOf(DomRenderer);
});
test('should render a brush element as a DOM node', async () => {
const surfaceView = getSurface(window.doc, window.editor);
const surfaceModel = surfaceView.model;
// Create a brush element with points (commands will be auto-generated)
const brushProps = {
type: 'brush',
points: [
[10, 10],
[50, 50],
[100, 20],
],
color: '#000000',
lineWidth: 2,
};
const brushId = surfaceModel.addElement(brushProps);
await wait(100);
const brushElement = surfaceView?.renderRoot.querySelector(
`[data-element-id="${brushId}"]`
);
expect(brushElement).not.toBeNull();
expect(brushElement).toBeInstanceOf(HTMLElement);
// Check if SVG element is present for brush rendering
const svgElement = brushElement?.querySelector('svg');
expect(svgElement).not.toBeNull();
// Check if path element is present
const pathElement = svgElement?.querySelector('path');
expect(pathElement).not.toBeNull();
// Commands are auto-generated from points, so just check it exists
expect(pathElement?.getAttribute('d')).toBeTruthy();
});
test('should render brush with different colors', async () => {
const surfaceView = getSurface(window.doc, window.editor);
const surfaceModel = surfaceView.model;
// Create a red brush element
const brushProps = {
type: 'brush',
points: [
[20, 20],
[35, 15],
[50, 25],
[65, 45],
[80, 80],
],
color: '#ff0000',
lineWidth: 3,
};
const brushId = surfaceModel.addElement(brushProps);
await wait(100);
const brushElement = surfaceView?.renderRoot.querySelector(
`[data-element-id="${brushId}"]`
);
expect(brushElement).not.toBeNull();
const svgElement = brushElement?.querySelector('svg');
expect(svgElement).not.toBeNull();
const pathElement = svgElement?.querySelector('path');
expect(pathElement).not.toBeNull();
// Check if color is applied (the actual color value might be processed)
const fillColor = pathElement?.getAttribute('fill');
expect(fillColor).toBeTruthy();
});
test('should render brush with opacity', async () => {
const surfaceView = getSurface(window.doc, window.editor);
const surfaceModel = surfaceView.model;
const brushProps = {
type: 'brush',
points: [
[10, 10],
[50, 50],
[90, 90],
],
color: '#0000ff',
lineWidth: 2,
};
const brushId = surfaceModel.addElement(brushProps);
// Set opacity after creation through model update
const brushModel = surfaceModel.getElementById(brushId);
if (brushModel) {
surfaceModel.updateElement(brushId, { opacity: 0.5 });
}
await wait(100);
const brushElement = surfaceView?.renderRoot.querySelector(
`[data-element-id="${brushId}"]`
);
expect(brushElement).not.toBeNull();
// Check opacity style
const opacity = (brushElement as HTMLElement)?.style.opacity;
expect(opacity).toBe('0.5');
});
test('should render brush with rotation', async () => {
const surfaceView = getSurface(window.doc, window.editor);
const surfaceModel = surfaceView.model;
const brushProps = {
type: 'brush',
points: [
[25, 25],
[50, 50],
[75, 75],
],
color: '#00ff00',
lineWidth: 2,
rotate: 45,
};
const brushId = surfaceModel.addElement(brushProps);
await wait(100);
const brushElement = surfaceView?.renderRoot.querySelector(
`[data-element-id="${brushId}"]`
);
expect(brushElement).not.toBeNull();
// Check rotation transform
const transform = (brushElement as HTMLElement)?.style.transform;
expect(transform).toContain('rotate(45deg)');
const transformOrigin = (brushElement as HTMLElement)?.style
.transformOrigin;
expect(transformOrigin).toBe('center center');
});
test('should have proper SVG viewport and sizing', async () => {
const surfaceView = getSurface(window.doc, window.editor);
const surfaceModel = surfaceView.model;
const brushProps = {
type: 'brush',
points: [
[0, 0],
[60, 40],
[120, 80],
],
color: '#333333',
lineWidth: 2,
};
const brushId = surfaceModel.addElement(brushProps);
await wait(100);
const brushElement = surfaceView?.renderRoot.querySelector(
`[data-element-id="${brushId}"]`
);
expect(brushElement).not.toBeNull();
const svgElement = brushElement?.querySelector('svg');
expect(svgElement).not.toBeNull();
// Check SVG attributes
expect(svgElement?.getAttribute('width')).toBe('100%');
expect(svgElement?.getAttribute('height')).toBe('100%');
expect(svgElement?.getAttribute('viewBox')).toBeTruthy();
expect(svgElement?.getAttribute('preserveAspectRatio')).toBe('none');
});
test('should add brush-specific CSS class', async () => {
const surfaceView = getSurface(window.doc, window.editor);
const surfaceModel = surfaceView.model;
const brushProps = {
type: 'brush',
points: [
[10, 10],
[25, 25],
[40, 40],
],
color: '#666666',
lineWidth: 2,
};
const brushId = surfaceModel.addElement(brushProps);
await wait(100);
const brushElement = surfaceView?.renderRoot.querySelector(
`[data-element-id="${brushId}"]`
);
expect(brushElement).not.toBeNull();
expect(brushElement?.classList.contains('brush-element')).toBe(true);
});
test('should remove brush DOM node when element is deleted', async () => {
const surfaceView = getSurface(window.doc, window.editor);
const surfaceModel = surfaceView.model;
expect(surfaceView.renderer).toBeInstanceOf(DomRenderer);
const brushProps = {
type: 'brush',
points: [
[25, 25],
[75, 25],
[75, 75],
[25, 75],
[25, 25],
],
color: '#aa00aa',
lineWidth: 2,
};
const brushId = surfaceModel.addElement(brushProps);
await wait(100);
let brushElement = surfaceView.renderRoot.querySelector(
`[data-element-id="${brushId}"]`
);
expect(brushElement).not.toBeNull();
surfaceModel.deleteElement(brushId);
await wait(100);
brushElement = surfaceView.renderRoot.querySelector(
`[data-element-id="${brushId}"]`
);
expect(brushElement).toBeNull();
});
});
@@ -0,0 +1,158 @@
import { DomRenderer } from '@blocksuite/affine-block-surface';
import { beforeEach, describe, expect, test } from 'vitest';
import { wait } from '../utils/common.js';
import { getSurface } from '../utils/edgeless.js';
import { setupEditor } from '../utils/setup.js';
describe('Connector rendering with DOM renderer', () => {
beforeEach(async () => {
const cleanup = await setupEditor('edgeless', [], {
enableDomRenderer: true,
});
return cleanup;
});
test('should use DomRenderer when enable_dom_renderer flag is true', async () => {
const surface = getSurface(doc, editor);
expect(surface).not.toBeNull();
expect(surface?.renderer).toBeInstanceOf(DomRenderer);
});
test('should render a connector element as a DOM node', async () => {
const surfaceView = getSurface(window.doc, window.editor);
const surfaceModel = surfaceView.model;
// Create two shapes to connect
const shape1Id = surfaceModel.addElement({
type: 'shape',
xywh: '[100, 100, 80, 60]',
});
const shape2Id = surfaceModel.addElement({
type: 'shape',
xywh: '[300, 200, 80, 60]',
});
// Create a connector between the shapes
const connectorProps = {
type: 'connector',
source: { id: shape1Id },
target: { id: shape2Id },
stroke: '#000000',
strokeWidth: 2,
};
const connectorId = surfaceModel.addElement(connectorProps);
await wait(100);
const connectorElement = surfaceView?.renderRoot.querySelector(
`[data-element-id="${connectorId}"]`
);
expect(connectorElement).not.toBeNull();
expect(connectorElement).toBeInstanceOf(HTMLElement);
// Check if SVG element is present for connector rendering
const svgElement = connectorElement?.querySelector('svg');
expect(svgElement).not.toBeNull();
});
test('should render connector with different stroke styles', async () => {
const surfaceView = getSurface(window.doc, window.editor);
const surfaceModel = surfaceView.model;
// Create a dashed connector
const connectorProps = {
type: 'connector',
source: { position: [100, 100] },
target: { position: [200, 200] },
strokeStyle: 'dash',
stroke: '#ff0000',
strokeWidth: 4,
};
const connectorId = surfaceModel.addElement(connectorProps);
// Wait for path generation and rendering
await wait(500);
const connectorElement = surfaceView?.renderRoot.querySelector(
`[data-element-id="${connectorId}"]`
);
expect(connectorElement).not.toBeNull();
const svgElement = connectorElement?.querySelector('svg');
expect(svgElement).not.toBeNull();
// Find the main path element (not the ones inside markers)
const pathElements = svgElement?.querySelectorAll('path');
// The main connector path should be the last one (after marker paths)
const pathElement = pathElements?.[pathElements.length - 1];
expect(pathElement).not.toBeNull();
// Check stroke-dasharray attribute
const strokeDasharray = pathElement!.getAttribute('stroke-dasharray');
expect(strokeDasharray).toBe('12,12');
});
test('should render connector with arrow endpoints', async () => {
const surfaceView = getSurface(window.doc, window.editor);
const surfaceModel = surfaceView.model;
const connectorProps = {
type: 'connector',
source: { position: [100, 100] },
target: { position: [200, 200] },
frontEndpointStyle: 'Triangle',
rearEndpointStyle: 'Arrow',
};
const connectorId = surfaceModel.addElement(connectorProps);
await wait(100);
const connectorElement = surfaceView?.renderRoot.querySelector(
`[data-element-id="${connectorId}"]`
);
expect(connectorElement).not.toBeNull();
// Check for markers in defs
const defsElement = connectorElement?.querySelector('defs');
expect(defsElement).not.toBeNull();
const markers = defsElement?.querySelectorAll('marker');
expect(markers?.length).toBeGreaterThan(0);
});
test('should remove connector DOM node when element is deleted', async () => {
const surfaceView = getSurface(window.doc, window.editor);
const surfaceModel = surfaceView.model;
expect(surfaceView.renderer).toBeInstanceOf(DomRenderer);
const connectorProps = {
type: 'connector',
source: { position: [50, 50] },
target: { position: [150, 150] },
};
const connectorId = surfaceModel.addElement(connectorProps);
await wait(100);
let connectorElement = surfaceView.renderRoot.querySelector(
`[data-element-id="${connectorId}"]`
);
expect(connectorElement).not.toBeNull();
surfaceModel.deleteElement(connectorId);
await wait(100);
connectorElement = surfaceView.renderRoot.querySelector(
`[data-element-id="${connectorId}"]`
);
expect(connectorElement).toBeNull();
});
});
@@ -31,15 +31,12 @@ describe('frame', () => {
);
await wait();
const getFrameTitle = (frameId: string) => {
const frameTitleWidget = service.std.view.getWidget(
'affine-frame-title-widget',
frameId
) as AffineFrameTitleWidget | null;
return frameTitleWidget?.shadowRoot?.querySelector('affine-frame-title');
};
const frameTitleWidget = service.std.view.getWidget(
'affine-frame-title-widget',
doc.root!.id
) as AffineFrameTitleWidget | null;
const frameTitle = getFrameTitle(frame);
const frameTitle = frameTitleWidget?.getFrameTitle(frame);
const rect = frameTitle?.getBoundingClientRect();
expect(frameTitle).toBeTruthy();
@@ -61,7 +58,7 @@ describe('frame', () => {
);
await wait();
const nestedTitle = getFrameTitle(nestedFrame);
const nestedTitle = frameTitleWidget?.getFrameTitle(nestedFrame);
expect(nestedTitle).toBeTruthy();
if (!nestedTitle) return;
@@ -40,7 +40,6 @@ describe('basic', () => {
xywh: '[100, 0, 100, 100]',
index: service.generateIndex(),
})!;
await wait(0); // wait next frame
frameId = service.crud.addBlock(
'affine:frame',
{
@@ -7,40 +7,30 @@ import type { InitFn } from './utils.js';
const presetMarkdown = `Click the 🔁 button to switch between editors dynamically - they are fully compatible!`;
export const preset: InitFn = async (collection: Workspace, id: string) => {
let doc = collection.getDoc(id);
const hasDoc = !!doc;
if (!doc) {
doc = collection.createDoc(id);
}
const doc = collection.createDoc(id).getStore({ id });
doc.load();
// Add root block and surface block at root level
const rootId = doc.addBlock('affine:page', {
title: new Text('BlockSuite Playground'),
});
doc.addBlock('affine:surface', {}, rootId);
const store = doc.getStore({ id });
store.load();
// Add note block inside root block
const noteId = doc.addBlock(
'affine:note',
{ xywh: '[0, 100, 800, 640]' },
rootId
);
// Run only once on all clients.
let noteId: string;
if (!hasDoc) {
// Add root block and surface block at root level
const rootId = store.addBlock('affine:page', {
title: new Text('BlockSuite Playground'),
});
store.addBlock('affine:surface', {}, rootId);
// Import preset markdown content inside note block
await MarkdownTransformer.importMarkdownToBlock({
doc,
blockId: noteId,
markdown: presetMarkdown,
extensions: getTestStoreManager().get('store'),
});
// Add note block inside root block
noteId = store.addBlock(
'affine:note',
{ xywh: '[0, 100, 800, 640]' },
rootId
);
// Import preset markdown content inside note block
await MarkdownTransformer.importMarkdownToBlock({
doc: store,
blockId: noteId,
markdown: presetMarkdown,
extensions: getTestStoreManager().get('store'),
});
}
store.resetHistory();
doc.resetHistory();
};
preset.id = 'preset';
-1
View File
@@ -24,7 +24,6 @@
"@types/katex": "^0.16.7",
"browser-fs-access": "^0.37.0",
"jszip": "^3.10.1",
"katex": "^0.16.11",
"lit": "^3.2.0",
"lz-string": "^1.5.0",
"rxjs": "^7.8.1",
-2
View File
@@ -1,8 +1,6 @@
@import '@toeverything/theme/style.css';
@import '@toeverything/theme/fonts.css';
@import 'katex/dist/katex.min.css';
@font-face {
font-family: 'color-emoji';
src:
+1 -11
View File
@@ -8,11 +8,6 @@ export const AFFINE_PRO_LICENSE_AES_KEY: string | undefined | null
export const AFFINE_PRO_PUBLIC_KEY: string | undefined | null
export interface Chunk {
index: number
content: string
}
export declare function fromModelName(modelName: string): Tokenizer | null
export declare function getMime(input: Uint8Array): string
@@ -27,11 +22,6 @@ export declare function mergeUpdatesInApplyWay(updates: Array<Buffer>): Buffer
export declare function mintChallengeResponse(resource: string, bits?: number | undefined | null): Promise<string>
export interface ParsedDoc {
name: string
chunks: Array<Chunk>
}
export declare function parseDoc(filePath: string, doc: Buffer): Promise<ParsedDoc>
export declare function parseDoc(filePath: string, doc: Buffer): Promise<{ name: string, chunks: Array<{index: number, content: string}> }>
export declare function verifyChallengeResponse(response: string, bits: number, resource: string): Promise<boolean>
+1 -1
View File
@@ -32,7 +32,7 @@
"build:debug": "napi build"
},
"devDependencies": {
"@napi-rs/cli": "3.0.0-alpha.81",
"@napi-rs/cli": "3.0.0-alpha.78",
"lib0": "^0.2.99",
"tiktoken": "^1.0.17",
"tinybench": "^4.0.0",
+25 -34
View File
@@ -2,21 +2,9 @@ use affine_common::doc_loader::Doc;
use napi::{
anyhow::anyhow,
bindgen_prelude::{AsyncTask, Buffer},
Env, Result, Task,
Env, JsObject, Result, Task,
};
#[napi(object)]
pub struct Chunk {
pub index: i64,
pub content: String,
}
#[napi(object)]
pub struct ParsedDoc {
pub name: String,
pub chunks: Vec<Chunk>,
}
pub struct Document {
inner: Doc,
}
@@ -26,20 +14,24 @@ impl Document {
self.inner.name.clone()
}
fn chunks(&self) -> Vec<Chunk> {
self
.inner
.chunks
.iter()
.enumerate()
.map(|(i, chunk)| {
let content = crate::utils::clean_content(&chunk.content);
Chunk {
index: i as i64,
content,
}
})
.collect::<Vec<Chunk>>()
fn chunks(&self, env: Env) -> Result<JsObject> {
let mut array = env.create_array_with_length(self.inner.chunks.len())?;
for (i, chunk) in self.inner.chunks.iter().enumerate() {
let content = crate::utils::clean_content(&chunk.content);
let mut obj = env.create_object()?;
obj.set_named_property("index", i as i64)?;
obj.set_named_property("content", content)?;
array.set_element(i as u32, obj)?;
}
Ok(array)
}
fn resolve(self, env: Env) -> Result<JsObject> {
let mut obj = env.create_object()?;
obj.set_named_property("name", self.name())?;
obj.set_named_property("chunks", self.chunks(env)?)?;
Ok(obj)
}
}
@@ -51,22 +43,21 @@ pub struct AsyncParseDocResponse {
#[napi]
impl Task for AsyncParseDocResponse {
type Output = Document;
type JsValue = ParsedDoc;
type JsValue = JsObject;
fn compute(&mut self) -> Result<Self::Output> {
let doc = Doc::new(&self.file_path, &self.doc).map_err(|e| anyhow!(e))?;
Ok(Document { inner: doc })
}
fn resolve(&mut self, _: Env, doc: Document) -> Result<Self::JsValue> {
Ok(ParsedDoc {
name: doc.name(),
chunks: doc.chunks(),
})
fn resolve(&mut self, env: Env, doc: Document) -> Result<Self::JsValue> {
doc.resolve(env)
}
}
#[napi]
#[napi(
ts_return_type = "Promise<{ name: string, chunks: Array<{index: number, content: string}> }>"
)]
pub fn parse_doc(file_path: String, doc: Buffer) -> AsyncTask<AsyncParseDocResponse> {
AsyncTask::new(AsyncParseDocResponse {
file_path,
+7 -7
View File
@@ -1,7 +1,7 @@
use std::convert::TryFrom;
use affine_common::hashcash::Stamp;
use napi::{bindgen_prelude::AsyncTask, Env, Result as NapiResult, Task};
use napi::{bindgen_prelude::AsyncTask, Env, JsBoolean, JsString, Result as NapiResult, Task};
use napi_derive::napi;
pub struct AsyncVerifyChallengeResponse {
@@ -13,7 +13,7 @@ pub struct AsyncVerifyChallengeResponse {
#[napi]
impl Task for AsyncVerifyChallengeResponse {
type Output = bool;
type JsValue = bool;
type JsValue = JsBoolean;
fn compute(&mut self) -> NapiResult<Self::Output> {
Ok(if let Ok(stamp) = Stamp::try_from(self.response.as_str()) {
@@ -23,8 +23,8 @@ impl Task for AsyncVerifyChallengeResponse {
})
}
fn resolve(&mut self, _: Env, output: bool) -> NapiResult<Self::JsValue> {
Ok(output)
fn resolve(&mut self, env: Env, output: bool) -> NapiResult<Self::JsValue> {
env.get_boolean(output)
}
}
@@ -49,14 +49,14 @@ pub struct AsyncMintChallengeResponse {
#[napi]
impl Task for AsyncMintChallengeResponse {
type Output = String;
type JsValue = String;
type JsValue = JsString;
fn compute(&mut self) -> NapiResult<Self::Output> {
Ok(Stamp::mint(self.resource.clone(), self.bits).format())
}
fn resolve(&mut self, _: Env, output: String) -> NapiResult<Self::JsValue> {
Ok(output)
fn resolve(&mut self, env: Env, output: String) -> NapiResult<Self::JsValue> {
env.create_string(&output)
}
}
+5 -5
View File
@@ -28,11 +28,11 @@
"dependencies": {
"@affine/reader": "workspace:*",
"@affine/server-native": "workspace:*",
"@ai-sdk/anthropic": "^1.2.12",
"@ai-sdk/anthropic": "^1.2.10",
"@ai-sdk/google": "^1.2.18",
"@ai-sdk/google-vertex": "^2.2.23",
"@ai-sdk/openai": "^1.3.22",
"@ai-sdk/perplexity": "^1.1.9",
"@ai-sdk/google-vertex": "^2.2.22",
"@ai-sdk/openai": "^1.3.21",
"@ai-sdk/perplexity": "^1.1.6",
"@apollo/server": "^4.11.3",
"@aws-sdk/client-s3": "^3.779.0",
"@aws-sdk/s3-request-presigner": "^3.779.0",
@@ -100,7 +100,7 @@
"nanoid": "^5.0.9",
"nest-commander": "^3.15.0",
"nest-winston": "^1.9.7",
"nestjs-cls": "^6.0.0",
"nestjs-cls": "^5.0.0",
"nodemailer": "^7.0.0",
"on-headers": "^1.0.2",
"piscina": "^5.0.0-alpha.0",
@@ -272,68 +272,13 @@ test('should validate markdown list', t => {
// ==================== action ====================
const actions = [
{
name: 'Should chat with histories',
promptName: ['Chat With AFFiNE AI'],
messages: [
{
role: 'user' as const,
content: `
Hi! Im going to send you a technical term related to real-time collaborative editing (e.g., CRDT, Operational Transformation, OT Composer, etc.). Whenever I send you a term:
1. Translate it into Chinese (send me the Chinese version).
2. Then translate that Chinese back into English (send me the retranslated English).
3. Provide a brief, English-language introduction and context for this concept.
4. In that English explanation, annotate any niche terms with their Chinese equivalents in parentheses (for example: “Conflict-Free Replicated Data Type (无冲突复制数据类型)”).
5. Finally, give the origin or “term history” (e.g., who introduced it, in which paper or year).
If you understand, please proceed by explaining the term “CRDT.”
`.trim(),
},
{
role: 'assistant' as const,
content: `
1. **Chinese Translation:**
“CRDT” → **无冲突复制数据类型**
2. **Back-Translation to English:**
无冲突复制数据类型 → **Conflict-Free Replicated Data Type**
3. **English Introduction & Context:**
A **Conflict-Free Replicated Data Type (无冲突复制数据类型)** is an abstract data type designed for distributed systems where replicas of shared state may be modified concurrently without requiring coordination. CRDTs allow multiple users or processes to update the same data structure (for example, a shared document in a collaborative editor) at the same time.
- **Key Terms (with Chinese equivalents):**
- **Replica (副本):** Each node or client maintains its own copy of the data.
- **State-based (状态型) vs. Operation-based (操作型):** Two main CRDT classes; state-based CRDTs exchange entire state snapshots occasionally, whereas operation-based CRDTs broadcast only incremental operations.
- **Merge Function (合并函数):** A deterministic function that resolves differences between two replicas without conflicts.
CRDTs enable **eventual consistency (最终一致性)** in real-time collaborative editors by ensuring that, after all updates propagate, every replica converges to the same state, even if operations arrive in different orders. This approach removes the need for a centralized server to resolve conflicts, making offline or peer-to-peer editing possible.
4. **Origin / Term History:**
The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Carlos Baquero, and Marek Zawirski in their 2011 paper titled “Conflict-free Replicated Data Types” (published in the _Stabilization, Safety, and Security of Distributed Systems (SSS)_ conference). They formalized two families of CRDTs—state-based (“Convergent Replicated Data Types” or CvRDTs) and operation-based (“Commutative Replicated Data Types” or CmRDTs)—and proved their convergence properties under asynchronous, unreliable networks.
`.trim(),
},
{
role: 'user' as const,
content: `Thanks! Now please just tell me the **Chinese translation** and the **back-translated English term** that you provided previously for “CRDT.” Do not reprint the full introduction—only those two lines.`,
},
],
verifier: (t: ExecutionContext<Tester>, result: string) => {
assertNotWrappedInCodeBlock(t, result);
const lower = result.toLowerCase();
t.assert(
lower.includes('无冲突复制数据类型') &&
lower.includes('conflict-free replicated data type'),
'The response should include “无冲突复制数据类型” and “Conflict-Free Replicated Data Type”'
);
},
type: 'text' as const,
},
{
name: 'Should not have citation',
promptName: ['Chat With AFFiNE AI'],
messages: [
{
role: 'user' as const,
content: 'what is AFFiNE AI?',
content: 'what is ssot',
params: {
files: [
{
@@ -48,7 +48,7 @@ export class JobQueue {
const removed = await queue.remove(jobId);
if (removed) {
this.logger.log(`Job ${jobName}(id=${jobId}) removed from queue ${ns}`);
this.logger.log(`Job ${jobName} removed from queue ${ns}`);
return job.data.payload;
}
@@ -1,135 +0,0 @@
# Snapshot report for `src/base/utils/__tests__/duration.spec.ts`
The actual snapshot is saved in `duration.spec.ts.snap`.
Generated by [AVA](https://avajs.dev).
## should parse duration strings correctly
> parser - 1ms
'{"ms":1}'
> ms - 1ms
1
> parser - 1s
'{"s":1}'
> ms - 1s
1000
> parser - 1m
'{"m":1}'
> ms - 1m
60000
> parser - 1h
'{"h":1}'
> ms - 1h
3600000
> parser - 1d
'{"d":1}'
> ms - 1d
86400000
> parser - 1w
'{"w":1}'
> ms - 1w
604800000
> parser - 1M
'{"M":1}'
> ms - 1M
2592000000
> parser - 1y
'{"y":1}'
> ms - 1y
31536000000
> parser - 1000ms
'{"ms":1000}'
> ms - 1000ms
1000
> parser - 60s
'{"s":60}'
> ms - 60s
60000
> parser - 30m
'{"m":30}'
> ms - 30m
1800000
> parser - 1h30m
'{"h":1,"m":30}'
> ms - 1h30m
5400000
> parser - 15d
'{"d":15}'
> ms - 15d
1296000000
> parser - 1y
'{"y":1}'
> ms - 1y
31536000000
> parser - 12M
'{"M":12}'
> ms - 12M
31104000000
> parser - 1y1M1d1h1m1s1ms
'{"y":1,"M":1,"d":1,"h":1,"m":1,"s":1,"ms":1}'
> ms - 1y1M1d1h1m1s1ms
34218061001
@@ -1,37 +0,0 @@
import test from 'ava';
import { Due } from '../duration';
test('should parse duration strings correctly', t => {
const testcases = [
'1ms',
'1s',
'1m',
'1h',
'1d',
'1w',
'1M',
'1y',
'1000ms',
'60s',
'30m',
'1h30m',
'15d',
'1y',
'12M',
'1y1M1d1h1m1s1ms',
];
for (const str of testcases) {
t.snapshot(JSON.stringify(Due.parse(str)), `parser - ${str}`);
t.snapshot(Due.ms(str), `ms - ${str}`);
}
});
test('should calc relative time correctly', t => {
const date = new Date();
t.is(Due.before('1d', date).getTime(), date.getTime() - 1000 * 60 * 60 * 24);
const date2 = new Date();
t.is(Due.after('1d', date2).getTime(), date2.getTime() + 1000 * 60 * 60 * 24);
});
@@ -1,78 +0,0 @@
type DurationUnit = 'd' | 'w' | 'M' | 'y' | 'h' | 'm' | 's' | 'ms';
type DurationInput = Partial<Record<DurationUnit, number>>;
const UnitToSecMap: Record<DurationUnit, number> = {
ms: 0.001,
s: 1,
m: 60,
h: 3600,
d: 24 * 3600,
w: 7 * 24 * 3600,
M: 30 * 24 * 3600,
y: 365 * 24 * 3600,
};
const KnownCharCodeToCharMap: Record<number, DurationUnit> = {
100: 'd',
119: 'w',
77: 'M',
121: 'y',
104: 'h',
109: 'm',
115: 's',
};
function parse(str: string): DurationInput {
let input: DurationInput = {};
let acc = 0;
for (let i = 0; i < str.length; i++) {
const ch = str[i];
const code = ch.charCodeAt(0);
// number [0..9]
if (code >= 48 && code <= 57) {
acc = acc * 10 + code - 48;
} else {
let unit = KnownCharCodeToCharMap[code];
if (!unit) {
throw new Error(`Invalid duration string unit ${ch}`);
}
// look ahead a char for 'ms' checking if unit met 'm'
if (unit === 'm' && str[i + 1] === 's') {
unit = 'ms';
i++;
}
input[unit] = acc;
acc = 0;
}
}
return input;
}
export const Due = {
ms: (dueStr: string | DurationInput) => {
const input = typeof dueStr === 'string' ? parse(dueStr) : dueStr;
return Object.entries(input).reduce((duration, [unit, val]) => {
return duration + UnitToSecMap[unit as DurationUnit] * (val || 0) * 1000;
}, 0);
},
s: (dueStr: string | DurationInput) => {
const input = typeof dueStr === 'string' ? parse(dueStr) : dueStr;
return Object.entries(input).reduce((duration, [unit, val]) => {
return duration + UnitToSecMap[unit as DurationUnit] * (val || 0);
}, 0);
},
parse,
after: (dueStr: string | number | DurationInput, date?: Date) => {
const timestamp = typeof dueStr === 'number' ? dueStr : Due.ms(dueStr);
return new Date((date?.getTime() ?? Date.now()) + timestamp);
},
before: (dueStr: string | number | DurationInput, date?: Date) => {
const timestamp = typeof dueStr === 'number' ? dueStr : Due.ms(dueStr);
return new Date((date?.getTime() ?? Date.now()) - timestamp);
},
};
@@ -1,4 +1,3 @@
export * from './duration';
export * from './promise';
export * from './request';
export * from './stream';
@@ -203,9 +203,7 @@ export class SpaceSyncGateway
await client.join(room);
}
} else {
if (spaceType === SpaceType.Workspace) {
this.event.emit('workspace.embedding', { workspaceId: spaceId });
}
this.event.emit('workspace.embedding', { workspaceId: spaceId });
await this.selectAdapter(client, spaceType).join(user.id, spaceId);
}
@@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common';
import {
AFFiNELogger,
BlobNotFound,
CallMetric,
Config,
CopilotContextFileNotSupported,
DocNotFound,
@@ -66,7 +65,6 @@ export class CopilotContextDocJob {
return this.client as EmbeddingClient;
}
@CallMetric('ai', 'addFileEmbeddingQueue')
async addFileEmbeddingQueue(file: Jobs['copilot.embedding.files']) {
if (!this.supportEmbedding) return;
@@ -131,10 +129,6 @@ export class CopilotContextDocJob {
if (enableDocEmbedding) {
const toBeEmbedDocIds =
await this.models.copilotWorkspace.findDocsToEmbed(workspaceId);
this.logger.debug('Trigger embedding for docs', {
workspaceId,
toBeEmbedDocs: toBeEmbedDocIds.length,
});
for (const docId of toBeEmbedDocIds) {
await this.queue.add(
'copilot.embedding.docs',
@@ -186,7 +180,7 @@ export class CopilotContextDocJob {
);
}
private async readCopilotBlob(
async readCopilotBlob(
userId: string,
workspaceId: string,
blobId: string,
@@ -377,9 +371,6 @@ export class CopilotContextDocJob {
error instanceof CopilotContextFileNotSupported &&
error.message.includes('no content found')
) {
this.logger.warn(
`Doc ${docId} in workspace ${workspaceId} has no content, fulfilling empty embedding.`
);
// if the doc is empty, we still need to fulfill the embedding
await this.fulfillEmptyEmbedding(workspaceId, docId);
return;
@@ -1558,7 +1558,7 @@ const imageActions: Prompt[] = [
];
const CHAT_PROMPT: Omit<Prompt, 'name'> = {
model: 'claude-sonnet-4@20250514',
model: 'gpt-4.1',
optionalModels: [
'gpt-4.1',
'o3',
@@ -58,7 +58,7 @@ export abstract class AnthropicProvider<T> extends CopilotProvider<T> {
try {
metrics.ai.counter('chat_text_calls').add(1, { model: model.id });
const [system, msgs] = await chatToGPTMessage(messages, true, true);
const [system, msgs] = await chatToGPTMessage(messages);
const modelInstance = this.instance(model.id);
const { text, reasoning } = await generateText({
@@ -142,7 +142,7 @@ export abstract class AnthropicProvider<T> extends CopilotProvider<T> {
}
private isReasoningModel(model: string) {
// claude 3.5 sonnet doesn't support reasoning config
return model.includes('sonnet') && !model.startsWith('claude-3-5-sonnet');
// only claude 3.7 sonnet supports reasoning config
return model.startsWith('claude-3-7-sonnet');
}
}
@@ -64,10 +64,7 @@ export async function inferMimeType(url: string) {
export async function chatToGPTMessage(
messages: PromptMessage[],
// TODO(@darkskygit): move this logic in interface refactoring
withAttachment: boolean = true,
// NOTE: some providers in vercel ai sdk are not able to handle url attachments yet
// so we need to use base64 encoded attachments instead
useBase64Attachment: boolean = false
withAttachment: boolean = true
): Promise<[string | undefined, ChatMessage[], ZodType?]> {
const system = messages[0]?.role === 'system' ? messages.shift() : undefined;
const schema =
@@ -104,10 +101,9 @@ export async function chatToGPTMessage(
if (mimeType.startsWith('image/')) {
contents.push({ type: 'image', image: attachment, mimeType });
} else {
const data =
attachment.startsWith('data:') || useBase64Attachment
? await fetch(attachment).then(r => r.arrayBuffer())
: new URL(attachment);
const data = attachment.startsWith('data:')
? await fetch(attachment).then(r => r.arrayBuffer())
: new URL(attachment);
contents.push({ type: 'file' as const, data, mimeType });
}
}
@@ -142,17 +142,17 @@ export class ChatSession implements AsyncDisposable {
}
private mergeUserContent(params: PromptParams) {
const messages = this.takeMessages();
const lastMessage = messages.pop();
const messages = this.stashMessages;
const firstMessage = messages.at(0);
if (
this.state.prompt.paramKeys.includes('content') &&
!messages.some(m => m.role === AiPromptRole.assistant) &&
lastMessage?.role === AiPromptRole.user
firstMessage
) {
const normalizedParams = {
...params,
...lastMessage.params,
content: lastMessage.content,
...firstMessage.params,
content: firstMessage.content,
};
const finished = this.state.prompt.finish(
normalizedParams,
@@ -160,16 +160,11 @@ export class ChatSession implements AsyncDisposable {
);
// attachments should be combined with the first user message
const firstUserMessageIndex = finished.findIndex(
m => m.role === AiPromptRole.user
);
// if prompt not contains user message, skip merge content
if (firstUserMessageIndex < 0) return null;
const firstUserMessage = finished[firstUserMessageIndex];
const firstUserMessage =
finished.find(m => m.role === 'user') || finished[0];
firstUserMessage.attachments = [
finished[0].attachments || [],
lastMessage.attachments || [],
firstMessage.attachments || [],
]
.flat()
.filter(v =>
@@ -177,8 +172,6 @@ export class ChatSession implements AsyncDisposable {
? !!v.trim()
: v && v.attachment.trim() && v.mimeType
);
//insert all previous user message content before first user message
finished.splice(firstUserMessageIndex, 0, ...messages);
return finished;
}
@@ -13,13 +13,11 @@ import { ConnectedAccount } from '@prisma/client';
import type { Request, Response } from 'express';
import {
Config,
InvalidAuthState,
InvalidOauthCallbackState,
MissingOauthQueryParameter,
OauthAccountAlreadyConnected,
OauthStateExpired,
SignUpForbidden,
UnknownOauthProvider,
URLHelper,
UseNamedGuard,
@@ -40,8 +38,7 @@ export class OAuthController {
private readonly oauth: OAuthService,
private readonly models: Models,
private readonly providerFactory: OAuthProviderFactory,
private readonly url: URLHelper,
private readonly config: Config
private readonly url: URLHelper
) {}
@Public()
@@ -83,10 +80,8 @@ export class OAuthController {
};
}
// the prerequest `/oauth/prelight` request already checked client version,
// let's simply ignore it for callback which will block apple oauth post_form mode
// @UseNamedGuard('version')
@Public()
@UseNamedGuard('version')
@Post('/callback')
@HttpCode(HttpStatus.OK)
async callback(
@@ -187,7 +182,7 @@ export class OAuthController {
}
const externAccount = await provider.getUser(tokens, state);
const user = await this.getOrCreateUserFromOauth(
const user = await this.loginFromOauth(
state.provider,
externAccount,
tokens
@@ -208,7 +203,7 @@ export class OAuthController {
});
}
private async getOrCreateUserFromOauth(
private async loginFromOauth(
provider: OAuthProviderName,
externalAccount: OAuthAccount,
tokens: Tokens
@@ -224,10 +219,6 @@ export class OAuthController {
return connectedAccount.user;
}
if (!this.config.auth.allowSignup) {
throw new SignUpForbidden();
}
const user = await this.models.user.fulfill(externalAccount.email, {
avatarUrl: externalAccount.avatarUrl,
});

Some files were not shown because too many files have changed in this diff Show More