diff --git a/bun.lock b/bun.lock index bcfb8cc..2a3afc3 100644 --- a/bun.lock +++ b/bun.lock @@ -84,7 +84,6 @@ "jsdom": "^28.1.0", "msw": "^2.12.10", "nx": "22.4.2", - "patch-package": "^8.0.1", "peggy": "5.1.0", "tailwindcss": "^4", "tw-animate-css": "^1.4.0", @@ -223,6 +222,9 @@ }, }, }, + "patchedDependencies": { + "@plait/draw@0.92.3": "patches/@plait%2Fdraw@0.92.3.patch", + }, "packages": { "@acemir/cssom": ["@acemir/cssom@0.9.31", "", {}, "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA=="], @@ -1220,7 +1222,7 @@ "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + "bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], @@ -1252,8 +1254,6 @@ "chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="], - "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], - "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], "classcat": ["classcat@5.0.5", "", {}, "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="], @@ -1554,8 +1554,6 @@ "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - "find-yarn-workspace-root": ["find-yarn-workspace-root@2.0.0", "", { "dependencies": { "micromatch": "^4.0.2" } }, "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ=="], - "flat": ["flat@5.0.2", "", { "bin": { "flat": "cli.js" } }, "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ=="], "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], @@ -1574,8 +1572,6 @@ "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], - "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -1820,18 +1816,12 @@ "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "json-stable-stringify": ["json-stable-stringify@1.3.0", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "isarray": "^2.0.5", "jsonify": "^0.0.1", "object-keys": "^1.1.1" } }, "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg=="], - "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "jsonc-parser": ["jsonc-parser@3.2.0", "", {}, "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w=="], - "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - - "jsonify": ["jsonify@0.0.1", "", {}, "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg=="], - "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], "katex": ["katex@0.16.37", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-TIGjO2cCGYono+uUzgkE7RFF329mLLWGuHUlSr6cwIVj9O8f0VQZ783rsanmJpFUo32vvtj7XT04NGRPh+SZFg=="], @@ -1840,8 +1830,6 @@ "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], - "klaw-sync": ["klaw-sync@6.0.0", "", { "dependencies": { "graceful-fs": "^4.1.11" } }, "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ=="], - "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], "langium": ["langium@4.2.1", "", { "dependencies": { "chevrotain": "~11.1.1", "chevrotain-allstar": "~0.3.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.1.0" } }, "sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ=="], @@ -2126,8 +2114,6 @@ "parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], - "patch-package": ["patch-package@8.0.1", "", { "dependencies": { "@yarnpkg/lockfile": "^1.1.0", "chalk": "^4.1.2", "ci-info": "^3.7.0", "cross-spawn": "^7.0.3", "find-yarn-workspace-root": "^2.0.0", "fs-extra": "^10.0.0", "json-stable-stringify": "^1.0.2", "klaw-sync": "^6.0.0", "minimist": "^1.2.6", "open": "^7.4.2", "semver": "^7.5.3", "slash": "^2.0.0", "tmp": "^0.2.4", "yaml": "^2.2.2" }, "bin": { "patch-package": "index.js" } }, "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw=="], - "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], @@ -2326,8 +2312,6 @@ "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], - "slash": ["slash@2.0.0", "", {}, "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A=="], - "slate": ["slate@0.116.0", "", { "dependencies": { "immer": "^10.0.3", "tiny-warning": "^1.0.3" } }, "sha512-CpI6S0PTFDjFsYKah3geR7X5O/38tjxVlwkYGpfQ583yYkSbt/pyEldTB1CJE3MK4htfn2D/wPkwWtDV06ZqWA=="], "slate-dom": ["slate-dom@0.116.0", "", { "dependencies": { "@juggle/resize-observer": "^3.4.0", "direction": "^1.0.4", "is-hotkey": "^0.2.0", "is-plain-object": "^5.0.0", "lodash": "^4.17.21", "scroll-into-view-if-needed": "^3.1.0", "tiny-invariant": "1.3.1" }, "peerDependencies": { "slate": ">=0.99.0" } }, "sha512-ZyyPdT4zY4d/P/gfqMWBaAWWqV3N2BbAiDqEfOtZwPLcy7vvC02PsVvSZLaGun7DvaM2EIqdN7tTq3REbQkYgA=="], @@ -2504,8 +2488,6 @@ "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], - "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - "unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="], "until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="], @@ -2886,8 +2868,6 @@ "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "patch-package/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], - "points-on-path/points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], diff --git a/features/board/utils/sticky-note.ts b/features/board/utils/sticky-note.ts index 61661ef..7150e0e 100644 --- a/features/board/utils/sticky-note.ts +++ b/features/board/utils/sticky-note.ts @@ -8,11 +8,11 @@ import { type StickyColorName, } from '@/shared/constants'; -const STICKY_TEXT_PADDING = 30; +const STICKY_TEXT_PADDING = 28; const STICKY_LINE_HEIGHT = 22; const STICKY_CHAR_WIDTH = 8; const MIN_STICKY_SIZE = 130; -const MAX_STICKY_SIZE = 320; +const MAX_STICKY_WIDTH = 280; export function isStickyColorName(value: string): value is StickyColorName { return value in STICKY_COLORS; @@ -28,18 +28,16 @@ export function estimateStickySize(text: string): { const lines = text.split(/\r?\n/); const longestLineLength = Math.max(...lines.map((line) => line.length), 1); - const targetWidth = Math.min( - 220, - Math.max( - MIN_STICKY_SIZE, - Math.ceil( - longestLineLength * STICKY_CHAR_WIDTH * 0.6 + STICKY_TEXT_PADDING, - ), - ), + + const desiredContentWidth = Math.ceil(longestLineLength * STICKY_CHAR_WIDTH * 0.6); + const width = Math.min( + MAX_STICKY_WIDTH, + Math.max(MIN_STICKY_SIZE, desiredContentWidth + STICKY_TEXT_PADDING), ); + const charsPerLine = Math.max( 1, - Math.floor((targetWidth - STICKY_TEXT_PADDING) / STICKY_CHAR_WIDTH), + Math.floor((width - STICKY_TEXT_PADDING) / (STICKY_CHAR_WIDTH * 0.6)), ); const wrappedLineCount = Math.max( 1, @@ -48,23 +46,9 @@ export function estimateStickySize(text: string): { 0, ), ); - const width = Math.min( - MAX_STICKY_SIZE, - Math.max( - MIN_STICKY_SIZE, - Math.max( - targetWidth, - Math.min(220, longestLineLength * STICKY_CHAR_WIDTH * 0.75) + - STICKY_TEXT_PADDING, - ), - ), - ); - const height = Math.min( - MAX_STICKY_SIZE, - Math.max( - MIN_STICKY_SIZE, - wrappedLineCount * STICKY_LINE_HEIGHT + STICKY_TEXT_PADDING, - ), + const height = Math.max( + MIN_STICKY_SIZE, + wrappedLineCount * STICKY_LINE_HEIGHT + STICKY_TEXT_PADDING, ); return { width, height }; diff --git a/features/board/utils/theme-elements.ts b/features/board/utils/theme-elements.ts index 99e3a9f..87232f3 100644 --- a/features/board/utils/theme-elements.ts +++ b/features/board/utils/theme-elements.ts @@ -70,6 +70,77 @@ function normalizeColorToken(color: string): string { return value; } +function parseRgb(color: string): { r: number; g: number; b: number; a: number } | null { + const value = color.trim().toLowerCase(); + + const shortHex = value.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/); + if (shortHex) { + const [, r, g, b] = shortHex; + return { + r: parseInt(r + r, 16), + g: parseInt(g + g, 16), + b: parseInt(b + b, 16), + a: 1, + }; + } + + const longHex = value.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/); + if (longHex) { + const [, r, g, b] = longHex; + return { r: parseInt(r, 16), g: parseInt(g, 16), b: parseInt(b, 16), a: 1 }; + } + + const rgbMatch = value.match( + /^rgba?\(\s*(\d{1,3})[\s,]+(\d{1,3})[\s,]+(\d{1,3})(?:[\s,\/]+([01]?(?:\.\d+)?))?\s*\)$/, + ); + if (rgbMatch) { + const [, r, g, b, alpha] = rgbMatch; + return { + r: Number(r), + g: Number(g), + b: Number(b), + a: alpha === undefined ? 1 : Number(alpha), + }; + } + + return null; +} + +function relativeLuminance(channel: number): number { + const c = channel / 255; + return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); +} + +function isLightFill(fill: string): boolean { + const rgb = parseRgb(fill); + if (!rgb || rgb.a === 0) { + return false; + } + const luminance = + 0.2126 * relativeLuminance(rgb.r) + + 0.7152 * relativeLuminance(rgb.g) + + 0.0722 * relativeLuminance(rgb.b); + return luminance > 0.5; +} + +function hasOpaqueFill(fill: string | undefined): fill is string { + if (typeof fill !== 'string') { + return false; + } + const rgb = parseRgb(fill); + return !!rgb && rgb.a > 0; +} + +function resolveTextInkForFill( + effectiveFill: string | undefined, + theme: BoardThemeMode, +): string { + if (hasOpaqueFill(effectiveFill)) { + return isLightFill(effectiveFill) ? LIGHT_BOARD_INK.text : DARK_BOARD_INK.text; + } + return getBoardInkColors(theme).text; +} + function remapDefaultInk(color: string, theme: BoardThemeMode, kind: 'stroke' | 'text') { const normalized = normalizeColorToken(color); const lightValue = kind === 'stroke' ? LIGHT_BOARD_INK.stroke : LIGHT_BOARD_INK.text; @@ -113,34 +184,42 @@ function isRootMindElement(element: ThemeSyncElement): boolean { return element.type === 'mindmap' && element.isRoot === true; } -function syncSlateTextValue(text: SlateTextValue | undefined, theme: BoardThemeMode) { +function syncSlateTextValue( + text: SlateTextValue | undefined, + theme: BoardThemeMode, + effectiveFill?: string, +) { if (!text || !Array.isArray(text.children)) { return text; } let changed = false; - const targetInk = getBoardInkColors(theme); + const targetTextColor = resolveTextInkForFill(effectiveFill, theme); const nextChildren = text.children.map((child) => { if (!child || typeof child !== 'object' || typeof child.text !== 'string') { return child; } if (!child.color) { - if (targetInk.text === LIGHT_BOARD_INK.text) { + if (targetTextColor === LIGHT_BOARD_INK.text && !hasOpaqueFill(effectiveFill)) { return child; } changed = true; - return { ...child, color: targetInk.text }; + return { ...child, color: targetTextColor }; + } + + const normalized = normalizeColorToken(child.color); + if (normalized !== LIGHT_BOARD_INK.text && normalized !== DARK_BOARD_INK.text) { + return child; } - const nextColor = remapDefaultInk(child.color, theme, 'text'); - if (nextColor === child.color) { + if (normalized === targetTextColor) { return child; } changed = true; - return { ...child, color: nextColor }; + return { ...child, color: targetTextColor }; }); return changed ? { ...text, children: nextChildren } : text; @@ -170,13 +249,16 @@ function syncElementForBoardTheme( changed = true; } + let effectiveFill: string | undefined; if (targetRootFill && (typeof element.fill !== 'string' || isThinkixMindRootFill(element.fill))) { + effectiveFill = targetRootFill; if (element.fill !== targetRootFill) { next.fill = targetRootFill; changed = true; } } else if (typeof element.fill === 'string') { const fill = remapDefaultFill(element.fill, theme); + effectiveFill = fill; if (fill !== element.fill) { next.fill = fill; changed = true; @@ -194,13 +276,13 @@ function syncElementForBoardTheme( changed = true; } - const text = syncSlateTextValue(element.text, theme); + const text = syncSlateTextValue(element.text, theme, effectiveFill); if (text !== element.text) { next.text = text; changed = true; } - const topic = syncSlateTextValue(element.data?.topic, theme); + const topic = syncSlateTextValue(element.data?.topic, theme, effectiveFill); if (element.data && topic !== element.data.topic) { next.data = { ...element.data, topic }; changed = true; diff --git a/package.json b/package.json index af9081f..6168af9 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ ], "scripts": { "dev": "next dev", - "prebuild": "bash scripts/apply-patches.sh && bun run build:parser", + "prebuild": "bun run build:parser", "pretest": "bun run build:parser", "build": "bunx next build", "start": "next start", @@ -23,7 +23,6 @@ "test:e2e:debug": "playwright test --debug", "test:e2e:report": "playwright show-report", "test:all": "bun run test:run && bun run test:e2e", - "postinstall": "bash scripts/apply-patches.sh", "build:parser": "peggy -o features/agent/tools/dsl/parser.js features/agent/tools/dsl/thinkix.peggy --format es" }, "dependencies": { @@ -106,12 +105,14 @@ "jsdom": "^28.1.0", "msw": "^2.12.10", "nx": "22.4.2", - "patch-package": "^8.0.1", "peggy": "5.1.0", "tailwindcss": "^4", "tw-animate-css": "^1.4.0", "typescript": "^5", "vitest": "^4.0.18" }, - "nx": {} + "nx": {}, + "patchedDependencies": { + "@plait/draw@0.92.3": "patches/@plait%2Fdraw@0.92.3.patch" + } } \ No newline at end of file diff --git a/patches/@plait%2Fdraw@0.92.3.patch b/patches/@plait%2Fdraw@0.92.3.patch new file mode 100644 index 0000000..1b0e2a3 --- /dev/null +++ b/patches/@plait%2Fdraw@0.92.3.patch @@ -0,0 +1,9736 @@ +diff --git a/fesm2022/plait-draw.mjs b/fesm2022/plait-draw.mjs +index c66478e071aa029f80ab3ca12a09ca5836612d65..1dce2fd6f0556afffd601d31effe595d6fe8f8f1 100644 +--- a/fesm2022/plait-draw.mjs ++++ b/fesm2022/plait-draw.mjs +@@ -1799,7 +1799,7 @@ const CloudEngine = { + .map((command) => `A ${command.rx} ${command.ry} ${command.xAxisRotation} ${command.largeArcFlag} ${command.sweepFlag} ${command.endX} ${command.endY}`) + .join('\n') + + ' Z'; +- const svgElement = rs.path(pathData, { ...options, fillStyle: 'solid' }); ++ const svgElement = rs.path(pathData, options); + setPathStrokeLinecap(svgElement, 'round'); + return svgElement; + }, +@@ -2201,8 +2201,7 @@ const drawBoundReaction = (board, element, roughOptions = { hasMask: true, hasCo + const maskG = drawShape(board, activeRectangle, shape, { + stroke: SELECTION_BORDER_COLOR, + strokeWidth: 0, +- fill: isClosedDrawElement(element) ? SELECTION_FILL_COLOR : DefaultDrawStyle.fill, +- fillStyle: 'solid' ++ fill: isClosedDrawElement(element) ? SELECTION_FILL_COLOR : DefaultDrawStyle.fill + }, drawOptions); + g.appendChild(maskG); + } +@@ -2212,8 +2211,7 @@ const drawBoundReaction = (board, element, roughOptions = { hasMask: true, hasCo + const circleG = drawCircle(PlaitBoard.getRoughSVG(board), point, 8, { + stroke: SELECTION_BORDER_COLOR, + strokeWidth: ACTIVE_STROKE_WIDTH, +- fill: '#FFF', +- fillStyle: 'solid' ++ fill: '#FFF' + }); + g.appendChild(circleG); + }); +@@ -2286,7 +2284,8 @@ class GeometryShapeGenerator extends Generator { + stroke: strokeColor, + strokeWidth, + fill, +- strokeLineDash ++ strokeLineDash, ++ fillStyle: element.fillStyle + }); + } + } +@@ -2496,8 +2495,7 @@ class ArrowLineAutoCompleteGenerator extends Generator { + middlePoints.forEach((point, index) => { + const circle = drawCircle(PlaitBoard.getRoughSVG(this.board), point, LINE_AUTO_COMPLETE_DIAMETER, { + stroke: 'none', +- fill: rgbaToHEX(PRIMARY_COLOR, LINE_AUTO_COMPLETE_OPACITY), +- fillStyle: 'solid' ++ fill: rgbaToHEX(PRIMARY_COLOR, LINE_AUTO_COMPLETE_OPACITY) + }); + circle.classList.add(`line-auto-complete-${index}`); + this.autoCompleteG.appendChild(circle); +@@ -2543,7 +2541,8 @@ class TableGenerator extends Generator { + return getEngine(TableSymbols.table).draw(this.board, rectangle, { + strokeWidth, + stroke: strokeColor, +- strokeLineDash ++ strokeLineDash, ++ fillStyle: element.fillStyle + }, { + element: element + }); +@@ -4085,7 +4084,7 @@ const CommentEngine = { + draw(board, rectangle, options) { + const points = getCommentPoints(rectangle); + const rs = PlaitBoard.getRoughSVG(board); +- const polygon = rs.polygon(points, { ...options, fillStyle: 'solid' }); ++ const polygon = rs.polygon(points, options); + setStrokeLinecap(polygon, 'round'); + return polygon; + }, +@@ -4132,7 +4131,7 @@ function createPolygonEngine(options) { + draw(board, rectangle, options) { + const points = getPoints(rectangle); + const rs = PlaitBoard.getRoughSVG(board); +- const polygon = rs.polygon(points, { ...options, fillStyle: 'solid' }); ++ const polygon = rs.polygon(points, options); + setStrokeLinecap(polygon, 'round'); + return polygon; + }, +@@ -4222,7 +4221,7 @@ function createEllipseEngine(createOptions) { + draw(board, rectangle, options) { + const centerPoint = [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height / 2]; + const rs = PlaitBoard.getRoughSVG(board); +- const shape = rs.ellipse(centerPoint[0], centerPoint[1], rectangle.width, rectangle.height, { ...options, fillStyle: 'solid' }); ++ const shape = rs.ellipse(centerPoint[0], centerPoint[1], rectangle.width, rectangle.height, options); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -4447,7 +4446,7 @@ const RightArrowEngine = createPolygonEngine({ + + const RectangleEngine = { + draw(board, rectangle, options) { +- return drawRectangle(board, rectangle, { ...options, fillStyle: 'solid' }); ++ return drawRectangle(board, rectangle, options); + }, + isInsidePoint(rectangle, point) { + const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); +@@ -4471,7 +4470,7 @@ const RectangleEngine = { + + const RoundRectangleEngine = { + draw(board, rectangle, options) { +- return drawRoundRectangle(PlaitBoard.getRoughSVG(board), rectangle.x, rectangle.y, rectangle.x + rectangle.width, rectangle.y + rectangle.height, { ...options, fillStyle: 'solid' }, false, getRoundRectangleRadius(rectangle)); ++ return drawRoundRectangle(PlaitBoard.getRoughSVG(board), rectangle.x, rectangle.y, rectangle.x + rectangle.width, rectangle.y + rectangle.height, options, false, getRoundRectangleRadius(rectangle)); + }, + isInsidePoint(rectangle, point) { + return isPointInRoundRectangle(point, rectangle, getRoundRectangleRadius(rectangle)); +@@ -4544,7 +4543,7 @@ const RoundCommentEngine = { + const point9 = [x1 + rectangle.width / 4, y2]; + const point10 = [x1 + rectangle.width / 4, rectangle.y + rectangle.height]; + const point11 = [x1 + rectangle.width / 2, y2]; +- const shape = rs.path(`M${point2[0]} ${point2[1]} A ${radius} ${radius}, 0, 0, 1, ${point3[0]} ${point3[1]} L ${point4[0]} ${point4[1]} A ${radius} ${radius}, 0, 0, 1, ${point5[0]} ${point5[1]} L ${point11[0]} ${point11[1]} ${point10[0]} ${point10[1]} ${point9[0]} ${point9[1]} ${point6[0]} ${point6[1]} A ${radius} ${radius}, 0, 0, 1, ${point7[0]} ${point7[1]} L ${point8[0]} ${point8[1]} A ${radius} ${radius}, 0, 0, 1, ${point1[0]} ${point1[1]} Z`, { ...options, fillStyle: 'solid' }); ++ const shape = rs.path(`M${point2[0]} ${point2[1]} A ${radius} ${radius}, 0, 0, 1, ${point3[0]} ${point3[1]} L ${point4[0]} ${point4[1]} A ${radius} ${radius}, 0, 0, 1, ${point5[0]} ${point5[1]} L ${point11[0]} ${point11[1]} ${point10[0]} ${point10[1]} ${point9[0]} ${point9[1]} ${point6[0]} ${point6[1]} A ${radius} ${radius}, 0, 0, 1, ${point7[0]} ${point7[1]} L ${point8[0]} ${point8[1]} A ${radius} ${radius}, 0, 0, 1, ${point1[0]} ${point1[1]} Z`, options); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -4696,7 +4695,7 @@ const StarEngine = createPolygonEngine({ + + const TerminalEngine = { + draw(board, rectangle, options) { +- return drawRoundRectangle(PlaitBoard.getRoughSVG(board), rectangle.x, rectangle.y, rectangle.x + rectangle.width, rectangle.y + rectangle.height, { ...options, fillStyle: 'solid' }, false, getStartEndRadius(rectangle)); ++ return drawRoundRectangle(PlaitBoard.getRoughSVG(board), rectangle.x, rectangle.y, rectangle.x + rectangle.width, rectangle.y + rectangle.height, options, false, getStartEndRadius(rectangle)); + }, + isInsidePoint(rectangle, point) { + return isPointInRoundRectangle(point, rectangle, getStartEndRadius(rectangle)); +@@ -4865,7 +4864,7 @@ const DelayEngine = { + draw(board, rectangle, options) { + const rs = PlaitBoard.getRoughSVG(board); + const shape = rs.path(`M${rectangle.x} ${rectangle.y} L${rectangle.x + (rectangle.width * 3) / 4} ${rectangle.y} A ${rectangle.width / +- 4} ${rectangle.height / 2}, 0, 0, 1,${rectangle.x + (rectangle.width * 3) / 4} ${rectangle.y + rectangle.height} L${rectangle.x} ${rectangle.y + rectangle.height} Z`, { ...options, fillStyle: 'solid' }); ++ 4} ${rectangle.height / 2}, 0, 0, 1,${rectangle.x + (rectangle.width * 3) / 4} ${rectangle.y + rectangle.height} L${rectangle.x} ${rectangle.y + rectangle.height} Z`, options); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -4906,7 +4905,7 @@ const DelayEngine = { + const StoredDataEngine = { + draw(board, rectangle, options) { + const rs = PlaitBoard.getRoughSVG(board); +- const shape = rs.path(`M${rectangle.x + rectangle.width / 10} ${rectangle.y} L${rectangle.x + rectangle.width} ${rectangle.y} A ${rectangle.width / 10} ${rectangle.height / 2}, 0, 0, 0,${rectangle.x + rectangle.width} ${rectangle.y + rectangle.height} L${rectangle.x + rectangle.width / 10} ${rectangle.y + rectangle.height}A ${rectangle.width / 10} ${rectangle.height / 2}, 0, 0, 1,${rectangle.x + rectangle.width / 10} ${rectangle.y}`, { ...options, fillStyle: 'solid' }); ++ const shape = rs.path(`M${rectangle.x + rectangle.width / 10} ${rectangle.y} L${rectangle.x + rectangle.width} ${rectangle.y} A ${rectangle.width / 10} ${rectangle.height / 2}, 0, 0, 0,${rectangle.x + rectangle.width} ${rectangle.y + rectangle.height} L${rectangle.x + rectangle.width / 10} ${rectangle.y + rectangle.height}A ${rectangle.width / 10} ${rectangle.height / 2}, 0, 0, 1,${rectangle.x + rectangle.width / 10} ${rectangle.y}`, options); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -4979,7 +4978,7 @@ const PredefinedProcessEngine = { + const shape = rs.path(`M${rectangle.x} ${rectangle.y} H${rectangle.x + rectangle.width} V${rectangle.y + rectangle.height} H${rectangle.x} Z M${rectangle.x + rectangle.width * 0.06} ${rectangle.y} L${rectangle.x + rectangle.width * 0.06} ${rectangle.y + + rectangle.height} M${rectangle.x + rectangle.width - rectangle.width * 0.06} ${rectangle.y} L${rectangle.x + + rectangle.width - +- rectangle.width * 0.06} ${rectangle.y + rectangle.height}`, { ...options, fillStyle: 'solid' }); ++ rectangle.width * 0.06} ${rectangle.y + rectangle.height}`, options); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -5057,7 +5056,7 @@ const OrEngine = createEllipseEngine({ + L${rectangle.x + rectangle.width} ${rectangle.y + rectangle.height / 2} + M${rectangle.x + rectangle.width / 2} ${rectangle.y} + L${rectangle.x + rectangle.width / 2} ${rectangle.y + rectangle.height} +- `, { ...options, fillStyle: 'solid' }); ++ `, options); + setStrokeLinecap(shape, 'round'); + return shape; + } +@@ -5078,7 +5077,7 @@ const SummingJunctionEngine = createEllipseEngine({ + L${line1Points[1][0]} ${line1Points[1][1]} + M${line2Points[0][0]} ${line2Points[0][1]} + L${line2Points[1][0]} ${line2Points[1][1]} +- `, { ...options, fillStyle: 'solid' }); ++ `, options); + setStrokeLinecap(shape, 'round'); + return shape; + } +@@ -5094,7 +5093,7 @@ const DocumentEngine = { + (rectangle.height / 9) * 3}, ${rectangle.x + rectangle.width / 2} ${rectangle.y + + rectangle.height - + rectangle.height / 9} T${rectangle.x} ${rectangle.y + rectangle.height - rectangle.height / 9} +- `, { ...options, fillStyle: 'solid' }); ++ `, options); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -5185,7 +5184,7 @@ const MultiDocumentEngine = { + M${rectangle.x + 5} ${rectangle.y + 10} H${rectangle.x + rectangle.width - 10} V${rectangle.y + rectangle.height - rectangle.height / 9} + + M${rectangle.x + 10} ${rectangle.y + 5} H${rectangle.x + rectangle.width - 5} V${rectangle.y + rectangle.height - rectangle.height / 9 - 10 - 3} +- `, { ...options, fillStyle: 'solid' }); ++ `, options); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -5261,7 +5260,7 @@ const DatabaseEngine = { + A${rectangle.width / 2} ${rectangle.height * 0.15}, 0, 0, 0,${rectangle.x} ${rectangle.y + rectangle.height * 0.15} + V${rectangle.y + rectangle.height - rectangle.height * 0.15} + A${rectangle.width / 2} ${rectangle.height * 0.15}, 0, 0, 0, ${rectangle.x + rectangle.width} ${rectangle.y + rectangle.height - rectangle.height * 0.15} +- V${rectangle.y + rectangle.height * 0.15}`, { ...options, fillStyle: 'solid' }); ++ V${rectangle.y + rectangle.height * 0.15}`, options); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -5338,7 +5337,7 @@ const HardDiskEngine = { + H${rectangle.x + rectangle.width * 0.15} + A${rectangle.width * 0.15} ${rectangle.height / 2}, 0, 0, 0, ${rectangle.x + rectangle.width * 0.15} ${rectangle.y + + rectangle.height} +- H${rectangle.x + rectangle.width - rectangle.width * 0.15}`, { ...options, fillStyle: 'solid' }); ++ H${rectangle.x + rectangle.width - rectangle.width * 0.15}`, options); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -5414,7 +5413,7 @@ const InternalStorageEngine = { + const shape = rs.path(`M${rectangle.x} ${rectangle.y} h${rectangle.width} v${rectangle.height} h${-rectangle.width} v${-rectangle.height} + M${rectangle.x} ${rectangle.y + rectangle.height / 10} h${rectangle.width} + M${rectangle.x + rectangle.width / 10} ${rectangle.y} v${rectangle.height} +- `, { ...options, fillStyle: 'solid' }); ++ `, options); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -5482,7 +5481,7 @@ const NoteCurlyLeftEngine = { + ${lowerCurve.controlPoint2[0]} ${lowerCurve.controlPoint2[1]}, + ${lowerCurve.endPoint[0]} ${lowerCurve.endPoint[1]}` + ].join(' '); +- const shape = rs.path(pathData, { ...options, fillStyle: 'solid', fill: 'transparent' }); ++ const shape = rs.path(pathData, { ...options, fill: 'transparent' }); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -5555,7 +5554,7 @@ const NoteCurlyRightEngine = { + ${lowerCurve.controlPoint2[0]} ${lowerCurve.controlPoint2[1]}, + ${lowerCurve.endPoint[0]} ${lowerCurve.endPoint[1]}` + ].join(' '); +- const shape = rs.path(pathData, { ...options, fillStyle: 'solid', fill: 'transparent' }); ++ const shape = rs.path(pathData, { ...options, fill: 'transparent' }); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -5608,7 +5607,7 @@ const NoteSquareEngine = { + const rs = PlaitBoard.getRoughSVG(board); + const shape = rs.path(`M${rectangle.x + rectangle.width * 0.075} ${rectangle.y + rectangle.height} H${rectangle.x} V${rectangle.y} H${rectangle.x + + rectangle.width * 0.075} +- `, { ...options, fillStyle: 'solid', fill: 'transparent' }); ++ `, { ...options, fill: 'transparent' }); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -5655,7 +5654,7 @@ const DisplayEngine = { + H${rectangle.x + rectangle.width * 0.15} + L${rectangle.x} ${rectangle.y + rectangle.height / 2} + Z +- `, { ...options, fillStyle: 'solid' }); ++ `, options); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -5717,7 +5716,7 @@ const TableEngine = { + y: y + ACTIVE_STROKE_WIDTH, + width: width - ACTIVE_STROKE_WIDTH * 2, + height: height - ACTIVE_STROKE_WIDTH * 2 +- }, { fill: cell.fill, fillStyle: 'solid', strokeWidth: 0 }); ++ }, { fill: cell.fill, fillStyle: roughOptions?.fillStyle, strokeWidth: 0 }); + const cellRightBorder = drawLine(rs, [x + width, y], [x + width, y + height], roughOptions); + const cellBottomBorder = drawLine(rs, [x, y + height], [x + width, y + height], roughOptions); + g.append(cellRectangle, cellRightBorder, cellBottomBorder); +@@ -5862,7 +5861,7 @@ const ActorEngine = { + `M${leftLegLine[0][0]} ${leftLegLine[0][1]} L${leftLegLine[1][0]} ${leftLegLine[1][1]}`, + `M${rightLegLine[0][0]} ${rightLegLine[0][1]} L${rightLegLine[1][0]} ${rightLegLine[1][1]}` + ].join(' '); +- const shape = rs.path(pathData, { ...options, fillStyle: 'solid' }); ++ const shape = rs.path(pathData, options); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -5934,7 +5933,7 @@ const ActorEngine = { + const ContainerEngine = { + draw(board, rectangle, options) { + const rs = PlaitBoard.getRoughSVG(board); +- const shape = rs.path(`M${rectangle.x} ${rectangle.y} H${rectangle.x + rectangle.width} V${rectangle.y + rectangle.height} H${rectangle.x} Z M${rectangle.x + 40} ${rectangle.y} L${rectangle.x + 40} ${rectangle.y + rectangle.height} `, { ...options, fillStyle: 'solid' }); ++ const shape = rs.path(`M${rectangle.x} ${rectangle.y} H${rectangle.x + rectangle.width} V${rectangle.y + rectangle.height} H${rectangle.x} Z M${rectangle.x + 40} ${rectangle.y} L${rectangle.x + 40} ${rectangle.y + rectangle.height} `, options); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -6005,7 +6004,7 @@ const PackageEngine = { + `V${points.leftMiddle[1]}`, + `H${points.middlePoint[0]}` + ].join(' '); +- const shape = rs.path(pathData, { ...options, fillStyle: 'solid' }); ++ const shape = rs.path(pathData, options); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -6085,7 +6084,7 @@ const CombinedFragmentEngine = { + H${rectangle.x + rectangle.width / 3 - 8} + L${rectangle.x + rectangle.width / 3} ${rectangle.y + 16} + V${rectangle.y} +- `, { ...options, fillStyle: 'solid' }); ++ `, options); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -6151,7 +6150,7 @@ const DeletionEngine = { + draw(board, rectangle, options) { + const rs = PlaitBoard.getRoughSVG(board); + const lines = getDeletionLines(rectangle); +- const shape = rs.path(lines.map(([from, to]) => `M${from[0]} ${from[1]} L${to[0]} ${to[1]}`).join(' '), { ...options, fillStyle: 'solid', strokeWidth: 4 }); ++ const shape = rs.path(lines.map(([from, to]) => `M${from[0]} ${from[1]} L${to[0]} ${to[1]}`).join(' '), { ...options, strokeWidth: 4 }); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -6192,7 +6191,7 @@ const ActiveClassEngine = { + const shape = rs.path(`M${rectangle.x} ${rectangle.y} H${rectangle.x + rectangle.width} V${rectangle.y + rectangle.height} H${rectangle.x} Z M${rectangle.x + rectangle.width * 0.125} ${rectangle.y} L${rectangle.x + rectangle.width * 0.125} ${rectangle.y + + rectangle.height} M${rectangle.x + rectangle.width - rectangle.width * 0.125} ${rectangle.y} L${rectangle.x + + rectangle.width - +- rectangle.width * 0.125} ${rectangle.y + rectangle.height}`, { ...options, fillStyle: 'solid' }); ++ rectangle.width * 0.125} ${rectangle.y + rectangle.height}`, options); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -6241,7 +6240,7 @@ const NoteEngine = { + Z + M${rectangle.x + rectangle.width - 16} ${rectangle.y} + A16 16, 0,0,1, ${rectangle.x + rectangle.width} ${rectangle.y + 16} +- `, { ...options, fillStyle: 'solid' }); ++ `, options); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -6337,10 +6336,7 @@ const AssemblyEngine = { + // 最后一条线 + `M${line2[0][0]} ${line2[0][1]} H${line2[1][0]}` + ].join(' '); +- const shape = rs.path(pathData, { +- ...options, +- fillStyle: 'solid' +- }); ++ const shape = rs.path(pathData, options); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -6420,7 +6416,6 @@ const RequiredInterfaceEngine = { + ].join(' '); + const shape = rs.path(pathData, { + ...options, +- fillStyle: 'solid', + fill: 'transparent' + }); + setStrokeLinecap(shape, 'round'); +@@ -6511,10 +6506,7 @@ const ProvidedInterfaceEngine = { + `H${line.endX}`, + ...arcCommands.map((command) => `A${command.rx} ${command.ry} ${command.xAxisRotation} ${command.largeArcFlag} ${command.sweepFlag} ${command.endX} ${command.endY}`) + ].join(' '); +- const shape = rs.path(pathData, { +- ...options, +- fillStyle: 'solid' +- }); ++ const shape = rs.path(pathData, options); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -6602,7 +6594,7 @@ const ComponentEngine = { + `M${points.bottomBoxEnd[0]} ${points.bottomBoxEnd[1]}`, + `V${points.mainEnd[1]}` + ].join(' '); +- const shape = rs.path(pathData, { ...options, fillStyle: 'solid' }); ++ const shape = rs.path(pathData, options); + setStrokeLinecap(shape, 'round'); + return shape; + }, +@@ -6685,7 +6677,7 @@ const ComponentBoxEngine = { + V${rectangle.y + rectangle.height} + H${rectangle.x} Z + +- `, { ...options, fillStyle: 'solid' }); ++ `, options); + const componentShape = ComponentEngine.draw(board, componentRectangle, options); + shape.append(componentShape); + setStrokeLinecap(shape, 'round'); +@@ -6730,7 +6722,6 @@ const TemplateEngine = { + const rs = PlaitBoard.getRoughSVG(board); + return drawRoundRectangle(rs, rectangle.x, rectangle.y, rectangle.x + rectangle.width, rectangle.y + rectangle.height, { + ...options, +- fillStyle: 'solid', + dashGap: 10, + strokeLineDash: [10, 10] + }, false, 4); +@@ -8041,8 +8032,7 @@ const withArrowLineBoundReaction = (board) => { + const circleG = drawCircle(PlaitBoard.getRoughSVG(board), ref.connectorPoint || ref.edgePoint, 6, { + stroke: SELECTION_BORDER_COLOR, + strokeWidth: SNAPPING_STROKE_WIDTH, +- fill: SELECTION_BORDER_COLOR, +- fillStyle: 'solid' ++ fill: SELECTION_BORDER_COLOR + }); + boundShapeG.appendChild(circleG); + } +@@ -8310,8 +8300,7 @@ const withArrowLineAutoCompleteReaction = (board) => { + // function 1: dnd + reactionG = drawCircle(PlaitBoard.getRoughSVG(board), hitPoint, LINE_AUTO_COMPLETE_HOVERED_DIAMETER, { + stroke: 'none', +- fill: rgbaToHEX(PRIMARY_COLOR, LINE_AUTO_COMPLETE_HOVERED_OPACITY), +- fillStyle: 'solid' ++ fill: rgbaToHEX(PRIMARY_COLOR, LINE_AUTO_COMPLETE_HOVERED_OPACITY) + }); + PlaitBoard.getActiveHost(board).append(reactionG); + PlaitBoard.getBoardContainer(board).classList.add(CursorClass.crosshair); +diff --git a/node_modules/@plait/draw/fesm2022/plait-draw.mjs.orig b/fesm2022/plait-draw.mjs.orig +new file mode 100644 +index 0000000000000000000000000000000000000000..c66478e071aa029f80ab3ca12a09ca5836612d65 +--- /dev/null ++++ b/fesm2022/plait-draw.mjs.orig +@@ -0,0 +1,9328 @@ ++import { ACTIVE_STROKE_WIDTH, DEFAULT_COLOR, ThemeColorMode, PlaitElement, RectangleClient, getSelectedElements, idCreator, createDebugGenerator, Point, createG, arrowPoints, createPath, distanceBetweenPointAndPoint, drawLinearPath, rotate, catmullRomFitting, PlaitBoard, setStrokeLinecap, findElements, createMask, createRect, getNearestPointBetweenPointAndArc, setPathStrokeLinecap, distanceBetweenPointAndSegments, HIT_DISTANCE_BUFFER, isPointInPolygon, isLineHitRectangle, rotatePointsByAngle, rotateAntiPointsByElement, getEllipseArcCenter, Transforms, clearSelectedElement, addSelectedElement, BoardTransforms, PlaitPointerType, depthFirstRecursion, getIsRecursionFunc, SNAPPING_STROKE_WIDTH, SELECTION_BORDER_COLOR, SELECTION_FILL_COLOR, drawCircle, getI18nValue, SELECTION_RECTANGLE_CLASS_NAME, toActivePointFromViewBoxPoint, toActiveRectangleFromViewBoxRectangle, drawRectangle, isSelectionMoving, rgbaToHEX, getElementById, rotatePointsByElement, PlaitNode, hasValidAngle, toViewBoxPoint, toHostPoint, Direction, rotatePoints, getSelectionAngle, rotatedDataPoints, isAxisChangedByAngle, getRectangleByElements, getRectangleByAngle, getSnapRectangles, getTripleAxis, getMinPointDelta, SNAP_TOLERANCE, drawPointSnapLines, drawSolidLines, getNearestPointBetweenPointAndSegments, getEllipseTangentSlope, getVectorFromPointAndSlope, getNearestPointBetweenPointAndEllipse, isPointInEllipse, isPointInRoundRectangle, drawRoundRectangle, getCrossingPointsBetweenEllipseAndSegment, drawLine, getNearestPointBetweenPointAndDiscreteSegments, getNearestPointBetweenPointAndSegment, Path, getHitElementByPoint, WritableClipboardOperationType, WritableClipboardType, addOrCreateClipboardContext, setAngleForG, toActivePoint, temporaryDisableSelection, toScreenPointFromActivePoint, PRESS_AND_MOVE_BUFFER, CursorClass, isHorizontalDirection, isMainPointer, throttleRAF, getAngleBetweenPoints, normalizeAngle, degreesToRadians, rotateElements, MERGING, ROTATE_HANDLE_CLASS_NAME, isSelectedElement, isDragging } from '@plait/core'; ++import { DEFAULT_FILL, Alignment, WithTextPluginKey, TextManage, getMemorizedLatest, memorizeLatest, removeDuplicatePoints, generateElbowLineRoute, simplifyOrthogonalPoints, getExtendPoint, getUnitVectorByPointAndPoint, Generator, getElementSize, DEFAULT_FONT_FAMILY, getPointByVectorComponent, getStrokeLineDash, StrokeStyle, getPointOnPolyline, buildText, getCrossingPointsBetweenPointAndSegment, isPointOnSegment, getFirstTextEditor, sortElementsByArea, isFilled, getTextEditorsByElement, RESIZE_HANDLE_DIAMETER, drawPrimaryHandle, drawFillPrimaryHandle, PRIMARY_COLOR, measureElement, getFirstTextManage, isSourceAndTargetIntersect, getPoints, DEFAULT_ROUTE_MARGIN, normalizeShapePoints, resetPointsAfterResize, getDirectionByVector, getRectangleResizeHandleRefs, getRotatedResizeCursorClassByAngle, ROTATE_HANDLE_SIZE, ROTATE_HANDLE_DISTANCE_TO_ELEMENT, isCornerHandle, getIndexByResizeHandle, withResize, getSymmetricHandleIndex, getResizeHandlePointByIndex, drawHandle, getDirectionFactorByDirectionComponent, buildClipboardData as buildClipboardData$1, insertClipboardData as insertClipboardData$1, getDirectionByPointOfRectangle, getDirectionFactor, rotateVector, getOppositeDirection, rotateVectorAnti90, getSourceAndTargetOuterRectangle, getNextPoint, CommonElementFlavour, createActiveGenerator, hasResizeHandle, ActiveGenerator, isVirtualKey, isDelete, isSpaceHotkey, isDndMode, isDrawingMode, getElementsText, acceptImageTypes, getElementOfFocusedImage, buildImage, isResizingByCondition, getRatioByPoint, getTextManages, ImageGenerator, getDirectionByIndex, moveXOfPoint, getXDistanceBetweenPoint, ResizeHandle, addRotating, removeRotating, drawRotateHandle } from '@plait/common'; ++import { pointsOnBezierCurves } from 'points-on-curve'; ++import { DEFAULT_FONT_SIZE, AlignEditor } from '@plait/text-plugins'; ++import { Editor, Node } from 'slate'; ++import { isKeyHotkey } from 'is-hotkey'; ++ ++var BasicShapes; ++(function (BasicShapes) { ++ BasicShapes["rectangle"] = "rectangle"; ++ BasicShapes["ellipse"] = "ellipse"; ++ BasicShapes["diamond"] = "diamond"; ++ BasicShapes["roundRectangle"] = "roundRectangle"; ++ BasicShapes["parallelogram"] = "parallelogram"; ++ BasicShapes["text"] = "text"; ++ BasicShapes["triangle"] = "triangle"; ++ BasicShapes["leftArrow"] = "leftArrow"; ++ BasicShapes["trapezoid"] = "trapezoid"; ++ BasicShapes["rightArrow"] = "rightArrow"; ++ BasicShapes["cross"] = "cross"; ++ BasicShapes["star"] = "star"; ++ BasicShapes["pentagon"] = "pentagon"; ++ BasicShapes["hexagon"] = "hexagon"; ++ BasicShapes["octagon"] = "octagon"; ++ BasicShapes["pentagonArrow"] = "pentagonArrow"; ++ BasicShapes["processArrow"] = "processArrow"; ++ BasicShapes["twoWayArrow"] = "twoWayArrow"; ++ BasicShapes["comment"] = "comment"; ++ BasicShapes["roundComment"] = "roundComment"; ++ BasicShapes["cloud"] = "cloud"; ++})(BasicShapes || (BasicShapes = {})); ++var FlowchartSymbols; ++(function (FlowchartSymbols) { ++ FlowchartSymbols["process"] = "process"; ++ FlowchartSymbols["decision"] = "decision"; ++ FlowchartSymbols["data"] = "data"; ++ FlowchartSymbols["connector"] = "connector"; ++ FlowchartSymbols["terminal"] = "terminal"; ++ FlowchartSymbols["manualInput"] = "manualInput"; ++ FlowchartSymbols["preparation"] = "preparation"; ++ FlowchartSymbols["manualLoop"] = "manualLoop"; ++ FlowchartSymbols["merge"] = "merge"; ++ FlowchartSymbols["delay"] = "delay"; ++ FlowchartSymbols["storedData"] = "storedData"; ++ FlowchartSymbols["or"] = "or"; ++ FlowchartSymbols["summingJunction"] = "summingJunction"; ++ FlowchartSymbols["predefinedProcess"] = "predefinedProcess"; ++ FlowchartSymbols["offPage"] = "offPage"; ++ FlowchartSymbols["document"] = "document"; ++ FlowchartSymbols["multiDocument"] = "multiDocument"; ++ FlowchartSymbols["database"] = "database"; ++ FlowchartSymbols["hardDisk"] = "hardDisk"; ++ FlowchartSymbols["internalStorage"] = "internalStorage"; ++ FlowchartSymbols["noteCurlyRight"] = "noteCurlyRight"; ++ FlowchartSymbols["noteCurlyLeft"] = "noteCurlyLeft"; ++ FlowchartSymbols["noteSquare"] = "noteSquare"; ++ FlowchartSymbols["display"] = "display"; ++})(FlowchartSymbols || (FlowchartSymbols = {})); ++var UMLSymbols; ++(function (UMLSymbols) { ++ UMLSymbols["actor"] = "actor"; ++ UMLSymbols["useCase"] = "useCase"; ++ UMLSymbols["container"] = "container"; ++ UMLSymbols["note"] = "note"; ++ UMLSymbols["simpleClass"] = "simpleClass"; ++ UMLSymbols["activityClass"] = "activityClass"; ++ UMLSymbols["branchMerge"] = "branchMerge"; ++ UMLSymbols["port"] = "port"; ++ UMLSymbols["package"] = "package"; ++ UMLSymbols["combinedFragment"] = "combinedFragment"; ++ UMLSymbols["class"] = "class"; ++ UMLSymbols["interface"] = "interface"; ++ UMLSymbols["object"] = "object"; ++ UMLSymbols["component"] = "component"; ++ UMLSymbols["componentBox"] = "componentBox"; ++ UMLSymbols["template"] = "template"; ++ UMLSymbols["activation"] = "activation"; ++ UMLSymbols["deletion"] = "deletion"; ++ UMLSymbols["assembly"] = "assembly"; ++ UMLSymbols["providedInterface"] = "providedInterface"; ++ UMLSymbols["requiredInterface"] = "requiredInterface"; ++})(UMLSymbols || (UMLSymbols = {})); ++var GeometryCommonTextKeys; ++(function (GeometryCommonTextKeys) { ++ GeometryCommonTextKeys["name"] = "name"; ++ GeometryCommonTextKeys["content"] = "content"; ++})(GeometryCommonTextKeys || (GeometryCommonTextKeys = {})); ++const PlaitGeometry = {}; ++ ++var SwimlaneSymbols; ++(function (SwimlaneSymbols) { ++ SwimlaneSymbols["swimlaneVertical"] = "swimlaneVertical"; ++ SwimlaneSymbols["swimlaneHorizontal"] = "swimlaneHorizontal"; ++})(SwimlaneSymbols || (SwimlaneSymbols = {})); ++var SwimlaneDrawSymbols; ++(function (SwimlaneDrawSymbols) { ++ SwimlaneDrawSymbols["swimlaneVertical"] = "swimlaneVertical"; ++ SwimlaneDrawSymbols["swimlaneHorizontal"] = "swimlaneHorizontal"; ++ SwimlaneDrawSymbols["swimlaneVerticalWithHeader"] = "swimlaneVerticalWithHeader"; ++ SwimlaneDrawSymbols["swimlaneHorizontalWithHeader"] = "swimlaneHorizontalWithHeader"; ++})(SwimlaneDrawSymbols || (SwimlaneDrawSymbols = {})); ++ ++var TableSymbols; ++(function (TableSymbols) { ++ TableSymbols["table"] = "table"; ++})(TableSymbols || (TableSymbols = {})); ++const PlaitTableElement = { ++ isTable: (value) => { ++ return value.type === 'table'; ++ }, ++ isVerticalText: (value) => { ++ return value.text?.direction === 'vertical'; ++ } ++}; ++ ++const WithDrawPluginKey = 'plait-draw-plugin-key'; ++var DrawI18nKey; ++(function (DrawI18nKey) { ++ DrawI18nKey["lineText"] = "line-text"; ++ DrawI18nKey["geometryText"] = "geometry-text"; ++})(DrawI18nKey || (DrawI18nKey = {})); ++ ++const ShapeDefaultSpace = { ++ rectangleAndText: 4 ++}; ++const DefaultDrawStyle = { ++ strokeWidth: 2, ++ defaultRadius: 4, ++ strokeColor: '#000', ++ fill: DEFAULT_FILL ++}; ++const DefaultDrawActiveStyle = { ++ strokeWidth: ACTIVE_STROKE_WIDTH, ++ selectionStrokeWidth: ACTIVE_STROKE_WIDTH ++}; ++const DefaultBasicShapeProperty = { ++ width: 100, ++ height: 100, ++ strokeColor: DEFAULT_COLOR, ++ strokeWidth: 2 ++}; ++const DefaultPentagonArrowProperty = { ++ width: 120, ++ height: 50 ++}; ++const DefaultTwoWayArrowProperty = { ++ width: 138, ++ height: 80 ++}; ++const DefaultArrowProperty = { ++ width: 100, ++ height: 80 ++}; ++const DefaultCloudProperty = { ++ width: 120, ++ height: 100 ++}; ++const DefaultTextProperty = { ++ width: 36, ++ height: 20, ++ text: '文本' ++}; ++const GeometryThreshold = { ++ defaultTextMaxWidth: 34 * 14 ++}; ++const DefaultConnectorProperty = { ++ width: 44, ++ height: 44 ++}; ++const DefaultFlowchartProperty = { ++ width: 120, ++ height: 60 ++}; ++const DefaultDataBaseProperty = { ++ width: 70, ++ height: 80 ++}; ++const DefaultInternalStorageProperty = { ++ width: 80, ++ height: 80 ++}; ++const DefaultDecisionProperty = { ++ width: 140, ++ height: 70 ++}; ++const DefaultDataProperty = { ++ width: 124, ++ height: 60 ++}; ++const DefaultDocumentProperty = { ++ width: 120, ++ height: 70 ++}; ++const DefaultNoteProperty = { ++ width: 160, ++ height: 100 ++}; ++const DefaultMultiDocumentProperty = { ++ width: 120, ++ height: 80 ++}; ++const DefaultManualInputProperty = { ++ width: 117, ++ height: 59 ++}; ++const DefaultMergeProperty = { ++ width: 47, ++ height: 33 ++}; ++const DefaultActorProperty = { ++ width: 68, ++ height: 100 ++}; ++const DefaultContainerProperty = { ++ width: 300, ++ height: 240 ++}; ++const DefaultPackageProperty = { ++ width: 210, ++ height: 150, ++ texts: [ ++ { ++ id: GeometryCommonTextKeys.name, ++ text: '包名', ++ align: Alignment.left ++ }, ++ { ++ id: GeometryCommonTextKeys.content, ++ text: '', ++ align: Alignment.left ++ } ++ ] ++}; ++const DefaultActivationProperty = { ++ width: 18, ++ height: 80 ++}; ++const DefaultObjectProperty = { ++ width: 120, ++ height: 60 ++}; ++const DefaultComponentBoxProperty = { ++ width: 200, ++ height: 150 ++}; ++const DefaultDeletionProperty = { ++ width: 40, ++ height: 40 ++}; ++const DefaultPortProperty = { ++ width: 20, ++ height: 20 ++}; ++const DefaultRequiredInterfaceProperty = { ++ width: 70, ++ height: 56 ++}; ++const DefaultAssemblyProperty = { ++ width: 120, ++ height: 56 ++}; ++const DefaultProvidedInterfaceProperty = { ++ width: 70, ++ height: 34 ++}; ++const DefaultCombinedFragmentProperty = { ++ width: 400, ++ height: 280, ++ texts: [ ++ { ++ id: GeometryCommonTextKeys.name, ++ text: 'Opt | Alt | Loop', ++ align: Alignment.left ++ }, ++ { ++ id: GeometryCommonTextKeys.content, ++ text: '[Condition]', ++ align: Alignment.left ++ } ++ ] ++}; ++const DefaultClassProperty = { ++ width: 230, ++ height: 180, ++ texts: [ ++ { text: 'Class', align: Alignment.center }, ++ { ++ text: '+ attribute1:type defaultValue\n+ attribute2:type\n- attribute3:type', ++ align: Alignment.left ++ }, ++ { ++ text: '+ operation1(params):returnType\n- operation2(params)\n- operation3()', ++ align: Alignment.left ++ } ++ ] ++}; ++const DefaultInterfaceProperty = { ++ width: 230, ++ height: 140, ++ texts: [ ++ { text: '<>\nInterface', align: Alignment.center }, ++ { ++ text: '+ operation1(params):returnType\n- operation2(params)\n- operation3()', ++ align: Alignment.left ++ } ++ ] ++}; ++const DefaultBasicShapePropertyMap = { ++ [BasicShapes.pentagonArrow]: DefaultPentagonArrowProperty, ++ [BasicShapes.processArrow]: DefaultPentagonArrowProperty, ++ [BasicShapes.cloud]: DefaultCloudProperty, ++ [BasicShapes.twoWayArrow]: DefaultTwoWayArrowProperty, ++ [BasicShapes.leftArrow]: DefaultArrowProperty, ++ [BasicShapes.rightArrow]: DefaultArrowProperty ++}; ++const DefaultFlowchartPropertyMap = { ++ [FlowchartSymbols.connector]: DefaultConnectorProperty, ++ [FlowchartSymbols.process]: DefaultFlowchartProperty, ++ [FlowchartSymbols.decision]: DefaultDecisionProperty, ++ [FlowchartSymbols.data]: DefaultDataProperty, ++ [FlowchartSymbols.terminal]: DefaultFlowchartProperty, ++ [FlowchartSymbols.manualInput]: DefaultManualInputProperty, ++ [FlowchartSymbols.preparation]: DefaultFlowchartProperty, ++ [FlowchartSymbols.manualLoop]: DefaultFlowchartProperty, ++ [FlowchartSymbols.merge]: DefaultMergeProperty, ++ [FlowchartSymbols.delay]: DefaultFlowchartProperty, ++ [FlowchartSymbols.storedData]: DefaultFlowchartProperty, ++ [FlowchartSymbols.or]: DefaultConnectorProperty, ++ [FlowchartSymbols.summingJunction]: DefaultConnectorProperty, ++ [FlowchartSymbols.predefinedProcess]: DefaultFlowchartProperty, ++ [FlowchartSymbols.offPage]: DefaultFlowchartProperty, ++ [FlowchartSymbols.document]: DefaultDocumentProperty, ++ [FlowchartSymbols.multiDocument]: DefaultMultiDocumentProperty, ++ [FlowchartSymbols.database]: DefaultDataBaseProperty, ++ [FlowchartSymbols.hardDisk]: DefaultFlowchartProperty, ++ [FlowchartSymbols.internalStorage]: DefaultInternalStorageProperty, ++ [FlowchartSymbols.noteCurlyLeft]: DefaultNoteProperty, ++ [FlowchartSymbols.noteCurlyRight]: DefaultNoteProperty, ++ [FlowchartSymbols.noteSquare]: DefaultNoteProperty, ++ [FlowchartSymbols.display]: DefaultFlowchartProperty ++}; ++const DefaultUMLPropertyMap = { ++ [UMLSymbols.actor]: DefaultActorProperty, ++ [UMLSymbols.useCase]: DefaultDocumentProperty, ++ [UMLSymbols.container]: DefaultContainerProperty, ++ [UMLSymbols.note]: DefaultObjectProperty, ++ [UMLSymbols.package]: DefaultPackageProperty, ++ [UMLSymbols.combinedFragment]: DefaultCombinedFragmentProperty, ++ [UMLSymbols.class]: DefaultClassProperty, ++ [UMLSymbols.interface]: DefaultInterfaceProperty, ++ [UMLSymbols.activation]: DefaultActivationProperty, ++ [UMLSymbols.object]: DefaultObjectProperty, ++ [UMLSymbols.deletion]: DefaultDeletionProperty, ++ [UMLSymbols.activityClass]: DefaultObjectProperty, ++ [UMLSymbols.simpleClass]: DefaultObjectProperty, ++ [UMLSymbols.component]: DefaultMultiDocumentProperty, ++ [UMLSymbols.template]: DefaultMultiDocumentProperty, ++ [UMLSymbols.componentBox]: DefaultComponentBoxProperty, ++ [UMLSymbols.port]: DefaultPortProperty, ++ [UMLSymbols.branchMerge]: DefaultDeletionProperty, ++ [UMLSymbols.assembly]: DefaultAssemblyProperty, ++ [UMLSymbols.providedInterface]: DefaultProvidedInterfaceProperty, ++ [UMLSymbols.requiredInterface]: DefaultRequiredInterfaceProperty ++}; ++const MultipleTextGeometryTextKeys = { ++ [UMLSymbols.package]: Object.keys(GeometryCommonTextKeys), ++ [UMLSymbols.combinedFragment]: Object.keys(GeometryCommonTextKeys) ++}; ++const LINE_HIT_GEOMETRY_BUFFER = 4; ++const LINE_SNAPPING_BUFFER = 4; ++const LINE_SNAPPING_CONNECTOR_BUFFER = 4; ++const LINE_ALIGN_TOLERANCE = 4; ++const GEOMETRY_WITHOUT_TEXT = [ ++ FlowchartSymbols.or, ++ FlowchartSymbols.summingJunction, ++ UMLSymbols.activation, ++ UMLSymbols.deletion, ++ UMLSymbols.port, ++ UMLSymbols.branchMerge, ++ UMLSymbols.assembly, ++ UMLSymbols.providedInterface, ++ UMLSymbols.requiredInterface ++]; ++const GEOMETRY_WITH_MULTIPLE_TEXT = [UMLSymbols.package, UMLSymbols.combinedFragment]; ++const GEOMETRY_NOT_CLOSED = [ ++ FlowchartSymbols.noteCurlyLeft, ++ FlowchartSymbols.noteCurlyRight, ++ FlowchartSymbols.noteSquare, ++ UMLSymbols.requiredInterface, ++ UMLSymbols.deletion ++]; ++ ++const getGeometryPointers = () => { ++ return [...Object.keys(BasicShapes), ...Object.keys(FlowchartSymbols), ...Object.keys(UMLSymbols)]; ++}; ++const getSwimlanePointers = () => { ++ return Object.keys(SwimlaneDrawSymbols); ++}; ++const getSwimlaneShapes = () => { ++ return Object.keys(SwimlaneSymbols); ++}; ++const getBasicPointers = () => { ++ return Object.keys(BasicShapes); ++}; ++const getFlowchartPointers = () => { ++ return Object.keys(FlowchartSymbols); ++}; ++const getUMLPointers = () => { ++ return Object.keys(UMLSymbols); ++}; ++const getArrowLinePointers = () => { ++ return Object.keys(ArrowLineShape); ++}; ++const getVectorLinePointers = () => { ++ return Object.keys(VectorLinePointerType); ++}; ++ ++const DEFAULT_IMAGE_WIDTH = 1000; ++ ++const DrawThemeColors = { ++ [ThemeColorMode.default]: { ++ strokeColor: DEFAULT_COLOR, ++ fill: '#FFFFFF' ++ }, ++ [ThemeColorMode.colorful]: { ++ strokeColor: '#06ADBF', ++ fill: '#CDEFF2' ++ }, ++ [ThemeColorMode.soft]: { ++ strokeColor: '#6D89C1', ++ fill: '#DADFEB' ++ }, ++ [ThemeColorMode.retro]: { ++ strokeColor: '#E9C358', ++ fill: '#F6EDCF' ++ }, ++ [ThemeColorMode.dark]: { ++ strokeColor: '#FFFFFF', ++ fill: '#434343' ++ }, ++ [ThemeColorMode.starry]: { ++ strokeColor: '#42ABE5', ++ fill: '#163F5A' ++ } ++}; ++ ++const SWIMLANE_HEADER_SIZE = 42; ++const DefaultSwimlaneVerticalWithHeaderProperty = { ++ width: 580, ++ height: 524 ++}; ++const DefaultSwimlaneHorizontalWithHeaderProperty = { ++ width: 524, ++ height: 580 ++}; ++const DefaultSwimlaneVerticalProperty = { ++ width: 580, ++ height: 524 ++}; ++const DefaultSwimlaneHorizontalProperty = { ++ width: 524, ++ height: 580 ++}; ++const DefaultSwimlanePropertyMap = { ++ [SwimlaneDrawSymbols.swimlaneHorizontal]: DefaultSwimlaneHorizontalProperty, ++ [SwimlaneDrawSymbols.swimlaneVertical]: DefaultSwimlaneVerticalProperty, ++ [SwimlaneDrawSymbols.swimlaneHorizontalWithHeader]: DefaultSwimlaneHorizontalWithHeaderProperty, ++ [SwimlaneDrawSymbols.swimlaneVerticalWithHeader]: DefaultSwimlaneVerticalWithHeaderProperty ++}; ++ ++const MIN_TEXT_WIDTH = 5; ++ ++const DefaultLineStyle = { ++ strokeWidth: 2, ++ strokeColor: '#000' ++}; ++const LINE_TEXT_SPACE = 4; ++const LINE_AUTO_COMPLETE_DIAMETER = 6; ++const LINE_AUTO_COMPLETE_OPACITY = 1; ++const LINE_AUTO_COMPLETE_HOVERED_OPACITY = 1; ++const LINE_AUTO_COMPLETE_HOVERED_DIAMETER = 12; ++const LINE_TEXT = '文本'; ++ ++// TODO: 是否可以完全基于位置定位 TextManager,实现 line 和 多文本 geometry 统一 ++// 一个元素有多个文本时,单纯通过位置无法获取 TextManage,因此这里单独通过 Map 保存关键字 key 和 TextManage 的对应关系 ++// 1. 单文本元素 key 就是元素的 id ++// 2. 表格元素 key 是单元格的 id ++// 3. 符合 isMultipleTextGeometry 的元素,key 是元素 id + text.id (通常不是 id 而是文本位置的常量) ++// 4. arrow-line 和 vector-line 文本不依赖于 text.generator,基于 text 可以直接找到 TextManage ++const KEY_TO_TEXT_MANAGE = new WeakMap(); ++const setTextManage = (board, element, text, textManage) => { ++ const textManages = KEY_TO_TEXT_MANAGE.get(board); ++ return KEY_TO_TEXT_MANAGE.set(board, { ...textManages, [getTextKey(element, text)]: textManage }); ++}; ++const getTextManage = (board, element, text) => { ++ const textManages = KEY_TO_TEXT_MANAGE.get(board); ++ return textManages[getTextKey(element, text)]; ++}; ++const deleteTextManage = (board, key) => { ++ const textManages = KEY_TO_TEXT_MANAGE.get(board); ++ delete textManages[key]; ++ KEY_TO_TEXT_MANAGE.set(board, textManages); ++}; ++class TextGenerator { ++ get shape() { ++ return this.element.shape || this.element.type; ++ } ++ constructor(board, element, texts, options) { ++ this.board = board; ++ this.texts = texts; ++ this.element = element; ++ this.options = options; ++ } ++ initialize() { ++ const textPlugins = (this.board.getPluginOptions(WithTextPluginKey) || {}) ++ .textPlugins; ++ this.textManages = this.texts.map(text => { ++ const textManage = this.createTextManage(text, textPlugins); ++ setTextManage(this.board, this.element, text, textManage); ++ return textManage; ++ }); ++ const ref = PlaitElement.getElementRef(this.element); ++ ref.initializeTextManage(this.textManages); ++ } ++ draw(elementG) { ++ const centerPoint = RectangleClient.getCenterPoint(this.board.getRectangle(this.element)); ++ this.texts.forEach(drawShapeText => { ++ const textManage = getTextManage(this.board, this.element, drawShapeText); ++ if (drawShapeText.text && textManage) { ++ textManage.draw(drawShapeText.text); ++ elementG.append(textManage.g); ++ (this.element.angle || this.element.angle === 0) && textManage.updateAngle(centerPoint, this.element.angle); ++ } ++ }); ++ } ++ update(element, previousDrawShapeTexts, currentDrawShapeTexts, elementG) { ++ this.element = element; ++ const centerPoint = RectangleClient.getCenterPoint(this.board.getRectangle(this.element)); ++ const textPlugins = (this.board.getPluginOptions(WithTextPluginKey) || {}) ++ .textPlugins; ++ const removedTexts = previousDrawShapeTexts.filter(value => { ++ return !currentDrawShapeTexts.find(item => item.id === value.id); ++ }); ++ if (removedTexts.length) { ++ removedTexts.forEach(item => { ++ const textManage = getTextManage(this.board, element, item); ++ const index = this.textManages.findIndex(value => value === textManage); ++ if (index > -1 && item.text) { ++ this.textManages.splice(index, 1); ++ } ++ textManage?.destroy(); ++ deleteTextManage(this.board, item.id); ++ }); ++ } ++ currentDrawShapeTexts.forEach(drawShapeText => { ++ if (drawShapeText.text) { ++ let textManage = getTextManage(this.board, this.element, drawShapeText); ++ if (!textManage) { ++ textManage = this.createTextManage(drawShapeText, textPlugins); ++ setTextManage(this.board, element, drawShapeText, textManage); ++ textManage.draw(drawShapeText.text); ++ elementG.append(textManage.g); ++ this.textManages.push(textManage); ++ } ++ else { ++ textManage.updateText(drawShapeText.text); ++ textManage.updateRectangle(); ++ } ++ (this.element.angle || this.element.angle === 0) && textManage.updateAngle(centerPoint, this.element.angle); ++ } ++ }); ++ } ++ createTextManage(text, textPlugins) { ++ const textManage = new TextManage(this.board, { ++ getRectangle: () => { ++ return this.getRectangle(text); ++ }, ++ onChange: (data) => { ++ return this.options.onChange(this.element, data, text); ++ }, ++ getMaxWidth: () => { ++ return this.getMaxWidth(text); ++ }, ++ getRenderRectangle: () => { ++ return this.options.getRenderRectangle ? this.options.getRenderRectangle(this.element, text) : this.getRectangle(text); ++ }, ++ textPlugins ++ }); ++ return textManage; ++ } ++ getRectangle(text) { ++ const getRectangle = getEngine(this.shape).getTextRectangle; ++ if (getRectangle) { ++ return getRectangle(this.board, this.element, text); ++ } ++ return getTextRectangle$1(this.board, this.element); ++ } ++ getMaxWidth(text) { ++ return this.options.getMaxWidth ? this.options.getMaxWidth() : this.getRectangle(text).width; ++ } ++ destroy() { ++ const ref = PlaitElement.getElementRef(this.element); ++ ref.destroyTextManage(); ++ this.textManages = []; ++ this.texts.forEach(item => { ++ deleteTextManage(this.board, item.id); ++ }); ++ } ++} ++ ++const isSingleSelectTable = (board) => { ++ const selectedElements = getSelectedElements(board); ++ return selectedElements && selectedElements.length === 1 && PlaitDrawElement.isElementByTable(selectedElements[0]); ++}; ++const getSelectedTableElements = (board, elements) => { ++ const selectedElements = elements?.length ? elements : getSelectedElements(board); ++ return selectedElements.filter(value => PlaitDrawElement.isElementByTable(value)); ++}; ++const SELECTED_CELLS = new WeakMap(); ++function getSelectedCells(element) { ++ return SELECTED_CELLS.get(element); ++} ++function setSelectedCells(element, cells) { ++ return SELECTED_CELLS.set(element, cells); ++} ++function clearSelectedCells(element) { ++ return SELECTED_CELLS.delete(element); ++} ++ ++function getCellsWithPoints(board, element) { ++ const table = board?.buildTable(element); ++ if (!table || !table.points || !table.columns || !table.rows) { ++ throw new Error('can not get table cells points'); ++ } ++ const rectangle = RectangleClient.getRectangleByPoints(table.points); ++ const columnsCount = table.columns.length; ++ const rowsCount = table.rows.length; ++ const cellWidths = calculateCellsSize(table.columns, rectangle.width, columnsCount, true); ++ const cellHeights = calculateCellsSize(table.rows, rectangle.height, rowsCount, false); ++ const cells = table.cells.map(cell => { ++ const rowIdx = table.rows.findIndex(row => row.id === cell.rowId); ++ const columnIdx = table.columns.findIndex(column => column.id === cell.columnId); ++ let cellTopLeftX = rectangle.x; ++ for (let i = 0; i < columnIdx; i++) { ++ cellTopLeftX += cellWidths[i]; ++ } ++ let cellTopLeftY = rectangle.y; ++ for (let i = 0; i < rowIdx; i++) { ++ cellTopLeftY += cellHeights[i]; ++ } ++ const cellWidth = calculateCellSize(cell, cellWidths, columnIdx, true); ++ const cellBottomRightX = cellTopLeftX + cellWidth; ++ const cellHeight = calculateCellSize(cell, cellHeights, rowIdx, false); ++ const cellBottomRightY = cellTopLeftY + cellHeight; ++ return { ++ ...cell, ++ points: [ ++ [cellTopLeftX, cellTopLeftY], ++ [cellBottomRightX, cellBottomRightY] ++ ] ++ }; ++ }); ++ return cells; ++} ++function getCellWithPoints(board, table, cellId) { ++ try { ++ const cells = getCellsWithPoints(board, table); ++ const cellIndex = cells && table.cells.findIndex(item => item.id === cellId); ++ return cells[cellIndex]; ++ } ++ catch (error) { ++ throw new Error('can not get table cell points'); ++ } ++} ++function calculateCellsSize(items, tableSize, count, isWidth) { ++ const cellSizes = []; ++ const sizeType = isWidth ? 'width' : 'height'; ++ // The remaining size of the table excluding cells with already set sizes. ++ let totalSizeRemaining = tableSize; ++ items.forEach((item, index) => { ++ if (item[sizeType]) { ++ cellSizes[index] = item[sizeType]; ++ totalSizeRemaining -= item[sizeType]; ++ } ++ }); ++ // Divide the remaining size equally. ++ const remainingItemCount = count - cellSizes.filter(item => !!item).length; ++ const remainingCellSize = remainingItemCount > 0 ? totalSizeRemaining / remainingItemCount : 0; ++ for (let i = 0; i < count; i++) { ++ if (!cellSizes[i]) { ++ cellSizes[i] = remainingCellSize; ++ } ++ } ++ return cellSizes; ++} ++function calculateCellSize(cell, sizes, index, isWidth) { ++ const span = isWidth ? cell.colspan || 1 : cell.rowspan || 1; ++ let size = 0; ++ for (let i = 0; i < span; i++) { ++ const cellIndex = index + i; ++ size += sizes[cellIndex]; ++ } ++ return size; ++} ++function getHitCell(board, element, point) { ++ const table = board.buildTable(element); ++ const cells = getCellsWithPoints(board, table); ++ const rectangle = RectangleClient.getRectangleByPoints([point, point]); ++ const cell = cells.find(item => { ++ const cellRectangle = RectangleClient.getRectangleByPoints(item.points); ++ return RectangleClient.isHit(rectangle, cellRectangle); ++ }); ++ if (cell) { ++ return table.cells.find(item => item.id === cell.id); ++ } ++ return null; ++} ++function editCell(board, cell) { ++ const textManage = getTextManageByCell(board, cell); ++ textManage && textManage.edit(); ++} ++function getTextManageByCell(board, cell) { ++ return getTextManage(board, undefined, cell); ++} ++const updateColumns = (table, columnId, width, offset) => { ++ const columns = table.columns.map(item => (item.id === columnId ? { ...item, width } : item)); ++ const points = [table.points[0], [table.points[1][0] + offset, table.points[1][1]]]; ++ return { columns, points }; ++}; ++const updateRows = (table, rowId, height, offset) => { ++ const rows = table.rows.map(item => (item.id === rowId ? { ...item, height } : item)); ++ const points = [table.points[0], [table.points[1][0], table.points[1][1] + offset]]; ++ return { rows, points }; ++}; ++function updateCellIdsByRowOrColumn(cells, oldId, newId, type) { ++ const id = `${type}Id`; ++ cells.forEach(item => { ++ if (item[id] === oldId) { ++ item[id] = newId; ++ } ++ }); ++} ++function updateRowOrColumnIds(element, type) { ++ element[`${type}s`].forEach(item => { ++ const newId = idCreator(); ++ updateCellIdsByRowOrColumn(element.cells, item.id, newId, type); ++ item.id = newId; ++ }); ++} ++function updateCellIds(cells) { ++ cells.forEach(item => { ++ const newId = idCreator(); ++ item.id = newId; ++ }); ++} ++function isCellIncludeText(cell) { ++ return cell.text; ++} ++function getCellsRectangle(board, element, cells) { ++ const cellsWithPoints = getCellsWithPoints(board, element); ++ const points = cells.map(cell => { ++ const cellWithPoints = cellsWithPoints.find(item => item.id === cell.id); ++ return cellWithPoints.points; ++ }); ++ return RectangleClient.getRectangleByPoints(points); ++} ++const createCell = (rowId, columnId, text = null) => { ++ const cell = { ++ id: idCreator(), ++ rowId, ++ columnId ++ }; ++ if (text !== null) { ++ cell['text'] = { ++ children: [{ text }], ++ align: Alignment.center ++ }; ++ } ++ return cell; ++}; ++const getSelectedTableCellsEditor = (board) => { ++ if (isSingleSelectTable(board)) { ++ const elements = getSelectedTableElements(board); ++ const selectedCells = getSelectedCells(elements[0]); ++ const selectedCellsEditor = selectedCells?.map(cell => { ++ const textManage = getTextManageByCell(board, cell); ++ return textManage?.editor; ++ }); ++ if (selectedCellsEditor?.length) { ++ return selectedCellsEditor; ++ } ++ } ++ return undefined; ++}; ++ ++const SHAPE_MAX_LENGTH = 6; ++const memorizedShape = new WeakMap(); ++const getMemorizeKey = (element) => { ++ let key = ''; ++ switch (true) { ++ case PlaitDrawElement.isText(element): { ++ key = MemorizeKey.text; ++ break; ++ } ++ case PlaitDrawElement.isBasicShape(element): { ++ key = MemorizeKey.basicShape; ++ break; ++ } ++ case PlaitDrawElement.isFlowchart(element): { ++ key = MemorizeKey.flowchart; ++ break; ++ } ++ case PlaitDrawElement.isArrowLine(element): { ++ key = MemorizeKey.arrowLine; ++ break; ++ } ++ case PlaitDrawElement.isUML(element): { ++ key = MemorizeKey.UML; ++ } ++ } ++ return key; ++}; ++const getLineMemorizedLatest = () => { ++ const properties = getMemorizedLatest(MemorizeKey.arrowLine); ++ return { ...properties }; ++}; ++const getMemorizedLatestByPointer = (pointer) => { ++ let memorizeKey = ''; ++ if (PlaitDrawElement.isBasicShape({ shape: pointer })) { ++ memorizeKey = pointer === BasicShapes.text ? MemorizeKey.text : MemorizeKey.basicShape; ++ } ++ else if (PlaitDrawElement.isUML({ shape: pointer })) { ++ memorizeKey = MemorizeKey.UML; ++ } ++ else { ++ memorizeKey = MemorizeKey.flowchart; ++ } ++ const properties = { ...getMemorizedLatest(memorizeKey) }; ++ const textProperties = { ...properties.text }; ++ delete properties.text; ++ return { textProperties, geometryProperties: properties }; ++}; ++const memorizeLatestText = (element, operations) => { ++ const memorizeKey = getMemorizeKey(element); ++ let textMemory = getMemorizedLatest(memorizeKey)?.text || {}; ++ const setNodeOperation = operations.find((operation) => operation.type === 'set_node'); ++ if (setNodeOperation) { ++ const { properties, newProperties } = setNodeOperation; ++ for (const key in newProperties) { ++ const value = newProperties[key]; ++ if (value == null) { ++ delete textMemory[key]; ++ } ++ else { ++ textMemory[key] = value; ++ } ++ } ++ for (const key in properties) { ++ if (!newProperties.hasOwnProperty(key)) { ++ delete textMemory[key]; ++ } ++ } ++ memorizeLatest(memorizeKey, 'text', textMemory); ++ } ++}; ++const memorizeLatestShape = (board, shape) => { ++ const shapes = memorizedShape.has(board) ? memorizedShape.get(board) : []; ++ const shapeIndex = shapes.indexOf(shape); ++ if (shape === BasicShapes.text || shapeIndex === 0) { ++ return; ++ } ++ if (shapeIndex !== -1) { ++ shapes.splice(shapeIndex, 1); ++ } ++ else { ++ if (shapes.length === SHAPE_MAX_LENGTH) { ++ shapes.pop(); ++ } ++ } ++ shapes.unshift(shape); ++ memorizedShape.set(board, shapes); ++}; ++const getMemorizedLatestShape = (board) => { ++ return memorizedShape.get(board); ++}; ++ ++const debugKey$4 = 'debug:plait:line-mirror'; ++const debugGenerator$6 = createDebugGenerator(debugKey$4); ++const alignPoint = (basePoint, movingPoint) => { ++ const newPoint = [...movingPoint]; ++ if (Point.isVertical(newPoint, basePoint, LINE_ALIGN_TOLERANCE)) { ++ newPoint[0] = basePoint[0]; ++ } ++ if (Point.isHorizontal(newPoint, basePoint, LINE_ALIGN_TOLERANCE)) { ++ newPoint[1] = basePoint[1]; ++ } ++ return newPoint; ++}; ++const alignPoints = (basePoints, movingPoint, targetIndex) => { ++ let newMovingPoint = [...movingPoint]; ++ basePoints.forEach((basePoint, index) => { ++ if (index === targetIndex) { ++ return; ++ } ++ newMovingPoint = alignPoint(basePoint, newMovingPoint); ++ }); ++ return newMovingPoint; ++}; ++function getResizedPreviousAndNextPoint(nextRenderPoints, sourcePoint, targetPoint, handleIndex) { ++ const referencePoint = { ++ previous: null, ++ next: null ++ }; ++ const startPoint = nextRenderPoints[handleIndex]; ++ const endPoint = nextRenderPoints[handleIndex + 1]; ++ const isHorizontal = Point.isHorizontal(startPoint, endPoint); ++ const isVertical = Point.isVertical(startPoint, endPoint); ++ const previousPoint = nextRenderPoints[handleIndex - 1] ?? nextRenderPoints[0]; ++ const beforePreviousPoint = nextRenderPoints[handleIndex - 2] ?? sourcePoint; ++ if ((isHorizontal && Point.isHorizontal(beforePreviousPoint, previousPoint)) || ++ (isVertical && Point.isVertical(beforePreviousPoint, previousPoint))) { ++ referencePoint.previous = previousPoint; ++ } ++ const nextPoint = nextRenderPoints[handleIndex + 2] ?? nextRenderPoints[nextRenderPoints.length - 1]; ++ const afterNextPoint = nextRenderPoints[handleIndex + 3] ?? targetPoint; ++ if ((isHorizontal && Point.isHorizontal(nextPoint, afterNextPoint)) || (isVertical && Point.isVertical(nextPoint, afterNextPoint))) { ++ referencePoint.next = nextPoint; ++ } ++ return referencePoint; ++} ++function alignElbowSegment(startKeyPoint, endKeyPoint, resizeState, resizedPreviousAndNextPoint) { ++ let newStartPoint = startKeyPoint; ++ let newEndPoint = endKeyPoint; ++ if (Point.isHorizontal(startKeyPoint, endKeyPoint)) { ++ const offsetY = Point.getOffsetY(resizeState.startPoint, resizeState.endPoint); ++ let pointY = startKeyPoint[1] + offsetY; ++ if (resizedPreviousAndNextPoint.previous && Math.abs(resizedPreviousAndNextPoint.previous[1] - pointY) < LINE_ALIGN_TOLERANCE) { ++ pointY = resizedPreviousAndNextPoint.previous[1]; ++ } ++ else if (resizedPreviousAndNextPoint.next && Math.abs(resizedPreviousAndNextPoint.next[1] - pointY) < LINE_ALIGN_TOLERANCE) { ++ pointY = resizedPreviousAndNextPoint.next[1]; ++ } ++ newStartPoint = [startKeyPoint[0], pointY]; ++ newEndPoint = [endKeyPoint[0], pointY]; ++ } ++ if (Point.isVertical(startKeyPoint, endKeyPoint)) { ++ const offsetX = Point.getOffsetX(resizeState.startPoint, resizeState.endPoint); ++ let pointX = startKeyPoint[0] + offsetX; ++ if (resizedPreviousAndNextPoint.previous && Math.abs(resizedPreviousAndNextPoint.previous[0] - pointX) < LINE_ALIGN_TOLERANCE) { ++ pointX = resizedPreviousAndNextPoint.previous[0]; ++ } ++ else if (resizedPreviousAndNextPoint.next && Math.abs(resizedPreviousAndNextPoint.next[0] - pointX) < LINE_ALIGN_TOLERANCE) { ++ pointX = resizedPreviousAndNextPoint.next[0]; ++ } ++ newStartPoint = [pointX, startKeyPoint[1]]; ++ newEndPoint = [pointX, endKeyPoint[1]]; ++ } ++ return [newStartPoint, newEndPoint]; ++} ++function getIndexAndDeleteCountByKeyPoint(board, element, dataPoints, nextRenderPoints, handleIndex) { ++ let index = null; ++ let deleteCount = null; ++ const startKeyPoint = nextRenderPoints[handleIndex]; ++ const endKeyPoint = nextRenderPoints[handleIndex + 1]; ++ if (!startKeyPoint || !endKeyPoint) { ++ return { ++ index, ++ deleteCount ++ }; ++ } ++ const midDataPoints = dataPoints.slice(1, -1); ++ const startIndex = midDataPoints.findIndex((item) => Point.isEquals(item, startKeyPoint)); ++ const endIndex = midDataPoints.findIndex((item) => Point.isEquals(item, endKeyPoint)); ++ if (Math.max(startIndex, endIndex) > -1) { ++ if (startIndex > -1 && endIndex > -1) { ++ return { ++ index: startIndex, ++ deleteCount: 2 ++ }; ++ } ++ if (startIndex > -1 && endIndex === -1) { ++ const isReplace = startIndex < midDataPoints.length - 1 && ++ Point.isAlign([midDataPoints[startIndex], midDataPoints[startIndex + 1], startKeyPoint, endKeyPoint]); ++ if (isReplace) { ++ return { ++ index: startIndex, ++ deleteCount: 2 ++ }; ++ } ++ return { ++ index: startIndex, ++ deleteCount: 1 ++ }; ++ } ++ if (startIndex === -1 && endIndex > -1) { ++ const isReplace = endIndex > 0 && Point.isAlign([midDataPoints[endIndex], midDataPoints[endIndex - 1], startKeyPoint, endKeyPoint]); ++ if (isReplace) { ++ return { ++ index: endIndex - 1, ++ deleteCount: 2 ++ }; ++ } ++ return { ++ index: endIndex, ++ deleteCount: 1 ++ }; ++ } ++ } ++ else { ++ for (let i = 0; i < midDataPoints.length - 1; i++) { ++ const currentPoint = midDataPoints[i]; ++ const nextPoint = midDataPoints[i + 1]; ++ if (Point.isAlign([currentPoint, nextPoint, startKeyPoint, endKeyPoint])) { ++ index = i; ++ deleteCount = 2; ++ break; ++ } ++ if (Point.isAlign([currentPoint, nextPoint, startKeyPoint])) { ++ index = Math.min(i + 1, midDataPoints.length - 1); ++ deleteCount = 1; ++ break; ++ } ++ if (Point.isAlign([currentPoint, nextPoint, endKeyPoint])) { ++ index = Math.max(i - 1, 0); ++ deleteCount = 1; ++ break; ++ } ++ } ++ } ++ if (index === null) { ++ deleteCount = 0; ++ if (midDataPoints.length > 0) { ++ const handleRefPair = getArrowLineHandleRefPair(board, element); ++ const params = getElbowLineRouteOptions(board, element, handleRefPair); ++ const keyPoints = removeDuplicatePoints(generateElbowLineRoute(params, board)); ++ const nextKeyPoints = simplifyOrthogonalPoints(keyPoints.slice(1, keyPoints.length - 1)); ++ const nextDataPoints = [nextRenderPoints[0], ...midDataPoints, nextRenderPoints[nextRenderPoints.length - 1]]; ++ const mirrorDataPoints = getMirrorDataPoints(board, nextDataPoints, nextKeyPoints, params); ++ for (let i = handleIndex - 1; i >= 0; i--) { ++ const previousIndex = mirrorDataPoints.slice(1, -1).findIndex((item) => Point.isEquals(item, nextRenderPoints[i])); ++ if (previousIndex > -1) { ++ index = previousIndex + 1; ++ break; ++ } ++ } ++ if (index === null) { ++ index = 0; ++ // When renderPoints is a straight line and dataPoints are not on the line, ++ // the default 'deleteCount' is set to midDataPoints.length. ++ if (Point.isAlign(nextRenderPoints)) { ++ deleteCount = midDataPoints.length; ++ } ++ } ++ } ++ else { ++ index = 0; ++ } ++ } ++ return { ++ index, ++ deleteCount ++ }; ++} ++function getMirrorDataPoints(board, nextDataPoints, nextKeyPoints, params) { ++ for (let index = 1; index < nextDataPoints.length - 2; index++) { ++ adjustByCustomPointStartIndex(board, index, nextDataPoints, nextKeyPoints, params); ++ } ++ return nextDataPoints; ++} ++/** ++ * adjust based parallel segment ++ */ ++const adjustByCustomPointStartIndex = (board, customPointStartIndex, nextDataPoints, nextKeyPoints, params) => { ++ const beforePoint = nextDataPoints[customPointStartIndex - 1]; ++ const startPoint = nextDataPoints[customPointStartIndex]; ++ const endPoint = nextDataPoints[customPointStartIndex + 1]; ++ const afterPoint = nextDataPoints[customPointStartIndex + 2]; ++ const beforeSegment = [beforePoint, startPoint]; ++ const afterSegment = [endPoint, afterPoint]; ++ const isStraightWithBefore = Point.isAlign(beforeSegment); ++ const isStraightWithAfter = Point.isAlign(afterSegment); ++ let isAdjustStart = false; ++ let isAdjustEnd = false; ++ if (!isStraightWithBefore || !isStraightWithAfter) { ++ const midKeyPointsWithBefore = getMidKeyPoints(nextKeyPoints, beforeSegment[0], beforeSegment[1]); ++ const midKeyPointsWithAfter = getMidKeyPoints(nextKeyPoints, afterSegment[0], afterSegment[1]); ++ const hasMidKeyPoints = midKeyPointsWithBefore.length > 0 && midKeyPointsWithAfter.length > 0; ++ isAdjustStart = !isStraightWithBefore && !hasMidKeyPoints; ++ isAdjustEnd = !isStraightWithAfter && !hasMidKeyPoints; ++ } ++ if (isAdjustStart || isAdjustEnd) { ++ const parallelSegment = [startPoint, endPoint]; ++ const parallelSegments = findOrthogonalParallelSegments(parallelSegment, nextKeyPoints); ++ const mirrorSegments = findMirrorSegments(board, parallelSegment, parallelSegments, params.sourceRectangle, params.targetRectangle); ++ if (mirrorSegments.length === 1) { ++ const mirrorSegment = mirrorSegments[0]; ++ if (isAdjustStart) { ++ nextDataPoints.splice(customPointStartIndex, 1, mirrorSegment[0]); ++ } ++ if (isAdjustEnd) { ++ nextDataPoints.splice(customPointStartIndex + 1, 1, mirrorSegment[1]); ++ } ++ } ++ else { ++ const isHorizontal = Point.isHorizontal(startPoint, endPoint); ++ const adjustIndex = isHorizontal ? 0 : 1; ++ if (isAdjustStart) { ++ const newStartPoint = [startPoint[0], startPoint[1]]; ++ newStartPoint[adjustIndex] = beforePoint[adjustIndex]; ++ nextDataPoints.splice(customPointStartIndex, 1, newStartPoint); ++ } ++ if (isAdjustEnd) { ++ const newEndPoint = [endPoint[0], endPoint[1]]; ++ newEndPoint[adjustIndex] = afterPoint[adjustIndex]; ++ nextDataPoints.splice(customPointStartIndex + 1, 1, newEndPoint); ++ } ++ } ++ } ++}; ++function isUpdatedHandleIndex(board, element, dataPoints, nextRenderPoints, handleIndex) { ++ const { deleteCount } = getIndexAndDeleteCountByKeyPoint(board, element, dataPoints, nextRenderPoints, handleIndex); ++ if (deleteCount !== null && deleteCount > 1) { ++ return true; ++ } ++ return false; ++} ++function getMidKeyPoints(simplifiedNextKeyPoints, startPoint, endPoint) { ++ let midElbowPoints = []; ++ let startPointIndex = -1; ++ let endPointIndex = -1; ++ for (let i = 0; i < simplifiedNextKeyPoints.length; i++) { ++ if (Point.isAlign([simplifiedNextKeyPoints[i], startPoint])) { ++ startPointIndex = i; ++ } ++ if (startPointIndex > -1 && Point.isAlign([simplifiedNextKeyPoints[i], endPoint])) { ++ endPointIndex = i; ++ break; ++ } ++ } ++ if (startPointIndex > -1 && endPointIndex > -1) { ++ midElbowPoints = simplifiedNextKeyPoints.slice(startPointIndex, endPointIndex + 1); ++ } ++ return midElbowPoints; ++} ++function findOrthogonalParallelSegments(segment, keyPoints) { ++ const isHorizontalSegment = Point.isHorizontal(segment[0], segment[1]); ++ const parallelSegments = []; ++ for (let i = 0; i < keyPoints.length - 1; i++) { ++ const current = keyPoints[i]; ++ const next = keyPoints[i + 1]; ++ const isHorizontal = Point.isHorizontal(current, next, 0.1); ++ if (isHorizontalSegment && isHorizontal) { ++ parallelSegments.push([current, next]); ++ } ++ if (!isHorizontalSegment && !isHorizontal) { ++ parallelSegments.push([current, next]); ++ } ++ } ++ return parallelSegments; ++} ++function findMirrorSegments(board, segment, parallelSegments, sourceRectangle, targetRectangle) { ++ debugGenerator$6.isDebug() && debugGenerator$6.clear(); ++ const mirrorSegments = []; ++ for (let index = 0; index < parallelSegments.length; index++) { ++ const parallelPath = parallelSegments[index]; ++ const startPoint = [segment[0][0], segment[0][1]]; ++ const endPoint = [segment[1][0], segment[1][1]]; ++ const isHorizontal = Point.isHorizontal(startPoint, endPoint); ++ const adjustDataIndex = isHorizontal ? 0 : 1; ++ startPoint[adjustDataIndex] = parallelPath[0][adjustDataIndex]; ++ endPoint[adjustDataIndex] = parallelPath[1][adjustDataIndex]; ++ const fakeRectangle = RectangleClient.getRectangleByPoints([startPoint, endPoint, ...parallelPath]); ++ const isValid = !RectangleClient.isHit(fakeRectangle, sourceRectangle) && !RectangleClient.isHit(fakeRectangle, targetRectangle); ++ if (isValid) { ++ mirrorSegments.push([startPoint, endPoint]); ++ debugGenerator$6.isDebug() && debugGenerator$6.drawPolygon(board, RectangleClient.getCornerPoints(fakeRectangle)); ++ } ++ } ++ return mirrorSegments; ++} ++const hasIllegalElbowPoint = (midDataPoints) => { ++ if (midDataPoints.length === 1) { ++ return true; ++ } ++ return midDataPoints.some((item, index) => { ++ const beforePoint = midDataPoints[index - 1]; ++ const afterPoint = midDataPoints[index + 1]; ++ const beforeSegment = beforePoint && [beforePoint, item]; ++ const afterSegment = afterPoint && [item, afterPoint]; ++ const isStraightWithBefore = beforeSegment && Point.isAlign(beforeSegment); ++ const isStraightWithAfter = afterSegment && Point.isAlign(afterSegment); ++ if (index === 0) { ++ return !isStraightWithAfter; ++ } ++ if (index === midDataPoints.length - 1) { ++ return !isStraightWithBefore; ++ } ++ return !isStraightWithBefore && !isStraightWithAfter; ++ }); ++}; ++ ++const ARROW_LENGTH = 20; ++const drawArrowLineArrow = (element, points, options) => { ++ const arrowG = createG(); ++ if (PlaitArrowLine.isSourceMark(element, ArrowLineMarkerType.none) && PlaitArrowLine.isTargetMark(element, ArrowLineMarkerType.none)) { ++ return null; ++ } ++ const strokeWidth = getStrokeWidthByElement(element); ++ const offset = (strokeWidth * strokeWidth) / 3; ++ if (points.length === 1) { ++ points = [points[0], [points[0][0] + 0.1, points[0][1]]]; ++ } ++ if (!PlaitArrowLine.isSourceMark(element, ArrowLineMarkerType.none)) { ++ const source = getExtendPoint(points[0], points[1], ARROW_LENGTH + offset); ++ const sourceArrow = getArrow(element, { marker: element.source.marker, source, target: points[0], isSource: true }, options); ++ sourceArrow && arrowG.appendChild(sourceArrow); ++ } ++ if (!PlaitArrowLine.isTargetMark(element, ArrowLineMarkerType.none)) { ++ const source = getExtendPoint(points[points.length - 1], points[points.length - 2], ARROW_LENGTH + offset); ++ const arrow = getArrow(element, { marker: element.target.marker, source, target: points[points.length - 1], isSource: false }, options); ++ arrow && arrowG.appendChild(arrow); ++ } ++ return arrowG; ++}; ++const getArrow = (element, arrowOptions, options) => { ++ const { marker, target, source, isSource } = arrowOptions; ++ let targetArrow; ++ switch (marker) { ++ case ArrowLineMarkerType.openTriangle: { ++ targetArrow = drawOpenTriangle(element, source, target, options); ++ break; ++ } ++ case ArrowLineMarkerType.solidTriangle: { ++ targetArrow = drawSolidTriangle(source, target, options); ++ break; ++ } ++ case ArrowLineMarkerType.arrow: { ++ targetArrow = drawArrow(element, source, target, options); ++ break; ++ } ++ case ArrowLineMarkerType.sharpArrow: { ++ targetArrow = drawSharpArrow(source, target, options); ++ break; ++ } ++ case ArrowLineMarkerType.oneSideUp: { ++ targetArrow = drawOneSideArrow(source, target, isSource ? 'down' : 'up', options); ++ break; ++ } ++ case ArrowLineMarkerType.oneSideDown: { ++ targetArrow = drawOneSideArrow(source, target, isSource ? 'up' : 'down', options); ++ break; ++ } ++ case ArrowLineMarkerType.hollowTriangle: { ++ targetArrow = drawHollowTriangleArrow(source, target, options); ++ break; ++ } ++ case ArrowLineMarkerType.singleSlash: { ++ targetArrow = drawSingleSlash(source, target, isSource, options); ++ break; ++ } ++ } ++ return targetArrow; ++}; ++const drawSharpArrow = (source, target, options) => { ++ const startPoint = target; ++ const { pointLeft, pointRight } = arrowPoints(source, target, 20); ++ const g = createG(); ++ const path = createPath(); ++ let polylinePath = `M${pointRight[0]},${pointRight[1]}A25,25,20,0,1,${pointLeft[0]},${pointLeft[1]}L${startPoint[0]},${startPoint[1]}Z`; ++ path.setAttribute('d', polylinePath); ++ path.setAttribute('stroke', `${options?.stroke}`); ++ path.setAttribute('stroke-width', `${options?.strokeWidth}`); ++ path.setAttribute('fill', `${options?.stroke}`); ++ g.appendChild(path); ++ return g; ++}; ++const drawArrow = (element, source, target, options) => { ++ const unitVector = getUnitVectorByPointAndPoint(source, target); ++ const strokeWidth = getStrokeWidthByElement(element); ++ const endPoint = [target[0] + (strokeWidth * unitVector[0]) / 2, target[1] + (strokeWidth * unitVector[1]) / 2]; ++ const distance = distanceBetweenPointAndPoint(...source, ...endPoint); ++ const middlePoint = [ ++ endPoint[0] - (((distance * 3) / 5 + strokeWidth) / 2) * unitVector[0], ++ endPoint[1] - (((distance * 3) / 5 + strokeWidth) / 2) * unitVector[1] ++ ]; ++ const { pointLeft, pointRight } = arrowPoints(source, endPoint, 30); ++ const arrowG = drawLinearPath([pointLeft, endPoint, pointRight, middlePoint], { ...options, fill: options.stroke }, true); ++ const path = arrowG.querySelector('path'); ++ path.setAttribute('stroke-linejoin', 'round'); ++ return arrowG; ++}; ++const drawSolidTriangle = (source, target, options) => { ++ const endPoint = target; ++ const { pointLeft, pointRight } = arrowPoints(source, endPoint, 30); ++ return drawLinearPath([pointLeft, endPoint, pointRight], { ...options, fill: options.stroke }, true); ++}; ++const drawOpenTriangle = (element, source, target, options) => { ++ const unitVector = getUnitVectorByPointAndPoint(source, target); ++ const strokeWidth = getStrokeWidthByElement(element); ++ const endPoint = [target[0] + (strokeWidth * unitVector[0]) / 2, target[1] + (strokeWidth * unitVector[1]) / 2]; ++ const { pointLeft, pointRight } = arrowPoints(source, endPoint, 40); ++ return drawLinearPath([pointLeft, endPoint, pointRight], options); ++}; ++const drawOneSideArrow = (source, target, side, options) => { ++ const { pointLeft, pointRight } = arrowPoints(source, target, 40); ++ return drawLinearPath([side === 'up' ? pointRight : pointLeft, target], options); ++}; ++const drawSingleSlash = (source, target, isSource, options) => { ++ const length = distanceBetweenPointAndPoint(...source, ...target); ++ const middlePoint = getExtendPoint(target, source, length / 2); ++ const angle = isSource ? 120 : 60; ++ const start = rotate(...source, ...middlePoint, (angle * Math.PI) / 180); ++ const end = rotate(...target, ...middlePoint, (angle * Math.PI) / 180); ++ return drawLinearPath([start, end], options); ++}; ++const drawHollowTriangleArrow = (source, target, options) => { ++ const { pointLeft, pointRight } = arrowPoints(source, target, 30); ++ return drawLinearPath([pointLeft, pointRight, target], { ...options, fill: 'white' }, true); ++}; ++ ++class ArrowLineShapeGenerator extends Generator { ++ canDraw(element) { ++ return true; ++ } ++ draw(element) { ++ let lineG; ++ lineG = drawArrowLine(this.board, element); ++ return lineG; ++ } ++} ++ ++const getTextSize = (board, text, maxWidth) => { ++ const textSize = getElementSize(board, text, { fontSize: DEFAULT_FONT_SIZE, fontFamily: DEFAULT_FONT_FAMILY }, maxWidth); ++ const normalizedTextSize = normalizeWidthAndHeight(textSize); ++ return normalizedTextSize; ++}; ++const normalizeWidthAndHeight = (textSize) => { ++ return { ...textSize, width: textSize.width < MIN_TEXT_WIDTH ? MIN_TEXT_WIDTH : textSize.width }; ++}; ++ ++const createArrowLineElement = (shape, points, source, target, texts, options) => { ++ return { ++ id: idCreator(), ++ type: 'arrow-line', ++ shape, ++ source, ++ texts: texts ? texts : [], ++ target, ++ opacity: 1, ++ points, ++ ...options ++ }; ++}; ++const getArrowLinePoints = (board, element) => { ++ switch (element.shape) { ++ case ArrowLineShape.elbow: { ++ return getElbowPoints(board, element); ++ } ++ case ArrowLineShape.curve: { ++ return getCurvePoints(board, element); ++ } ++ default: { ++ const points = PlaitArrowLine.getPoints(board, element); ++ const handleRefPair = getArrowLineHandleRefPair(board, element); ++ points[0] = handleRefPair.source.point; ++ points[points.length - 1] = handleRefPair.target.point; ++ return points; ++ } ++ } ++}; ++const getCurvePoints = (board, element) => { ++ if (element.points.length === 2) { ++ const handleRefPair = getArrowLineHandleRefPair(board, element); ++ const { source, target } = handleRefPair; ++ const sourceBoundElement = handleRefPair.source.boundElement; ++ const targetBoundElement = handleRefPair.target.boundElement; ++ let curvePoints = [source.point]; ++ const sumDistance = distanceBetweenPointAndPoint(...source.point, ...target.point); ++ const offset = 12 + sumDistance / 3; ++ if (sourceBoundElement) { ++ curvePoints.push(getPointByVectorComponent(source.point, source.vector, offset)); ++ } ++ if (targetBoundElement) { ++ curvePoints.push(getPointByVectorComponent(target.point, target.vector, offset)); ++ } ++ const isSingleBound = (sourceBoundElement && !targetBoundElement) || (!sourceBoundElement && targetBoundElement); ++ if (isSingleBound) { ++ curvePoints.push(target.point); ++ const points = Q2C(curvePoints); ++ return pointsOnBezierCurves(points); ++ } ++ if (!sourceBoundElement && !targetBoundElement) { ++ curvePoints.push(getPointByVectorComponent(source.point, source.vector, offset)); ++ curvePoints.push(getPointByVectorComponent(target.point, target.vector, offset)); ++ } ++ curvePoints.push(target.point); ++ return pointsOnBezierCurves(curvePoints); ++ } ++ else { ++ let dataPoints = PlaitArrowLine.getPoints(board, element); ++ dataPoints = removeDuplicatePoints(dataPoints); ++ const points = catmullRomFitting(dataPoints); ++ return pointsOnBezierCurves(points); ++ } ++}; ++const drawArrowLine = (board, element) => { ++ const strokeWidth = getStrokeWidthByElement(element); ++ const strokeColor = getStrokeColorByElement(board, element); ++ const strokeStyle = getStrokeStyleByElement(board, element); ++ const strokeLineDash = getStrokeLineDash(strokeStyle, strokeWidth); ++ const options = { stroke: strokeColor, strokeWidth, strokeLineDash }; ++ const lineG = createG(); ++ let points = getArrowLinePoints(board, element); ++ let line; ++ if (element.shape === ArrowLineShape.curve) { ++ line = PlaitBoard.getRoughSVG(board).curve(points, options); ++ } ++ else { ++ line = drawLinearPath(points, options); ++ } ++ const id = idCreator(); ++ line.setAttribute('mask', `url(#${id})`); ++ if (element.strokeStyle === StrokeStyle.dotted) { ++ setStrokeLinecap(line, 'round'); ++ } ++ lineG.appendChild(line); ++ const { mask, maskTargetFillRect } = drawArrowLineMask(board, element, id); ++ lineG.appendChild(mask); ++ line.appendChild(maskTargetFillRect); ++ const arrow = drawArrowLineArrow(element, points, { stroke: strokeColor, strokeWidth }); ++ arrow && lineG.appendChild(arrow); ++ return lineG; ++}; ++const getHitConnection = (board, point, hitElement) => { ++ const ref = getSnappingRef(board, hitElement, point); ++ const connectionPoint = ref.connectorPoint || ref.edgePoint; ++ return getHitConnectionFromConnectionPoint(connectionPoint, hitElement); ++}; ++const getHitConnectionFromConnectionPoint = (connectionPoint, hitElement) => { ++ let rectangle = RectangleClient.getRectangleByPoints(hitElement.points); ++ return [(connectionPoint[0] - rectangle.x) / rectangle.width, (connectionPoint[1] - rectangle.y) / rectangle.height]; ++}; ++const getHitConnectorPoint = (point, hitElement) => { ++ const rectangle = RectangleClient.getRectangleByPoints(hitElement.points); ++ const shape = getElementShape(hitElement); ++ const connectorPoints = getEngine(shape).getConnectorPoints(rectangle); ++ return connectorPoints.find((connectorPoint) => { ++ return distanceBetweenPointAndPoint(...connectorPoint, ...point) <= LINE_SNAPPING_CONNECTOR_BUFFER; ++ }); ++}; ++const getArrowLineTextRectangle = (board, element, index) => { ++ const text = element.texts[index]; ++ const elbowPoints = getArrowLinePoints(board, element); ++ const point = getPointOnPolyline(elbowPoints, text.position); ++ const textSize = getTextSize(board, text.text); ++ return { ++ x: point[0] - textSize.width / 2, ++ y: point[1] - textSize.height / 2, ++ width: textSize.width, ++ height: textSize.height ++ }; ++}; ++const getArrowLines = (board) => { ++ return findElements(board, { ++ match: (element) => PlaitDrawElement.isArrowLine(element), ++ recursion: (element) => PlaitDrawElement.isDrawElement(element) ++ }); ++}; ++// quadratic Bezier to cubic Bezier ++const Q2C = (points) => { ++ const result = []; ++ const numSegments = points.length / 3; ++ for (let i = 0; i < numSegments; i++) { ++ const start = points[i]; ++ const qControl = points[i + 1]; ++ const end = points[i + 2]; ++ const startDistance = distanceBetweenPointAndPoint(...start, ...qControl); ++ const endDistance = distanceBetweenPointAndPoint(...end, ...qControl); ++ const cControl1 = getExtendPoint(start, qControl, (startDistance * 2) / 3); ++ const cControl2 = getExtendPoint(end, qControl, (endDistance * 2) / 3); ++ result.push(start, cControl1, cControl2, end); ++ } ++ return result; ++}; ++const debugGenerator$5 = createDebugGenerator('debug:plait:arrow-line-resize'); ++const handleArrowLineCreating = (board, lineShape, sourcePoint, movingPoint, sourceElement, lineShapeG, options) => { ++ if (debugGenerator$5.isDebug()) { ++ debugGenerator$5.clear(); ++ debugGenerator$5.drawCircles(board, [sourcePoint], 3, false); ++ } ++ const alignedMovingPoint = alignPoint(sourcePoint, movingPoint); ++ const hitElement = getSnappingShape(board, alignedMovingPoint); ++ const targetConnection = hitElement ? getHitConnection(board, alignedMovingPoint, hitElement) : undefined; ++ const sourceConnection = sourceElement ? getHitConnection(board, sourcePoint, sourceElement) : undefined; ++ const targetBoundId = hitElement ? hitElement.id : undefined; ++ const lineGenerator = new ArrowLineShapeGenerator(board); ++ const memorizedLatest = getLineMemorizedLatest(); ++ let sourceMarker, targetMarker; ++ sourceMarker = memorizedLatest.source; ++ targetMarker = memorizedLatest.target; ++ sourceMarker && delete memorizedLatest.source; ++ targetMarker && delete memorizedLatest.target; ++ const temporaryLineElement = createArrowLineElement(lineShape, [sourcePoint, alignedMovingPoint], { marker: sourceMarker || ArrowLineMarkerType.none, connection: sourceConnection, boundId: sourceElement?.id }, { marker: targetMarker || ArrowLineMarkerType.arrow, connection: targetConnection, boundId: targetBoundId }, [], { ++ strokeWidth: DefaultLineStyle.strokeWidth, ++ ...memorizedLatest, ++ ...options ++ }); ++ const linePoints = getArrowLinePoints(board, temporaryLineElement); ++ const otherPoint = linePoints[0]; ++ temporaryLineElement.points[1] = alignPoint(otherPoint, alignedMovingPoint); ++ lineGenerator.processDrawing(temporaryLineElement, lineShapeG); ++ PlaitBoard.getElementTopHost(board).append(lineShapeG); ++ return temporaryLineElement; ++}; ++function drawArrowLineMask(board, element, id) { ++ const mask = createMask(); ++ mask.setAttribute('id', id); ++ const points = getArrowLinePoints(board, element); ++ let rectangle = RectangleClient.getRectangleByPoints(points); ++ rectangle = RectangleClient.getOutlineRectangle(rectangle, -30); ++ const maskFillRect = createRect(rectangle, { ++ fill: 'white' ++ }); ++ mask.appendChild(maskFillRect); ++ const texts = element.texts; ++ texts.forEach((text, index) => { ++ let textRectangle = getArrowLineTextRectangle(board, element, index); ++ textRectangle = RectangleClient.inflate(textRectangle, LINE_TEXT_SPACE * 2); ++ const rect = createRect(textRectangle, { ++ fill: 'black' ++ }); ++ mask.appendChild(rect); ++ }); ++ // open line ++ const maskTargetFillRect = createRect(rectangle); ++ maskTargetFillRect.setAttribute('opacity', '0'); ++ maskTargetFillRect.setAttribute('fill', 'none'); ++ return { mask, maskTargetFillRect }; ++} ++ ++const getHitArrowLineTextIndex = (board, element, point) => { ++ const texts = element.texts; ++ if (!texts.length) ++ return -1; ++ const points = getArrowLinePoints(board, element); ++ return texts.findIndex((text) => { ++ const center = getPointOnPolyline(points, text.position); ++ const textSize = getTextSize(board, text.text); ++ const rectangle = { ++ x: center[0] - textSize.width / 2, ++ y: center[1] - textSize.height / 2, ++ width: textSize.width, ++ height: textSize.height ++ }; ++ return RectangleClient.isHit(rectangle, RectangleClient.getRectangleByPoints([point, point])); ++ }); ++}; ++ ++const isMultipleTextShape = (shape) => { ++ return GEOMETRY_WITH_MULTIPLE_TEXT.includes(shape); ++}; ++const isMultipleTextGeometry = (geometry) => { ++ return PlaitDrawElement.isGeometry(geometry) && isMultipleTextShape(geometry.shape); ++}; ++const getMultipleTextGeometryTextKeys = (shape) => { ++ return MultipleTextGeometryTextKeys[shape]; ++}; ++const createMultipleTextGeometryElement = (shape, points, options = {}) => { ++ const id = idCreator(); ++ const drawShapeTexts = buildDefaultTextsByShape(shape); ++ return { ++ id, ++ type: 'geometry', ++ shape, ++ angle: 0, ++ opacity: 1, ++ texts: drawShapeTexts, ++ points, ++ ...options ++ }; ++}; ++const buildDefaultTextsByShape = (shape) => { ++ const memorizedLatest = getMemorizedLatestByPointer(shape); ++ const textProperties = { ...memorizedLatest.textProperties }; ++ const alignment = textProperties?.align; ++ delete textProperties?.align; ++ const defaultTexts = getDefaultGeometryProperty(shape)?.texts || []; ++ const textKeys = getMultipleTextGeometryTextKeys(shape); ++ return (textKeys || []).map((textKey) => { ++ const text = defaultTexts?.find((item) => item?.key === textKey); ++ return { ++ id: textKey, ++ text: buildText(text?.text || '', alignment || text?.align || Alignment.center, textProperties) ++ }; ++ }); ++}; ++const getHitMultipleGeometryText = (board, element, point) => { ++ const engine = getEngine(element.shape); ++ const rectangle = RectangleClient.getRectangleByPoints([point, point]); ++ let hitText; ++ if (engine.getTextRectangle) { ++ hitText = element.texts.find((text) => { ++ const textRectangle = engine.getTextRectangle(board, element, { id: text.id }); ++ return RectangleClient.isHit(rectangle, textRectangle); ++ }); ++ } ++ return hitText; ++}; ++ ++class VectorLineShapeGenerator extends Generator { ++ canDraw(element) { ++ return true; ++ } ++ draw(element) { ++ let lineG; ++ lineG = drawVectorLine(this.board, element); ++ return lineG; ++ } ++} ++ ++const getVectorLinePoints = (board, element) => { ++ switch (element.shape) { ++ case VectorLineShape.straight: { ++ return element.points; ++ } ++ case VectorLineShape.curve: { ++ if (element.points.length === 2) { ++ return pointsOnBezierCurves(element.points); ++ } ++ else { ++ let dataPoints = element.points; ++ const points = catmullRomFitting(dataPoints); ++ return pointsOnBezierCurves(points); ++ } ++ } ++ default: ++ return null; ++ } ++}; ++const createVectorLineElement = (shape, points, options) => { ++ return { ++ id: idCreator(), ++ type: 'vector-line', ++ shape, ++ opacity: 1, ++ points, ++ ...options ++ }; ++}; ++const vectorLineCreating = (board, lineShape, points, movingPoint, lineShapeG) => { ++ const lineGenerator = new VectorLineShapeGenerator(board); ++ const memorizedLatest = getLineMemorizedLatest(); ++ const temporaryLineElement = createVectorLineElement(lineShape, [...points, movingPoint], { ++ strokeWidth: DefaultLineStyle.strokeWidth, ++ ...memorizedLatest ++ }); ++ const otherPoint = points[points.length - 1]; ++ temporaryLineElement.points[temporaryLineElement.points.length - 1] = alignPoint(otherPoint, movingPoint); ++ lineGenerator.processDrawing(temporaryLineElement, lineShapeG); ++ PlaitBoard.getElementTopHost(board).append(lineShapeG); ++ return temporaryLineElement; ++}; ++const drawVectorLine = (board, element) => { ++ const strokeWidth = getStrokeWidthByElement(element); ++ const strokeColor = getStrokeColorByElement(board, element); ++ const strokeStyle = getStrokeStyleByElement(board, element); ++ const strokeLineDash = getStrokeLineDash(strokeStyle, strokeWidth); ++ const fill = getFillByElement(board, element); ++ const options = { stroke: strokeColor, strokeWidth, strokeLineDash, fill }; ++ const lineG = createG(); ++ let points = getVectorLinePoints(board, element); ++ const line = drawLinearPath(points, options); ++ const id = idCreator(); ++ line.setAttribute('mask', `url(#${id})`); ++ if (element.strokeStyle === StrokeStyle.dotted) { ++ setStrokeLinecap(line, 'round'); ++ } ++ lineG.appendChild(line); ++ return lineG; ++}; ++ ++const getCenterPointsOnPolygon$1 = (points) => { ++ const centerPoints = []; ++ for (let i = 0; i < points.length; i++) { ++ let j = i == points.length - 1 ? 0 : i + 1; ++ centerPoints.push([(points[i][0] + points[j][0]) / 2, (points[i][1] + points[j][1]) / 2]); ++ } ++ return centerPoints; ++}; ++const getCrossingPointBetweenPointAndPolygon = (corners, point) => { ++ const result = []; ++ for (let index = 1; index <= corners.length; index++) { ++ let start = corners[index - 1]; ++ let end = index === corners.length ? corners[0] : corners[index]; ++ const crossingPoint = getCrossingPointsBetweenPointAndSegment(point, start, end); ++ result.push(...crossingPoint); ++ } ++ return result; ++}; ++const getPolygonEdgeByConnectionPoint = (corners, point) => { ++ for (let index = 1; index <= corners.length; index++) { ++ let start = corners[index - 1]; ++ let end = index === corners.length ? corners[0] : corners[index]; ++ if (isPointOnSegment(point, start, end)) { ++ return [start, end]; ++ } ++ } ++ return null; ++}; ++ ++function generateCloudPath(rectangle) { ++ const divisionWidth = rectangle.width / 7; ++ const divisionHeight = rectangle.height / 3.2; ++ const xRadius = divisionWidth / 8.5; ++ const yRadius = divisionHeight / 20; ++ const startPoint = [rectangle.x + divisionWidth, rectangle.y + divisionHeight]; ++ const arcCommands = [ ++ { ++ rx: xRadius, ++ ry: yRadius * 1.2, ++ xAxisRotation: 0, ++ largeArcFlag: 1, ++ sweepFlag: 1, ++ endX: rectangle.x + divisionWidth * 2, ++ endY: rectangle.y + divisionHeight / 2 ++ }, ++ { ++ rx: xRadius, ++ ry: yRadius, ++ xAxisRotation: 0, ++ largeArcFlag: 1, ++ sweepFlag: 1, ++ endX: rectangle.x + divisionWidth * 4.2, ++ endY: rectangle.y + divisionHeight / 2.2 ++ }, ++ { ++ rx: xRadius, ++ ry: yRadius, ++ xAxisRotation: 0, ++ largeArcFlag: 1, ++ sweepFlag: 1, ++ endX: rectangle.x + divisionWidth * 5.8, ++ endY: rectangle.y + divisionHeight ++ }, ++ { ++ rx: xRadius, ++ ry: yRadius * 1.3, ++ xAxisRotation: 0, ++ largeArcFlag: 1, ++ sweepFlag: 1, ++ endX: rectangle.x + divisionWidth * 6, ++ endY: rectangle.y + divisionHeight * 2.2 ++ }, ++ { ++ rx: xRadius, ++ ry: yRadius * 1.2, ++ xAxisRotation: 0, ++ largeArcFlag: 1, ++ sweepFlag: 1, ++ endX: rectangle.x + divisionWidth * 5, ++ endY: rectangle.y + divisionHeight * 2.8 ++ }, ++ { ++ rx: xRadius, ++ ry: yRadius / 1.2, ++ xAxisRotation: 0, ++ largeArcFlag: 1, ++ sweepFlag: 1, ++ endX: rectangle.x + divisionWidth * 2.8, ++ endY: rectangle.y + divisionHeight * 2.8 ++ }, ++ { ++ rx: xRadius, ++ ry: yRadius, ++ xAxisRotation: 0, ++ largeArcFlag: 1, ++ sweepFlag: 1, ++ endX: rectangle.x + divisionWidth, ++ endY: rectangle.y + divisionHeight * 2.2 ++ }, ++ { ++ rx: xRadius, ++ ry: yRadius * 1.42, ++ xAxisRotation: 0, ++ largeArcFlag: 1, ++ sweepFlag: 1, ++ endX: rectangle.x + divisionWidth, ++ endY: rectangle.y + divisionHeight ++ } ++ ]; ++ return { startPoint, arcCommands }; ++} ++const CloudEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const { startPoint, arcCommands } = generateCloudPath(rectangle); ++ const pathData = `M ${startPoint[0]} ${startPoint[1]} ` + ++ arcCommands ++ .map((command) => `A ${command.rx} ${command.ry} ${command.xAxisRotation} ${command.largeArcFlag} ${command.sweepFlag} ${command.endX} ${command.endY}`) ++ .join('\n') + ++ ' Z'; ++ const svgElement = rs.path(pathData, { ...options, fillStyle: 'solid' }); ++ setPathStrokeLinecap(svgElement, 'round'); ++ return svgElement; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ const { startPoint, arcCommands } = generateCloudPath(rectangle); ++ let minDistance = Infinity; ++ let nearestPoint = point; ++ let currentStart = startPoint; ++ for (const arcCommand of arcCommands) { ++ const arcNearestPoint = getNearestPointBetweenPointAndArc(point, currentStart, arcCommand); ++ const distance = distanceBetweenPointAndPoint(point[0], point[1], arcNearestPoint[0], arcNearestPoint[1]); ++ if (distance < minDistance) { ++ minDistance = distance; ++ nearestPoint = arcNearestPoint; ++ } ++ currentStart = [arcCommand.endX, arcCommand.endY]; ++ } ++ return nearestPoint; ++ }, ++ getEdgeByConnectionPoint(rectangle, pointOfRectangle) { ++ const corners = CloudEngine.getCornerPoints(rectangle); ++ const point = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ return getPolygonEdgeByConnectionPoint(corners, point); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle: (board, element) => { ++ const widthRatio = 1 / 1.5; ++ const rectangle = RectangleClient.getRectangleByPoints(element.points); ++ const textRectangle = getCustomTextRectangle(board, element, widthRatio); ++ return textRectangle; ++ } ++}; ++ ++const isHitArrowLineText = (board, element, point) => { ++ return getHitArrowLineTextIndex(board, element, point) !== -1; ++}; ++const isHitPolyLine = (pathPoints, point) => { ++ const distance = distanceBetweenPointAndSegments(point, pathPoints); ++ return distance <= HIT_DISTANCE_BUFFER; ++}; ++const isHitArrowLine = (board, element, point) => { ++ const points = getArrowLinePoints(board, element); ++ const isHitText = isHitArrowLineText(board, element, point); ++ return isHitText || isHitPolyLine(points, point); ++}; ++const isHitVectorLine = (board, element, point) => { ++ const points = getVectorLinePoints(board, element); ++ if (isClosedPoints(element.points)) { ++ return isPointInPolygon(point, points) || isHitPolyLine(points, point); ++ } ++ else { ++ return isHitPolyLine(points, point); ++ } ++}; ++const isRectangleHitElementText = (board, element, rectangle) => { ++ const engine = getEngine(element.shape); ++ if (isMultipleTextGeometry(element)) { ++ const texts = element.texts; ++ return texts.some((item) => { ++ const textClient = engine.getTextRectangle(board, element, { id: item.id }); ++ return isRectangleHitRotatedPoints(rectangle, RectangleClient.getCornerPoints(textClient), element.angle); ++ }); ++ } ++ else { ++ const textClient = engine.getTextRectangle ? engine.getTextRectangle(board, element) : getTextRectangle$1(board, element); ++ return isRectangleHitRotatedPoints(rectangle, RectangleClient.getCornerPoints(textClient), element.angle); ++ } ++}; ++const isHitElementText = (board, element, point) => { ++ const engine = getEngine(element.shape); ++ if (isMultipleTextGeometry(element)) { ++ const texts = element.texts; ++ return texts.some((item) => { ++ const textClient = engine.getTextRectangle(board, element, { id: item.id }); ++ return RectangleClient.isPointInRectangle(textClient, point); ++ }); ++ } ++ else { ++ const textClient = engine.getTextRectangle ? engine.getTextRectangle(board, element) : getTextRectangle$1(board, element); ++ return RectangleClient.isPointInRectangle(textClient, point); ++ } ++}; ++const isEmptyTextElement = (element) => { ++ if (!isDrawElementIncludeText(element)) { ++ return true; ++ } ++ const editor = getFirstTextEditor(element); ++ return Editor.isEmpty(editor, editor.children[0]); ++}; ++const isRectangleHitDrawElement = (board, element, selection) => { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([selection.anchor, selection.focus]); ++ if (PlaitDrawElement.isGeometry(element)) { ++ const isHitElement = isRectangleHitRotatedElement(board, rangeRectangle, element); ++ if (isHitElement) { ++ return isHitElement; ++ } ++ return !isEmptyTextElement(element) && isRectangleHitElementText(board, element, rangeRectangle); ++ } ++ if (PlaitDrawElement.isImage(element)) { ++ return isRectangleHitRotatedElement(board, rangeRectangle, element); ++ } ++ if (PlaitDrawElement.isArrowLine(element)) { ++ const points = getArrowLinePoints(board, element); ++ return isLineHitRectangle(points, rangeRectangle); ++ } ++ if (PlaitDrawElement.isVectorLine(element)) { ++ const points = getVectorLinePoints(board, element); ++ return isLineHitRectangle(points, rangeRectangle); ++ } ++ return null; ++}; ++const isRectangleHitRotatedElement = (board, rectangle, element) => { ++ const client = RectangleClient.getRectangleByPoints(element.points); ++ return isRectangleHitRotatedPoints(rectangle, RectangleClient.getCornerPoints(client), element.angle); ++}; ++const isRectangleHitRotatedPoints = (rectangle, points, angle) => { ++ let rotatedPoints = rotatePointsByAngle(points, angle) || points; ++ return isLineHitRectangle(rotatedPoints, rectangle); ++}; ++const getHitDrawElement = (board, elements) => { ++ let firstFilledElement = getFirstFilledDrawElement(board, elements); ++ let endIndex = elements.length; ++ if (firstFilledElement) { ++ endIndex = elements.indexOf(firstFilledElement) + 1; ++ } ++ const newElements = elements.slice(0, endIndex); ++ const solidElements = getSolidElements(newElements); ++ if (solidElements) { ++ return solidElements[0]; ++ } ++ const sortElements = sortElementsByArea(board, newElements, 'asc'); ++ return sortElements[0]; ++}; ++const getFirstFilledDrawElement = (board, elements) => { ++ let filledElement = null; ++ for (let i = 0; i < elements.length; i++) { ++ const element = elements[i]; ++ if (isClosedCustomGeometry(board, element) || isClosedDrawElement(element)) { ++ const fill = getFillByElement(board, element); ++ if (isFilled(fill)) { ++ filledElement = element; ++ break; ++ } ++ } ++ } ++ return filledElement; ++}; ++const isFilledDrawElement = (board, element) => { ++ return getFirstFilledDrawElement(board, [element]) !== null; ++}; ++const getSolidElements = (elements) => { ++ const solidElements = elements.filter((item) => PlaitDrawElement.isText(item) || PlaitDrawElement.isLine(item) || PlaitDrawElement.isImage(item)); ++ if (solidElements.length) { ++ return solidElements; ++ } ++ return null; ++}; ++const debugKey$3 = 'debug:plait:hit:shape:edge:sample-points'; ++const debugGenerator$4 = createDebugGenerator(debugKey$3); ++const shapes = [BasicShapes.cloud]; ++const isHitDrawElement = (board, element, point, isStrict = true) => { ++ const rectangle = board.getRectangle(element); ++ point = rotateAntiPointsByElement(board, point, element) || point; ++ if (PlaitDrawElement.isGeometry(element) && rectangle) { ++ if (debugGenerator$4.isDebug() && shapes.includes(element.shape)) { ++ debugGenerator$4.clear(); ++ const { startPoint, arcCommands } = generateCloudPath(rectangle); ++ const points = [startPoint, ...arcCommands.map((arc) => [arc.endX, arc.endY])]; ++ debugGenerator$4.drawCircles(board, points, 5, false); ++ let minDistance = Infinity; ++ let nearestPoint = point; ++ let currentStart = startPoint; ++ for (const arc of arcCommands) { ++ const arcNearestPoint = getNearestPointBetweenPointAndArc(point, currentStart, arc); ++ const distance = distanceBetweenPointAndPoint(point[0], point[1], arcNearestPoint[0], arcNearestPoint[1]); ++ const { center } = getEllipseArcCenter(currentStart, arc); ++ debugGenerator$4.drawCircles(board, [center], 8, false, { fill: 'yellow' }); ++ if (distance < minDistance) { ++ minDistance = distance; ++ nearestPoint = arcNearestPoint; ++ } ++ currentStart = [arc.endX, arc.endY]; ++ } ++ debugGenerator$4.drawCircles(board, [point], 12, false, { fill: 'black', stroke: 'black' }); ++ debugGenerator$4.drawCircles(board, [nearestPoint], 12, false, { fill: 'green', stroke: 'green' }); ++ } ++ if (isHitEdgeOfShape(board, element, point, HIT_DISTANCE_BUFFER)) { ++ return true; ++ } ++ const engine = getEngine(getElementShape(element)); ++ if (PlaitDrawElement.isText(element)) { ++ const textClient = getTextRectangle$1(board, element); ++ return RectangleClient.isPointInRectangle(textClient, point); ++ } ++ if (!!isStrict && isEmptyTextElement(element) && !isFilledDrawElement(board, element)) { ++ return false; ++ } ++ const isHitText = isHitElementText(board, element, point); ++ return isHitText || engine.isInsidePoint(rectangle, point); ++ } ++ if (PlaitDrawElement.isImage(element)) { ++ const client = RectangleClient.getRectangleByPoints(element.points); ++ return RectangleClient.isPointInRectangle(client, point); ++ } ++ if (PlaitDrawElement.isArrowLine(element)) { ++ return isHitArrowLine(board, element, point); ++ } ++ if (PlaitDrawElement.isVectorLine(element)) { ++ return isHitVectorLine(board, element, point); ++ } ++ return null; ++}; ++const isHitEdgeOfShape = (board, element, point, hitDistanceBuffer) => { ++ const nearestPoint = getNearestPoint(element, point); ++ const distance = distanceBetweenPointAndPoint(nearestPoint[0], nearestPoint[1], point[0], point[1]); ++ return distance <= hitDistanceBuffer; ++}; ++const isInsideOfShape = (board, element, point, hitDistanceBuffer) => { ++ const client = RectangleClient.inflate(RectangleClient.getRectangleByPoints(element.points), hitDistanceBuffer); ++ return getEngine(getElementShape(element)).isInsidePoint(client, point); ++}; ++const isHitElementInside = (board, element, point) => { ++ const rectangle = board.getRectangle(element); ++ point = rotateAntiPointsByElement(board, point, element) || point; ++ if (PlaitDrawElement.isGeometry(element) && !PlaitDrawElement.isGeometryByTable(element)) { ++ const engine = getEngine(getElementShape(element)); ++ const isHitInside = engine.isInsidePoint(rectangle, point); ++ if (isHitInside) { ++ return isHitInside; ++ } ++ if (engine.getTextRectangle) { ++ const isHitText = isHitElementText(board, element, point); ++ if (isHitText) { ++ return isHitText; ++ } ++ } ++ } ++ if (PlaitDrawElement.isImage(element)) { ++ const client = RectangleClient.getRectangleByPoints(element.points); ++ return RectangleClient.isPointInRectangle(client, point); ++ } ++ if (PlaitDrawElement.isArrowLine(element)) { ++ return isHitArrowLine(board, element, point); ++ } ++ if (PlaitDrawElement.isVectorLine(element)) { ++ return isHitVectorLine(board, element, point); ++ } ++ return null; ++}; ++ ++const getTextRectangle$1 = (board, element) => { ++ const isAutoSize = PlaitDrawElement.isText(element) ? element.autoSize : false; ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const strokeWidth = getStrokeWidthByElement(element); ++ const width = elementRectangle.width - ShapeDefaultSpace.rectangleAndText * 2 - strokeWidth * 2; ++ const textSize = getTextSize(board, element.text, isAutoSize ? GeometryThreshold.defaultTextMaxWidth : width); ++ if (isAutoSize) { ++ return { ++ height: textSize.height, ++ width: textSize.width, ++ x: elementRectangle.x + ShapeDefaultSpace.rectangleAndText + strokeWidth, ++ y: elementRectangle.y + (elementRectangle.height - textSize.height) / 2 ++ }; ++ } ++ return { ++ height: textSize.height, ++ width: width > 0 ? width : 0, ++ x: elementRectangle.x + ShapeDefaultSpace.rectangleAndText + strokeWidth, ++ y: elementRectangle.y + (elementRectangle.height - textSize.height) / 2 ++ }; ++}; ++const getCustomTextRectangle = (board, element, widthRatio) => { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const width = widthRatio * elementRectangle.width - ShapeDefaultSpace.rectangleAndText * 2 - getStrokeWidthByElement(element) * 2; ++ const textSize = getTextSize(board, element.text, width); ++ return { ++ height: textSize.height, ++ width: width, ++ x: elementRectangle.x + (elementRectangle.width - width) / 2, ++ y: elementRectangle.y + (elementRectangle.height - textSize.height) / 2 ++ }; ++}; ++const getStrokeWidthByElement = (element) => { ++ if (PlaitDrawElement.isText(element)) { ++ return 0; ++ } ++ const strokeWidth = element.strokeWidth || DefaultDrawStyle.strokeWidth; ++ return strokeWidth; ++}; ++const insertElement = (board, element) => { ++ memorizeLatestShape(board, element.shape); ++ Transforms.insertNode(board, element, [board.children.length]); ++ clearSelectedElement(board); ++ addSelectedElement(board, element); ++ BoardTransforms.updatePointerType(board, PlaitPointerType.selection); ++}; ++const isDrawElementIncludeText = (element) => { ++ if (PlaitDrawElement.isText(element)) { ++ return true; ++ } ++ if (PlaitDrawElement.isImage(element)) { ++ return false; ++ } ++ if (PlaitDrawElement.isGeometry(element)) { ++ return isGeometryIncludeText(element); ++ } ++ if (PlaitDrawElement.isArrowLine(element)) { ++ const editors = getTextEditorsByElement(element); ++ return editors.length > 0; ++ } ++ if (PlaitDrawElement.isElementByTable(element)) { ++ return element.cells.some((cell) => isCellIncludeText(cell)); ++ } ++ return true; ++}; ++const isDrawElementsIncludeText = (elements) => { ++ return elements.some((item) => { ++ return isDrawElementIncludeText(item); ++ }); ++}; ++const isClosedDrawElement = (element) => { ++ if (PlaitDrawElement.isDrawElement(element)) { ++ if (PlaitDrawElement.isText(element) || PlaitDrawElement.isArrowLine(element) || PlaitDrawElement.isImage(element)) { ++ return false; ++ } ++ if (PlaitDrawElement.isVectorLine(element)) { ++ return isClosedPoints(element.points); ++ } ++ if (PlaitDrawElement.isGeometry(element)) { ++ return isGeometryClosed(element); ++ } ++ return true; ++ } ++ return false; ++}; ++const isClosedCustomGeometry = (board, value) => { ++ return PlaitDrawElement.isCustomGeometryElement(board, value) && isClosedPoints(value.points); ++}; ++const getSnappingShape = (board, point) => { ++ let hitElement = getHitShape(board, point); ++ if (hitElement) { ++ const ref = getSnappingRef(board, hitElement, point); ++ if (ref.isHitConnector || ref.isHitEdge) { ++ return hitElement; ++ } ++ } ++ return null; ++}; ++const getSnappingRef = (board, hitElement, point) => { ++ const rotatedPoint = rotateAntiPointsByElement(board, point, hitElement) || point; ++ const connectorPoint = getHitConnectorPoint(rotatedPoint, hitElement); ++ const edgePoint = getNearestPoint(hitElement, rotatedPoint); ++ const isHitEdge = isHitEdgeOfShape(board, hitElement, rotatedPoint, LINE_SNAPPING_BUFFER); ++ return { isHitEdge, isHitConnector: !!connectorPoint, connectorPoint, edgePoint }; ++}; ++const getHitShape = (board, point, offset = LINE_HIT_GEOMETRY_BUFFER) => { ++ let hitShape = null; ++ traverseDrawShapes(board, (element) => { ++ if (hitShape === null && isInsideOfShape(board, element, rotateAntiPointsByElement(board, point, element) || point, offset * 2)) { ++ hitShape = element; ++ } ++ }); ++ return hitShape; ++}; ++const traverseDrawShapes = (board, callback) => { ++ depthFirstRecursion(board, (node) => { ++ if (!PlaitBoard.isBoard(node) && PlaitDrawElement.isShapeElement(node)) { ++ callback(node); ++ } ++ }, getIsRecursionFunc(board), true); ++}; ++const drawShape = (board, outerRectangle, shape, roughOptions, drawOptions) => { ++ return getEngine(shape).draw(board, outerRectangle, roughOptions, drawOptions); ++}; ++const drawBoundReaction = (board, element, roughOptions = { hasMask: true, hasConnector: true }) => { ++ const g = createG(); ++ const rectangle = RectangleClient.getRectangleByPoints(element.points); ++ const activeRectangle = RectangleClient.inflate(rectangle, SNAPPING_STROKE_WIDTH); ++ const shape = getElementShape(element); ++ let drawOptions; ++ if (PlaitDrawElement.isElementByTable(element)) { ++ drawOptions = { element }; ++ } ++ const strokeG = drawShape(board, activeRectangle, shape, { ++ stroke: SELECTION_BORDER_COLOR, ++ strokeWidth: SNAPPING_STROKE_WIDTH ++ }, drawOptions); ++ g.appendChild(strokeG); ++ if (roughOptions.hasMask) { ++ const maskG = drawShape(board, activeRectangle, shape, { ++ stroke: SELECTION_BORDER_COLOR, ++ strokeWidth: 0, ++ fill: isClosedDrawElement(element) ? SELECTION_FILL_COLOR : DefaultDrawStyle.fill, ++ fillStyle: 'solid' ++ }, drawOptions); ++ g.appendChild(maskG); ++ } ++ if (roughOptions.hasConnector) { ++ const connectorPoints = getEngine(shape).getConnectorPoints(rectangle); ++ connectorPoints.forEach((point) => { ++ const circleG = drawCircle(PlaitBoard.getRoughSVG(board), point, 8, { ++ stroke: SELECTION_BORDER_COLOR, ++ strokeWidth: ACTIVE_STROKE_WIDTH, ++ fill: '#FFF', ++ fillStyle: 'solid' ++ }); ++ g.appendChild(circleG); ++ }); ++ } ++ return g; ++}; ++const getTextKey = (element, text) => { ++ if (element && isMultipleTextGeometry(element)) { ++ return `${element.id}-${text.id}`; ++ } ++ else { ++ return text.id; ++ } ++}; ++const getGeometryAlign = (board, element) => { ++ if (isMultipleTextGeometry(element)) { ++ const drawShapeText = element.texts.find((item) => item.id.includes(GeometryCommonTextKeys.content)); ++ return drawShapeText?.text.align || Alignment.center; ++ } ++ if (isSingleTextGeometry(element)) { ++ return element.text?.align || Alignment.center; ++ } ++ if (PlaitDrawElement.isElementByTable(element)) { ++ const firstTextCell = element.cells.find((item) => item.text); ++ return firstTextCell?.text?.align || Alignment.center; ++ } ++ return Alignment.center; ++}; ++const isClosedPoints = (points) => { ++ const startPoint = points[0]; ++ const endPoint = points[points.length - 1]; ++ return startPoint[0] === endPoint[0] && startPoint[1] === endPoint[1]; ++}; ++const getDefaultGeometryText = (board) => { ++ return getI18nValue(board, DrawI18nKey.geometryText, DefaultTextProperty.text); ++}; ++ ++const getStrokeColorByElement = (board, element) => { ++ const defaultColor = getDrawDefaultStrokeColor(board.theme.themeColorMode); ++ const strokeColor = element.strokeColor || defaultColor; ++ return strokeColor; ++}; ++const getFillByElement = (board, element) => { ++ const defaultFill = PlaitDrawElement.isFlowchart(element) && isClosedDrawElement(element) ++ ? getFlowchartDefaultFill(board.theme.themeColorMode) ++ : DefaultDrawStyle.fill; ++ const fill = element.fill || defaultFill; ++ return fill; ++}; ++const getStrokeStyleByElement = (board, element) => { ++ return element.strokeStyle || StrokeStyle.solid; ++}; ++ ++class GeometryShapeGenerator extends Generator { ++ canDraw(element, data) { ++ return true; ++ } ++ draw(element, data) { ++ const rectangle = RectangleClient.getRectangleByPoints(element.points); ++ const shape = element.shape; ++ if (shape === BasicShapes.text) { ++ return; ++ } ++ const fill = getFillByElement(this.board, element); ++ const strokeWidth = getStrokeWidthByElement(element); ++ const strokeColor = getStrokeColorByElement(this.board, element); ++ const strokeStyle = getStrokeStyleByElement(this.board, element); ++ const strokeLineDash = getStrokeLineDash(strokeStyle, strokeWidth); ++ return drawGeometry(this.board, RectangleClient.inflate(rectangle, -strokeWidth), shape, { ++ stroke: strokeColor, ++ strokeWidth, ++ fill, ++ strokeLineDash ++ }); ++ } ++} ++ ++function getMiddlePoints(board, element) { ++ const result = []; ++ const shape = element.shape; ++ const hideBuffer = 10; ++ if (shape === ArrowLineShape.straight) { ++ const points = PlaitDrawElement.isArrowLine(element) ++ ? PlaitArrowLine.getPoints(board, element) ++ : element.points; ++ for (let i = 0; i < points.length - 1; i++) { ++ const distance = distanceBetweenPointAndPoint(...points[i], ...points[i + 1]); ++ if (distance < hideBuffer) ++ continue; ++ result.push([(points[i][0] + points[i + 1][0]) / 2, (points[i][1] + points[i + 1][1]) / 2]); ++ } ++ } ++ if (shape === ArrowLineShape.curve) { ++ const points = PlaitDrawElement.isArrowLine(element) ++ ? PlaitArrowLine.getPoints(board, element) ++ : element.points; ++ const pointsOnBezier = PlaitDrawElement.isArrowLine(element) ++ ? getCurvePoints(board, element) ++ : getVectorLinePoints(board, element); ++ if (points.length === 2) { ++ const start = 0; ++ const endIndex = pointsOnBezier.length - 1; ++ const middleIndex = Math.round((start + endIndex) / 2); ++ result.push(pointsOnBezier[middleIndex]); ++ } ++ else { ++ for (let i = 0; i < points.length - 1; i++) { ++ const startIndex = pointsOnBezier.findIndex(point => point[0] === points[i][0] && point[1] === points[i][1]); ++ const endIndex = pointsOnBezier.findIndex(point => point[0] === points[i + 1][0] && point[1] === points[i + 1][1]); ++ const middleIndex = Math.round((startIndex + endIndex) / 2); ++ const distance = distanceBetweenPointAndPoint(...points[i], ...points[i + 1]); ++ if (distance < hideBuffer) ++ continue; ++ result.push(pointsOnBezier[middleIndex]); ++ } ++ } ++ } ++ if (shape === ArrowLineShape.elbow) { ++ const renderPoints = getElbowPoints(board, element); ++ const options = getElbowLineRouteOptions(board, element); ++ if (!isUseDefaultOrthogonalRoute(element, options)) { ++ const [nextSourcePoint, nextTargetPoint] = getNextSourceAndTargetPoints(board, element); ++ for (let i = 0; i < renderPoints.length - 1; i++) { ++ if ((i == 0 && Point.isEquals(renderPoints[i + 1], nextSourcePoint)) || ++ (i === renderPoints.length - 2 && Point.isEquals(renderPoints[renderPoints.length - 2], nextTargetPoint))) { ++ continue; ++ } ++ const [currentX, currentY] = renderPoints[i]; ++ const [nextX, nextY] = renderPoints[i + 1]; ++ const middlePoint = [(currentX + nextX) / 2, (currentY + nextY) / 2]; ++ result.push(middlePoint); ++ } ++ } ++ } ++ return result; ++} ++ ++var LineResizeHandle; ++(function (LineResizeHandle) { ++ LineResizeHandle["source"] = "source"; ++ LineResizeHandle["target"] = "target"; ++ LineResizeHandle["addHandle"] = "addHandle"; ++})(LineResizeHandle || (LineResizeHandle = {})); ++const getHitLineResizeHandleRef = (board, element, point) => { ++ let dataPoints = PlaitDrawElement.isArrowLine(element) ? PlaitArrowLine.getPoints(board, element) : element.points; ++ const index = getHitPointIndex(dataPoints, point); ++ if (index !== -1) { ++ const handleIndex = index; ++ if (index === 0) { ++ return { handle: LineResizeHandle.source, handleIndex }; ++ } ++ if (index === dataPoints.length - 1) { ++ return { handle: LineResizeHandle.target, handleIndex }; ++ } ++ // elbow line, data points only verify source connection point and target connection point ++ if (element.shape !== ArrowLineShape.elbow) { ++ return { handleIndex }; ++ } ++ } ++ const middlePoints = getMiddlePoints(board, element); ++ const indexOfMiddlePoints = getHitPointIndex(middlePoints, point); ++ if (indexOfMiddlePoints !== -1) { ++ return { ++ handle: LineResizeHandle.addHandle, ++ handleIndex: indexOfMiddlePoints ++ }; ++ } ++ return undefined; ++}; ++function getHitPointIndex(points, movingPoint) { ++ const rectangles = points.map(point => { ++ return { ++ x: point[0] - RESIZE_HANDLE_DIAMETER / 2, ++ y: point[1] - RESIZE_HANDLE_DIAMETER / 2, ++ width: RESIZE_HANDLE_DIAMETER, ++ height: RESIZE_HANDLE_DIAMETER ++ }; ++ }); ++ const rectangle = rectangles.find(rectangle => { ++ return RectangleClient.isHit(RectangleClient.getRectangleByPoints([movingPoint, movingPoint]), rectangle); ++ }); ++ return rectangle ? rectangles.indexOf(rectangle) : -1; ++} ++ ++class LineActiveGenerator extends Generator { ++ constructor(board, options = { active: true }) { ++ super(board, options); ++ this.board = board; ++ this.onlySelectedCurrentLine = false; ++ } ++ canDraw(element, data) { ++ if (data.selected) { ++ return true; ++ } ++ else { ++ return false; ++ } ++ } ++ draw(element, data) { ++ const activeG = createG(); ++ const selectedElements = getSelectedElements(this.board); ++ this.onlySelectedCurrentLine = selectedElements.length === 1; ++ if (this.onlySelectedCurrentLine) { ++ activeG.classList.add(SELECTION_RECTANGLE_CLASS_NAME); ++ const points = PlaitDrawElement.isArrowLine(element) ? PlaitArrowLine.getPoints(this.board, element) : element.points; ++ let updatePoints = [...points]; ++ let elbowNextRenderPoints = []; ++ if (element.shape === ArrowLineShape.elbow) { ++ updatePoints = points.slice(0, 1).concat(points.slice(-1)); ++ elbowNextRenderPoints = getNextRenderPoints(this.board, element, data.linePoints); ++ } ++ const activePoints = updatePoints.map((point) => toActivePointFromViewBoxPoint(this.board, point)); ++ activePoints.forEach((point) => { ++ const updateHandle = drawPrimaryHandle(this.board, point); ++ activeG.appendChild(updateHandle); ++ }); ++ const middlePoints = getMiddlePoints(this.board, element); ++ const activeMiddlePoints = middlePoints.map((point) => toActivePointFromViewBoxPoint(this.board, point)); ++ if (!PlaitBoard.hasBeenTextEditing(this.board)) { ++ for (let i = 0; i < activeMiddlePoints.length; i++) { ++ const point = activeMiddlePoints[i]; ++ if (element.shape === ArrowLineShape.elbow && elbowNextRenderPoints.length) { ++ const handleIndex = getHitPointIndex(activeMiddlePoints, point); ++ const isUpdateHandleIndex = isUpdatedHandleIndex(this.board, element, [...points], elbowNextRenderPoints, handleIndex); ++ if (isUpdateHandleIndex) { ++ const updateHandle = drawPrimaryHandle(this.board, point); ++ activeG.appendChild(updateHandle); ++ continue; ++ } ++ } ++ const circle = drawFillPrimaryHandle(this.board, point); ++ activeG.appendChild(circle); ++ } ++ } ++ } ++ else { ++ const rectangle = this.board.getRectangle(element); ++ if (rectangle) { ++ const activeRectangle = toActiveRectangleFromViewBoxRectangle(this.board, rectangle); ++ let opacity = '0.5'; ++ if (activeRectangle.height === 0 || activeRectangle.width === 0) { ++ opacity = '0.8'; ++ } ++ const strokeG = drawRectangle(this.board, activeRectangle, { ++ stroke: PRIMARY_COLOR, ++ strokeWidth: DefaultDrawActiveStyle.selectionStrokeWidth ++ }); ++ strokeG.style.opacity = opacity; ++ activeG.appendChild(strokeG); ++ } ++ } ++ return activeG; ++ } ++ needUpdate() { ++ const selectedElements = getSelectedElements(this.board); ++ const onlySelectedCurrentLine = selectedElements.length === 1; ++ return onlySelectedCurrentLine !== this.onlySelectedCurrentLine; ++ } ++} ++ ++class ArrowLineAutoCompleteGenerator extends Generator { ++ static { this.key = 'line-auto-complete-generator'; } ++ constructor(board) { ++ super(board, { active: true }); ++ this.board = board; ++ this.hoverElement = null; ++ } ++ canDraw(element, data) { ++ const selectedElements = getSelectedElements(this.board); ++ if (data.selected && selectedElements.length === 1 && !isSelectionMoving(this.board)) { ++ return true; ++ } ++ else { ++ return false; ++ } ++ } ++ draw(element, data) { ++ this.autoCompleteG = createG(); ++ const middlePoints = getAutoCompletePoints(this.board, element, true); ++ middlePoints.forEach((point, index) => { ++ const circle = drawCircle(PlaitBoard.getRoughSVG(this.board), point, LINE_AUTO_COMPLETE_DIAMETER, { ++ stroke: 'none', ++ fill: rgbaToHEX(PRIMARY_COLOR, LINE_AUTO_COMPLETE_OPACITY), ++ fillStyle: 'solid' ++ }); ++ circle.classList.add(`line-auto-complete-${index}`); ++ this.autoCompleteG.appendChild(circle); ++ }); ++ return this.autoCompleteG; ++ } ++ removeAutoCompleteG(index) { ++ this.hoverElement = this.autoCompleteG.querySelector(`.line-auto-complete-${index}`); ++ this.hoverElement.style.visibility = 'hidden'; ++ } ++ recoverAutoCompleteG() { ++ if (this.hoverElement) { ++ this.hoverElement.style.visibility = 'visible'; ++ this.hoverElement = null; ++ } ++ } ++} ++ ++class SingleTextGenerator extends TextGenerator { ++ get textManage() { ++ return this.textManages[0]; ++ } ++ constructor(board, element, text, options) { ++ super(board, element, [{ id: element.id, text: text }], options); ++ } ++ update(element, previousText, currentText, elementG) { ++ if (!isMultipleTextGeometry(element)) { ++ super.update(element, [{ text: previousText, id: element.id }], [{ text: currentText, id: element.id }], elementG); ++ } ++ } ++} ++ ++class TableGenerator extends Generator { ++ canDraw(element, data) { ++ return true; ++ } ++ draw(element, data) { ++ const rectangle = RectangleClient.getRectangleByPoints(element.points); ++ const strokeWidth = getStrokeWidthByElement(element); ++ const strokeColor = getStrokeColorByElement(this.board, element); ++ const strokeStyle = getStrokeStyleByElement(this.board, element); ++ const strokeLineDash = getStrokeLineDash(strokeStyle, strokeWidth); ++ return getEngine(TableSymbols.table).draw(this.board, rectangle, { ++ strokeWidth, ++ stroke: strokeColor, ++ strokeLineDash ++ }, { ++ element: element ++ }); ++ } ++} ++ ++const getElementShape = (value) => { ++ if (PlaitDrawElement.isImage(value)) { ++ return BasicShapes.rectangle; ++ } ++ if (PlaitDrawElement.isTable(value)) { ++ return TableSymbols.table; ++ } ++ return value.shape; ++}; ++const getGeometryGeneratorByShape = (board, shape) => { ++ if (PlaitDrawElement.isUMLClassOrInterface({ shape: shape })) { ++ return new TableGenerator(board); ++ } ++ else { ++ return new GeometryShapeGenerator(board); ++ } ++}; ++ ++const createUMLClassOrInterfaceGeometryElement = (board, shape, points) => { ++ const memorizedLatest = getMemorizedLatestByPointer(shape); ++ const element = { ++ id: idCreator(), ++ type: 'geometry', ++ angle: 0, ++ opacity: 1, ++ points, ++ strokeWidth: DefaultBasicShapeProperty.strokeWidth, ++ ...memorizedLatest.geometryProperties ++ }; ++ let rows; ++ let columns; ++ if (shape === UMLSymbols.class) { ++ rows = [ ++ { ++ id: idCreator(), ++ height: 30 ++ }, ++ { ++ id: idCreator() ++ }, ++ { ++ id: idCreator() ++ } ++ ]; ++ columns = [ ++ { ++ id: idCreator() ++ } ++ ]; ++ } ++ else { ++ rows = [ ++ { ++ id: idCreator(), ++ height: 50 ++ }, ++ { ++ id: idCreator() ++ } ++ ]; ++ columns = [ ++ { ++ id: idCreator() ++ } ++ ]; ++ } ++ return { ++ ...element, ++ shape, ++ rows, ++ columns, ++ cells: buildTableCellsForGeometry(board, rows, columns, shape) ++ }; ++}; ++const buildTableCellsForGeometry = (board, rows, columns, shape) => { ++ const cellCount = rows.length * columns.length; ++ const defaultTexts = getDefaultGeometryProperty(shape)?.texts || []; ++ return new Array(cellCount).fill('').map((item, index) => { ++ const rowIndex = Math.floor(index / columns.length); ++ const columnIndex = index % columns.length; ++ return { ++ id: idCreator(), ++ rowId: rows[rowIndex].id, ++ columnId: columns[columnIndex].id, ++ text: { ++ children: [ ++ { ++ text: defaultTexts[index].text ++ } ++ ], ++ align: defaultTexts[index].align ++ } ++ }; ++ }); ++}; ++ ++const createGeometryElement = (shape, points, text, options = {}, textProperties = {}) => { ++ if (GEOMETRY_WITHOUT_TEXT.includes(shape)) { ++ return createGeometryElementWithoutText(shape, points, options); ++ } ++ else { ++ return createGeometryElementWithText(shape, points, text, options, textProperties); ++ } ++}; ++const createGeometryElementWithText = (shape, points, text, options = {}, textProperties = {}) => { ++ let textOptions = {}; ++ let alignment = Alignment.center; ++ if (shape === BasicShapes.text) { ++ textOptions = { autoSize: true }; ++ alignment = undefined; ++ } ++ textProperties = { ...textProperties }; ++ textProperties?.align && (alignment = textProperties?.align); ++ delete textProperties?.align; ++ return { ++ id: idCreator(), ++ type: 'geometry', ++ shape, ++ angle: 0, ++ opacity: 1, ++ text: buildText(text, alignment, textProperties), ++ points, ++ ...textOptions, ++ ...options ++ }; ++}; ++const createGeometryElementWithoutText = (shape, points, options = {}) => { ++ return { ++ id: idCreator(), ++ type: 'geometry', ++ shape, ++ angle: 0, ++ opacity: 1, ++ points, ++ ...options ++ }; ++}; ++const drawGeometry = (board, outerRectangle, shape, roughOptions) => { ++ return getEngine(shape).draw(board, outerRectangle, roughOptions); ++}; ++const getNearestPoint = (element, point) => { ++ const rectangle = RectangleClient.getRectangleByPoints(element.points); ++ const shape = getElementShape(element); ++ return getEngine(shape).getNearestPoint(rectangle, point); ++}; ++const getCenterPointsOnPolygon = (points) => { ++ const centerPoint = []; ++ for (let i = 0; i < points.length; i++) { ++ let j = i == points.length - 1 ? 0 : i + 1; ++ centerPoint.push([(points[i][0] + points[j][0]) / 2, (points[i][1] + points[j][1]) / 2]); ++ } ++ return centerPoint; ++}; ++const getDefaultFlowchartProperty = (symbol) => { ++ return DefaultFlowchartPropertyMap[symbol]; ++}; ++const getDefaultBasicShapeProperty = (shape) => { ++ return DefaultBasicShapePropertyMap[shape] || DefaultBasicShapeProperty; ++}; ++const getDefaultUMLProperty = (shape) => { ++ return DefaultUMLPropertyMap[shape]; ++}; ++const getAutoCompletePoints = (board, element, isToActive = false) => { ++ const AutoCompleteMargin = (12 + RESIZE_HANDLE_DIAMETER / 2) * 2; ++ const rectangle = RectangleClient.getRectangleByPoints(element.points); ++ const activeRectangle = toActiveRectangleFromViewBoxRectangle(board, rectangle); ++ const targetRectangle = isToActive ? activeRectangle : rectangle; ++ return RectangleClient.getEdgeCenterPoints(RectangleClient.inflate(targetRectangle, AutoCompleteMargin)); ++}; ++const getHitIndexOfAutoCompletePoint = (movingPoint, points) => { ++ return points.findIndex((point) => { ++ const movingRectangle = RectangleClient.getRectangleByPoints([movingPoint]); ++ let rectangle = RectangleClient.getRectangleByPoints([point]); ++ rectangle = RectangleClient.inflate(rectangle, RESIZE_HANDLE_DIAMETER); ++ return RectangleClient.isHit(movingRectangle, rectangle); ++ }); ++}; ++const getDrawDefaultStrokeColor = (theme) => { ++ return DrawThemeColors[theme].strokeColor; ++}; ++const getFlowchartDefaultFill = (theme) => { ++ return DrawThemeColors[theme].fill; ++}; ++const getTextShapeProperty = (board, text, fontSize) => { ++ fontSize = fontSize ? Number(fontSize) : DEFAULT_FONT_SIZE; ++ const textSize = measureElement(board, buildText(text), { fontSize, fontFamily: DEFAULT_FONT_FAMILY }); ++ return { ++ width: textSize.width + ShapeDefaultSpace.rectangleAndText * 2, ++ height: textSize.height ++ }; ++}; ++const getDefaultGeometryPoints = (pointer, centerPoint) => { ++ const property = getDefaultGeometryProperty(pointer); ++ return RectangleClient.getPoints(RectangleClient.getRectangleByCenterPoint(centerPoint, property.width, property.height)); ++}; ++const getDefaultGeometryProperty = (pointer) => { ++ const isFlowChart = getFlowchartPointers().includes(pointer); ++ const isUML = getUMLPointers().includes(pointer); ++ if (isFlowChart) { ++ return getDefaultFlowchartProperty(pointer); ++ } ++ else if (isUML) { ++ return getDefaultUMLProperty(pointer); ++ } ++ else { ++ return getDefaultBasicShapeProperty(pointer); ++ } ++}; ++const getDefaultTextPoints = (board, centerPoint, fontSize) => { ++ const property = getTextShapeProperty(board, DefaultTextProperty.text, fontSize); ++ return RectangleClient.getPoints(RectangleClient.getRectangleByCenterPoint(centerPoint, property.width, property.height)); ++}; ++const createTextElement = (board, points, text) => { ++ const memorizedLatest = getMemorizedLatestByPointer(BasicShapes.text); ++ return createGeometryElement(BasicShapes.text, points, text, memorizedLatest.geometryProperties, { ++ ...memorizedLatest.textProperties ++ }); ++}; ++const createDefaultGeometry = (board, points, shape) => { ++ const memorizedLatest = getMemorizedLatestByPointer(shape); ++ if (PlaitDrawElement.isUMLClassOrInterface({ shape })) { ++ return createUMLClassOrInterfaceGeometryElement(board, shape, points); ++ } ++ if (isMultipleTextShape(shape)) { ++ return createMultipleTextGeometryElement(shape, points, { ++ strokeWidth: DefaultBasicShapeProperty.strokeWidth, ++ ...memorizedLatest.geometryProperties ++ }); ++ } ++ else { ++ return createGeometryElement(shape, points, '', { ++ strokeWidth: DefaultBasicShapeProperty.strokeWidth, ++ ...memorizedLatest.geometryProperties ++ }, { ...memorizedLatest.textProperties }); ++ } ++}; ++const editText = (board, element, text) => { ++ const textManage = text ? getTextManage(board, element, text) : getFirstTextManage(element); ++ if (textManage) { ++ textManage.edit(() => { }); ++ } ++}; ++const isGeometryIncludeText = (element) => { ++ return isSingleTextGeometry(element) || isMultipleTextGeometry(element); ++}; ++const isSingleTextShape = (shape) => { ++ return !GEOMETRY_WITHOUT_TEXT.includes(shape) && !isMultipleTextShape(shape); ++}; ++const isSingleTextGeometry = (element) => { ++ return PlaitDrawElement.isGeometry(element) && isSingleTextShape(element.shape); ++}; ++const isGeometryClosed = (element) => { ++ return !GEOMETRY_NOT_CLOSED.includes(element.shape); ++}; ++ ++const isSelfLoop = (element) => { ++ return element.source.boundId && element.source.boundId === element.target.boundId; ++}; ++const isUseDefaultOrthogonalRoute = (element, options) => { ++ return isSourceAndTargetIntersect(options) && !isSelfLoop(element); ++}; ++const getElbowPoints = (board, element) => { ++ const handleRefPair = getArrowLineHandleRefPair(board, element); ++ const params = getElbowLineRouteOptions(board, element, handleRefPair); ++ // console.log(params, 'params'); ++ if (isUseDefaultOrthogonalRoute(element, params)) { ++ return simplifyOrthogonalPoints(getPoints(handleRefPair.source.point, handleRefPair.source.direction, handleRefPair.target.point, handleRefPair.target.direction, DEFAULT_ROUTE_MARGIN)); ++ } ++ const keyPoints = removeDuplicatePoints(generateElbowLineRoute(params, board)); ++ const nextKeyPoints = keyPoints.slice(1, keyPoints.length - 1); ++ if (element.points.length === 2) { ++ return simplifyOrthogonalPoints(keyPoints); ++ } ++ else { ++ const simplifiedNextKeyPoints = simplifyOrthogonalPoints(nextKeyPoints); ++ const dataPoints = removeDuplicatePoints(PlaitArrowLine.getPoints(board, element)); ++ const midDataPoints = dataPoints.slice(1, -1); ++ if (hasIllegalElbowPoint(midDataPoints)) { ++ return simplifyOrthogonalPoints(keyPoints); ++ } ++ const nextDataPoints = [simplifiedNextKeyPoints[0], ...midDataPoints, simplifiedNextKeyPoints[simplifiedNextKeyPoints.length - 1]]; ++ const mirrorDataPoints = getMirrorDataPoints(board, nextDataPoints, simplifiedNextKeyPoints, params); ++ // console.log(mirrorDataPoints, 'mirrorDataPoints'); ++ const renderPoints = [keyPoints[0]]; ++ for (let index = 0; index < mirrorDataPoints.length - 1; index++) { ++ let currentPoint = mirrorDataPoints[index]; ++ let nextPoint = mirrorDataPoints[index + 1]; ++ const isStraight = Point.isAlign([currentPoint, nextPoint]); ++ if (!isStraight) { ++ const midKeyPoints = getMidKeyPoints(simplifiedNextKeyPoints, currentPoint, nextPoint); ++ if (midKeyPoints.length) { ++ renderPoints.push(currentPoint); ++ renderPoints.push(...midKeyPoints); ++ } ++ else { ++ renderPoints.push(currentPoint); ++ console.log('unknown data points'); ++ } ++ } ++ else { ++ renderPoints.push(currentPoint); ++ } ++ } ++ renderPoints.push(keyPoints[keyPoints.length - 2], keyPoints[keyPoints.length - 1]); ++ // Remove the middle point to avoid the situation where the starting and ending positions are drawn back, such as when sourcePoint is between nextSourcePoint and the first key point. ++ // Issue ++ // keyPoint2 ++ // | ++ // | ++ // nextPoint---sourcePoint---keyPoint1 ++ // The correct rendering should be (nextPoint should be filtered out): ++ // keyPoint2 ++ // | ++ // | ++ // sourcePoint---keyPoint1 ++ const ret = simplifyOrthogonalPoints(renderPoints); ++ return ret; ++ } ++}; ++const getNextSourceAndTargetPoints = (board, element) => { ++ const options = getElbowLineRouteOptions(board, element); ++ return [options.nextSourcePoint, options.nextTargetPoint]; ++}; ++const getSourceAndTargetRectangle = (board, element, handleRefPair) => { ++ let sourceElement = element.source.boundId ? getElementById(board, element.source.boundId) : undefined; ++ let targetElement = element.target.boundId ? getElementById(board, element.target.boundId) : undefined; ++ if (!sourceElement) { ++ const source = handleRefPair.source; ++ sourceElement = createFakeElement(source.point, source.vector); ++ } ++ if (!targetElement) { ++ const target = handleRefPair.target; ++ targetElement = createFakeElement(target.point, target.vector); ++ } ++ let sourceRectangle = RectangleClient.getRectangleByPoints(sourceElement.points); ++ const rotatedSourceCornerPoints = rotatePointsByElement(RectangleClient.getCornerPoints(sourceRectangle), sourceElement) || ++ RectangleClient.getCornerPoints(sourceRectangle); ++ sourceRectangle = RectangleClient.getRectangleByPoints(rotatedSourceCornerPoints); ++ sourceRectangle = RectangleClient.inflate(sourceRectangle, getStrokeWidthByElement(sourceElement) * 2); ++ let targetRectangle = RectangleClient.getRectangleByPoints(targetElement.points); ++ const rotatedTargetCornerPoints = rotatePointsByElement(RectangleClient.getCornerPoints(targetRectangle), targetElement) || ++ RectangleClient.getCornerPoints(targetRectangle); ++ targetRectangle = RectangleClient.getRectangleByPoints(rotatedTargetCornerPoints); ++ targetRectangle = RectangleClient.inflate(targetRectangle, getStrokeWidthByElement(targetElement) * 2); ++ return { ++ sourceRectangle, ++ targetRectangle ++ }; ++}; ++const createFakeElement = (startPoint, vector) => { ++ const point = getPointByVectorComponent(startPoint, vector, -25); ++ const points = RectangleClient.getPoints(RectangleClient.getRectangleByCenterPoint(point, 50, 50)); ++ return createGeometryElement(BasicShapes.rectangle, points, ''); ++}; ++function getNextRenderPoints(board, element, renderPoints) { ++ let newRenderKeyPoints = renderPoints ?? getElbowPoints(board, element); ++ const [nextSourcePoint, nextTargetPoint] = getNextSourceAndTargetPoints(board, element); ++ newRenderKeyPoints.splice(0, 1, nextSourcePoint); ++ newRenderKeyPoints.splice(-1, 1, nextTargetPoint); ++ return removeDuplicatePoints(newRenderKeyPoints); ++} ++ ++const getSelectedDrawElements = (board, elements) => { ++ const selectedElements = elements?.length ? elements : getSelectedElements(board); ++ return selectedElements.filter(value => PlaitDrawElement.isDrawElement(value)); ++}; ++const getSelectedGeometryElements = (board) => { ++ const selectedElements = getSelectedElements(board).filter(value => PlaitDrawElement.isGeometry(value)); ++ return selectedElements; ++}; ++const getSelectedCustomGeometryElements = (board) => { ++ const selectedElements = getSelectedElements(board).filter(value => PlaitDrawElement.isCustomGeometryElement(board, value)); ++ return selectedElements; ++}; ++const getSelectedArrowLineElements = (board) => { ++ const selectedElements = getSelectedElements(board).filter(value => PlaitDrawElement.isArrowLine(value)); ++ return selectedElements; ++}; ++const getSelectedVectorLineElements = (board) => { ++ const selectedElements = getSelectedElements(board).filter(value => PlaitDrawElement.isVectorLine(value)); ++ return selectedElements; ++}; ++const getSelectedImageElements = (board) => { ++ const selectedElements = getSelectedElements(board).filter(value => PlaitDrawElement.isImage(value)); ++ return selectedElements; ++}; ++const isSingleSelectSwimlane = (board) => { ++ const selectedElements = getSelectedElements(board); ++ return selectedElements && selectedElements.length === 1 && PlaitDrawElement.isSwimlane(selectedElements[0]); ++}; ++const isSingleSelectLine = (board) => { ++ const selectedElements = getSelectedElements(board); ++ return selectedElements && selectedElements.length === 1 && PlaitDrawElement.isLine(selectedElements[0]); ++}; ++const getSelectedSwimlane = (board) => { ++ const selectedElements = getSelectedElements(board); ++ return selectedElements.find(item => PlaitDrawElement.isSwimlane(item)); ++}; ++ ++const insertGeometry = (board, points, shape) => { ++ const newElement = createDefaultGeometry(board, points, shape); ++ insertElement(board, newElement); ++ return newElement; ++}; ++const insertText = (board, point, text) => { ++ const memorizedLatest = getMemorizedLatestByPointer(BasicShapes.text); ++ const property = getTextShapeProperty(board, text, memorizedLatest.textProperties['font-size']); ++ const points = [point, [point[0] + property.width, point[1] + property.height]]; ++ const newElement = createTextElement(board, points, text); ++ insertElement(board, newElement); ++}; ++const resizeGeometry = (board, points, path) => { ++ const normalizePoints = normalizeShapePoints(points); ++ const element = PlaitNode.get(board, path); ++ const newProperties = { points: normalizePoints, text: { ...element.text } }; ++ if (PlaitDrawElement.isText(element) && element.autoSize) { ++ newProperties.autoSize = false; ++ } ++ Transforms.setNode(board, newProperties, path); ++}; ++const switchGeometryShape = (board, shape) => { ++ const selectedElements = getSelectedElements(board); ++ const refs = []; ++ selectedElements.forEach((item) => { ++ if (PlaitDrawElement.isGeometry(item) && !PlaitDrawElement.isText(item)) { ++ const path = PlaitBoard.findPath(board, item); ++ Transforms.setNode(board, { shape }, path); ++ collectArrowLineUpdatedRefsByGeometry(board, { ...item, shape }, refs); ++ } ++ }); ++ if (refs.length) { ++ refs.forEach((ref) => { ++ DrawTransforms.resizeArrowLine(board, ref.property, ref.path); ++ }); ++ } ++}; ++ ++const normalizePoints = (board, element, width, height) => { ++ let points = element.points; ++ let autoSize = PlaitDrawElement.isText(element) ? element.autoSize : false; ++ const defaultSpace = ShapeDefaultSpace.rectangleAndText; ++ if (autoSize) { ++ const newWidth = width < MIN_TEXT_WIDTH ? MIN_TEXT_WIDTH : width; ++ const editor = getFirstTextEditor(element); ++ if (AlignEditor.isActive(editor, Alignment.right)) { ++ points = [ ++ [points[1][0] - (newWidth + defaultSpace * 2), points[0][1]], ++ [points[1][0], points[0][1] + height] ++ ]; ++ } ++ else if (AlignEditor.isActive(editor, Alignment.center)) { ++ const oldWidth = Math.abs(points[0][0] - points[1][0]); ++ const offset = (newWidth - oldWidth) / 2; ++ points = [ ++ [points[0][0] - offset - defaultSpace, points[0][1]], ++ [points[1][0] + offset + defaultSpace, points[0][1] + height] ++ ]; ++ } ++ else { ++ points = [points[0], [points[0][0] + newWidth + defaultSpace * 2, points[0][1] + height]]; ++ } ++ if (hasValidAngle(element)) { ++ points = resetPointsAfterResize(RectangleClient.getRectangleByPoints(element.points), RectangleClient.getRectangleByPoints(points), RectangleClient.getCenterPoint(RectangleClient.getRectangleByPoints(element.points)), RectangleClient.getCenterPoint(RectangleClient.getRectangleByPoints(points)), element.angle); ++ } ++ } ++ return { points }; ++}; ++const setText = (board, element, text, width, height) => { ++ const newElement = { ++ text, ++ ...normalizePoints(board, element, width, height) ++ }; ++ const path = board.children.findIndex((child) => child === element); ++ Transforms.setNode(board, newElement, [path]); ++}; ++const setTextSize = (board, element, width, height) => { ++ const isAutoSize = PlaitDrawElement.isText(element) ? element.autoSize : false; ++ if (isAutoSize) { ++ const newElement = { ++ ...normalizePoints(board, element, width, height) ++ }; ++ const path = board.children.findIndex((child) => child === element); ++ Transforms.setNode(board, newElement, [path]); ++ } ++}; ++ ++const insertImage = (board, imageItem, startPoint) => { ++ const { width, height, url } = imageItem; ++ const viewportWidth = PlaitBoard.getBoardContainer(board).clientWidth; ++ const viewportHeight = PlaitBoard.getBoardContainer(board).clientHeight; ++ const point = toViewBoxPoint(board, toHostPoint(board, viewportWidth / 2, viewportHeight / 2)); ++ const points = startPoint ++ ? [startPoint, [startPoint[0] + width, startPoint[1] + height]] ++ : [ ++ [point[0] - width / 2, point[1] - height / 2], ++ [point[0] + width / 2, point[1] + height / 2] ++ ]; ++ const imageElement = { ++ id: idCreator(), ++ type: 'image', ++ points, ++ url ++ }; ++ Transforms.insertNode(board, imageElement, [board.children.length]); ++ Transforms.addSelectionWithTemporaryElements(board, [imageElement]); ++}; ++const createImage = (startPoint, imageItem) => { ++ const { width, height, url } = imageItem; ++ const points = [startPoint, [startPoint[0] + width, startPoint[1] + height]]; ++ const imageElement = { ++ id: idCreator(), ++ type: 'image', ++ points, ++ url ++ }; ++ return imageElement; ++}; ++ ++const resizeArrowLine = (board, options, path) => { ++ Transforms.setNode(board, options, path); ++}; ++const setArrowLineTexts = (board, element, texts) => { ++ const path = PlaitBoard.findPath(board, element); ++ Transforms.setNode(board, { texts }, path); ++}; ++const removeArrowLineText = (board, element, index) => { ++ const path = PlaitBoard.findPath(board, element); ++ const texts = element.texts?.length ? [...element.texts] : []; ++ const newTexts = [...texts]; ++ newTexts.splice(index, 1); ++ Transforms.setNode(board, { texts: newTexts }, path); ++}; ++const setArrowLineMark = (board, handleKey, marker) => { ++ memorizeLatest(MemorizeKey.arrowLine, handleKey, marker); ++ const selectedElements = getSelectedArrowLineElements(board); ++ selectedElements.forEach((element) => { ++ const path = PlaitBoard.findPath(board, element); ++ let handle = handleKey === ArrowLineHandleKey.source ? element.source : element.target; ++ handle = { ...handle, marker }; ++ Transforms.setNode(board, { [handleKey]: handle }, path); ++ }); ++}; ++const setArrowLineShape = (board, newProperties) => { ++ const elements = getSelectedArrowLineElements(board); ++ elements.map(element => { ++ const _properties = { ...newProperties }; ++ if (element.shape === newProperties.shape) { ++ return; ++ } ++ const path = PlaitBoard.findPath(board, element); ++ Transforms.setNode(board, _properties, path); ++ }); ++}; ++const connectArrowLineToDraw = (board, lineElement, handle, geometryElement) => { ++ const linePoints = PlaitArrowLine.getPoints(board, lineElement); ++ const point = handle === ArrowLineHandleKey.source ? linePoints[0] : linePoints[linePoints.length - 1]; ++ const connection = getHitConnection(board, point, geometryElement); ++ if (connection) { ++ let source = lineElement.source; ++ let target = lineElement.target; ++ if (handle === ArrowLineHandleKey.source) { ++ source = { ++ ...source, ++ boundId: geometryElement.id, ++ connection ++ }; ++ } ++ else { ++ target = { ++ ...target, ++ boundId: geometryElement.id, ++ connection ++ }; ++ } ++ const path = PlaitBoard.findPath(board, lineElement); ++ resizeArrowLine(board, { source, target }, path); ++ } ++}; ++ ++function buildSwimlaneTable(element) { ++ const swimlaneElement = { ...element }; ++ if (PlaitDrawElement.isHorizontalSwimlane(element)) { ++ swimlaneElement.cells = element.cells.map((item, index) => { ++ if (index === 0 && element.header) { ++ item = { ++ ...element.cells[0], ++ rowspan: element.rows.length ++ }; ++ } ++ if (item.text && !item.text.direction) { ++ item = { ++ ...item, ++ text: { ++ ...item.text, ++ direction: 'vertical' ++ } ++ }; ++ } ++ return item; ++ }); ++ return swimlaneElement; ++ } ++ if (element.header) { ++ swimlaneElement.cells = [ ++ { ++ ...element.cells[0], ++ colspan: element.columns.length ++ }, ++ ...element.cells.slice(1, element.cells.length) ++ ]; ++ } ++ return swimlaneElement; ++} ++const getDefaultSwimlanePoints = (pointer, centerPoint) => { ++ const property = DefaultSwimlanePropertyMap[pointer]; ++ return RectangleClient.getPoints(RectangleClient.getRectangleByCenterPoint(centerPoint, property.width, property.height)); ++}; ++const createDefaultSwimlane = (shape, points) => { ++ const header = isSwimlaneWithHeader(shape); ++ const dataShape = adjustSwimlaneShape(shape); ++ const width = points[1][0] - points[0][0]; ++ const height = points[1][1] - points[0][1]; ++ const rows = createDefaultRowsOrColumns(dataShape, 'row', header, height); ++ const columns = createDefaultRowsOrColumns(dataShape, 'column', header, width); ++ const swimlane = { ++ id: idCreator(), ++ type: 'swimlane', ++ shape: dataShape, ++ points, ++ rows, ++ columns, ++ header, ++ cells: createDefaultCells(dataShape, rows, columns, header) ++ }; ++ return swimlane; ++}; ++const createDefaultRowsOrColumns = (shape, type, header, size) => { ++ const createItems = (count) => new Array(count).fill('').map(() => ({ id: idCreator() })); ++ let data = createItems(3); ++ if ((type === 'row' && shape === SwimlaneSymbols.swimlaneVertical) || ++ (type === 'column' && shape === SwimlaneSymbols.swimlaneHorizontal)) { ++ data = header ? data : createItems(2); ++ const dimension = type === 'row' ? 'height' : 'width'; ++ let defaultSize = SWIMLANE_HEADER_SIZE; ++ if (size < SWIMLANE_HEADER_SIZE * data.length) { ++ defaultSize = Math.min((size / data.length / SWIMLANE_HEADER_SIZE) * SWIMLANE_HEADER_SIZE, SWIMLANE_HEADER_SIZE); ++ } ++ data = data.map((item, index) => { ++ if (index === 0 || (index === 1 && header)) { ++ return { ++ ...item, ++ [dimension]: defaultSize ++ }; ++ } ++ return item; ++ }); ++ } ++ return data; ++}; ++const createDefaultCells = (shape, rows, columns, header) => { ++ let headerCell = []; ++ let startIndex = 0; ++ if (header) { ++ headerCell = [createCell(rows[0].id, columns[0].id, 'New Swimlane')]; ++ startIndex = 1; ++ } ++ const cells = new Array(6).fill('').map((_, index) => { ++ if (index < 3) { ++ const rowId = shape === SwimlaneSymbols.swimlaneVertical ? rows[startIndex].id : rows[index].id; ++ const columnId = shape === SwimlaneSymbols.swimlaneVertical ? columns[index].id : columns[startIndex].id; ++ return createCell(rowId, columnId, header ? 'Lane' : 'New Swimlane'); ++ } ++ const rowId = shape === SwimlaneSymbols.swimlaneVertical ? rows[startIndex + 1].id : rows[index - 3].id; ++ const columnId = shape === SwimlaneSymbols.swimlaneVertical ? columns[index - 3].id : columns[startIndex + 1].id; ++ return createCell(rowId, columnId); ++ }); ++ return [...headerCell, ...cells]; ++}; ++const getSwimlaneCount = (swimlane) => { ++ if (PlaitDrawElement.isHorizontalSwimlane(swimlane)) { ++ return swimlane.rows.length; ++ } ++ if (PlaitDrawElement.isVerticalSwimlane(swimlane)) { ++ return swimlane.columns.length; ++ } ++ return 0; ++}; ++const isSwimlaneWithHeader = (shape) => { ++ return [SwimlaneDrawSymbols.swimlaneHorizontalWithHeader, SwimlaneDrawSymbols.swimlaneVerticalWithHeader].includes(shape); ++}; ++const isSwimlaneShape = (shape) => { ++ return getSwimlaneShapes().includes(shape); ++}; ++const adjustSwimlaneShape = (shape) => { ++ return [SwimlaneDrawSymbols.swimlaneHorizontalWithHeader, SwimlaneDrawSymbols.swimlaneHorizontal].includes(shape) ++ ? SwimlaneSymbols.swimlaneHorizontal ++ : SwimlaneSymbols.swimlaneVertical; ++}; ++const isSwimlanePointers = (board, pointer = board.pointer) => { ++ return getSwimlanePointers().includes(pointer); ++}; ++ ++const updateSwimlaneCount = (board, swimlane, count) => { ++ if (count > 0 && PlaitDrawElement.isSwimlane(swimlane)) { ++ const currentCount = getSwimlaneCount(swimlane); ++ if (PlaitDrawElement.isHorizontalSwimlane(swimlane)) { ++ if (count > currentCount) { ++ addSwimlaneRow(board, swimlane, swimlane.rows.length, count - currentCount); ++ } ++ else { ++ const deleteIndex = swimlane.rows.length - (currentCount - count); ++ removeSwimlaneRow(board, swimlane, deleteIndex, currentCount - count); ++ } ++ } ++ if (PlaitDrawElement.isVerticalSwimlane(swimlane)) { ++ if (count > currentCount) { ++ addSwimlaneColumn(board, swimlane, swimlane.columns.length, count - currentCount); ++ } ++ else { ++ const deleteIndex = swimlane.columns.length - (currentCount - count); ++ removeSwimlaneColumn(board, swimlane, deleteIndex, currentCount - count); ++ } ++ } ++ } ++}; ++const addSwimlaneRow = (board, swimlane, index, count = 1) => { ++ if (PlaitDrawElement.isHorizontalSwimlane(swimlane)) { ++ const newRows = [...swimlane.rows]; ++ const addRows = []; ++ for (let i = 0; i < count; i++) { ++ addRows.push({ id: idCreator() }); ++ } ++ newRows.splice(index, 0, ...addRows); ++ const newCells = [...swimlane.cells]; ++ addRows.forEach(item => { ++ newCells.push(...createNewSwimlaneCells(swimlane, item.id, 'column')); ++ }); ++ const lastCellPoints = getCellWithPoints(board, swimlane, swimlane.cells[swimlane.cells.length - 1].id).points; ++ const lastRowHeight = RectangleClient.getRectangleByPoints(lastCellPoints).height; ++ const newPoints = [swimlane.points[0], [swimlane.points[1][0], swimlane.points[1][1] + lastRowHeight * count]]; ++ updateSwimlane(board, swimlane, swimlane.columns, newRows, newCells, newPoints); ++ } ++}; ++const addSwimlaneColumn = (board, swimlane, index, count = 1) => { ++ if (PlaitDrawElement.isVerticalSwimlane(swimlane)) { ++ const newColumns = [...swimlane.columns]; ++ const addColumns = []; ++ for (let i = 0; i < count; i++) { ++ addColumns.push({ id: idCreator() }); ++ } ++ newColumns.splice(index, 0, ...addColumns); ++ const newCells = [...swimlane.cells]; ++ addColumns.forEach(item => { ++ newCells.push(...createNewSwimlaneCells(swimlane, item.id, 'row')); ++ }); ++ const lastCellPoints = getCellWithPoints(board, swimlane, swimlane.cells[swimlane.cells.length - 1].id).points; ++ const lastColumnWidth = RectangleClient.getRectangleByPoints(lastCellPoints).width; ++ const newPoints = [swimlane.points[0], [swimlane.points[1][0] + lastColumnWidth * count, swimlane.points[1][1]]]; ++ updateSwimlane(board, swimlane, newColumns, swimlane.rows, newCells, newPoints); ++ } ++}; ++const removeSwimlaneRow = (board, swimlane, index, count = 1) => { ++ if (PlaitDrawElement.isHorizontalSwimlane(swimlane)) { ++ if (count > swimlane.rows.length) { ++ return; ++ } ++ const newRows = [...swimlane.rows]; ++ newRows.splice(index, count); ++ if (newRows.length === 0) { ++ const path = PlaitBoard.findPath(board, swimlane); ++ Transforms.removeNode(board, path); ++ } ++ else { ++ let newCells = [...swimlane.cells]; ++ const removeRows = []; ++ for (let i = index; i < count + index; i++) { ++ const removeRow = swimlane.rows[i]; ++ removeRows.push(removeRow); ++ newCells = newCells.filter(item => item.rowId !== removeRow.id); ++ } ++ let removeRowHeight = 0; ++ removeRows.forEach(row => { ++ if (!row.height) { ++ const rowCell = swimlane.cells.find(item => item.rowId === row.id); ++ const cellPoints = getCellWithPoints(board, swimlane, rowCell.id).points; ++ removeRowHeight += RectangleClient.getRectangleByPoints(cellPoints).height; ++ } ++ else { ++ removeRowHeight += row.height; ++ } ++ }); ++ const newPoints = [swimlane.points[0], [swimlane.points[1][0], swimlane.points[1][1] - removeRowHeight]]; ++ updateSwimlane(board, swimlane, swimlane.columns, newRows, newCells, newPoints); ++ } ++ } ++}; ++const removeSwimlaneColumn = (board, swimlane, index, count = 1) => { ++ if (PlaitDrawElement.isVerticalSwimlane(swimlane)) { ++ if (count > swimlane.columns.length) { ++ return; ++ } ++ const newColumns = [...swimlane.columns]; ++ newColumns.splice(index, count); ++ if (newColumns.length === 0) { ++ const path = PlaitBoard.findPath(board, swimlane); ++ Transforms.removeNode(board, path); ++ } ++ else { ++ let newCells = [...swimlane.cells]; ++ const removeColumns = []; ++ for (let i = index; i < count + index; i++) { ++ const removeColumn = swimlane.columns[i]; ++ removeColumns.push(removeColumn); ++ newCells = newCells.filter(item => item.columnId !== removeColumn.id); ++ } ++ let removeColumnWidth = 0; ++ removeColumns.forEach(column => { ++ if (!column.width) { ++ const rowCell = swimlane.cells.find(item => item.columnId === column.id); ++ const cellPoints = getCellWithPoints(board, swimlane, rowCell.id).points; ++ removeColumnWidth += RectangleClient.getRectangleByPoints(cellPoints).width; ++ } ++ else { ++ removeColumnWidth += column.width; ++ } ++ }); ++ const newPoints = [swimlane.points[0], [swimlane.points[1][0] - removeColumnWidth, swimlane.points[1][1]]]; ++ updateSwimlane(board, swimlane, newColumns, swimlane.rows, newCells, newPoints); ++ } ++ } ++}; ++const createNewSwimlaneCells = (swimlane, newId, type) => { ++ const cells = swimlane[`${type}s`].map(item => ({ ++ id: idCreator(), ++ rowId: type === 'row' ? item.id : newId, ++ columnId: type === 'row' ? newId : item.id ++ })); ++ if (swimlane.header) { ++ cells.shift(); ++ } ++ cells[0] = { ++ ...cells[0], ++ text: { ++ children: [{ text: swimlane.header ? 'Lane' : 'New Swimlane' }], ++ align: Alignment.center, ++ direction: type === 'row' ? undefined : 'vertical' ++ } ++ }; ++ return cells; ++}; ++const updateSwimlane = (board, swimlane, newColumns, newRows, newCells, newPoints) => { ++ const path = PlaitBoard.findPath(board, swimlane); ++ Transforms.setNode(board, { ++ columns: newColumns, ++ rows: newRows, ++ cells: newCells, ++ points: newPoints ++ }, path); ++}; ++ ++const setDrawTexts = (board, element, text) => { ++ const newTexts = element.texts?.map(item => { ++ if (item.id === text.id) { ++ return { ...item, ...text }; ++ } ++ return item; ++ }); ++ const newElement = { ++ texts: newTexts ++ }; ++ const path = board.children.findIndex(child => child === element); ++ Transforms.setNode(board, newElement, [path]); ++}; ++ ++const setTableText = (board, path, cellId, text, textHeight) => { ++ const table = PlaitNode.get(board, path); ++ const cell = getCellWithPoints(board, table, cellId); ++ const cellIndex = table.cells.findIndex((item) => item.id === cell.id); ++ let rows = [...table.rows]; ++ let columns = [...table.columns]; ++ let cells = [...table.cells]; ++ let points = [...table.points]; ++ const { width: cellWidth, height: cellHeight } = RectangleClient.getRectangleByPoints(cell.points); ++ const defaultSpace = ShapeDefaultSpace.rectangleAndText; ++ if (PlaitTableElement.isVerticalText(cell)) { ++ const columnIdx = table.columns.findIndex((column) => column.id === cell.columnId); ++ if (textHeight > cellWidth) { ++ const newColumnWidth = textHeight + defaultSpace * 2; ++ const offset = newColumnWidth - cellWidth; ++ const result = updateColumns(table, table.columns[columnIdx].id, newColumnWidth, offset); ++ points = result.points; ++ columns = result.columns; ++ } ++ } ++ else { ++ const rowIdx = table.rows.findIndex((row) => row.id === cell.rowId); ++ const tableRow = table.rows[rowIdx]; ++ const compareHeight = tableRow.height ?? Math.max(cellHeight, 0); ++ if (textHeight > compareHeight) { ++ const newRowHeight = textHeight + defaultSpace * 2; ++ const offset = newRowHeight - compareHeight; ++ const result = updateRows(table, table.rows[rowIdx].id, newRowHeight, offset); ++ points = result.points; ++ rows = result.rows; ++ } ++ } ++ cells[cellIndex] = { ++ ...cells[cellIndex], ++ text ++ }; ++ Transforms.setNode(board, { rows, columns, cells, points }, path); ++}; ++ ++const setTableFill = (board, element, fill, path) => { ++ const selectedCells = getSelectedCells(element); ++ let newCells = element.cells; ++ if (selectedCells?.length) { ++ newCells = element.cells.map(cell => { ++ if (selectedCells.map(item => item.id).includes(cell.id)) { ++ return getNewCell(cell, fill); ++ } ++ return cell; ++ }); ++ } ++ else { ++ newCells = element.cells.map(cell => { ++ if (cell.text) { ++ return getNewCell(cell, fill); ++ } ++ return cell; ++ }); ++ } ++ Transforms.setNode(board, { cells: newCells }, path); ++}; ++const getNewCell = (cell, fill) => { ++ const newCell = { ++ ...cell ++ }; ++ if (fill) { ++ newCell.fill = fill; ++ } ++ else { ++ delete newCell.fill; ++ } ++ return newCell; ++}; ++ ++const setVectorLineShape = (board, newProperties) => { ++ const elements = getSelectedVectorLineElements(board); ++ elements.map(element => { ++ if (element.shape === newProperties.shape) { ++ return; ++ } ++ const path = PlaitBoard.findPath(board, element); ++ Transforms.setNode(board, { ...newProperties }, path); ++ }); ++}; ++ ++const insertDrawByVector = (board, point, shape, vector) => { ++ const swimlanePointers = getSwimlanePointers(); ++ const isSwimlanePointer = swimlanePointers.includes(shape); ++ let shapeProperty = DefaultFlowchartPropertyMap[shape] || ++ DefaultBasicShapePropertyMap[shape] || ++ DefaultUMLPropertyMap[shape] || ++ DefaultBasicShapeProperty; ++ if (isSwimlanePointer) { ++ shapeProperty = DefaultSwimlanePropertyMap[shape]; ++ } ++ const direction = getDirectionByVector(vector); ++ if (direction) { ++ let offset = 0; ++ if ([Direction.left, Direction.right].includes(direction)) { ++ offset = -shapeProperty.width / 2; ++ } ++ else { ++ offset = -shapeProperty.height / 2; ++ } ++ const vectorPoint = getPointByVectorComponent(point, vector, offset); ++ const points = RectangleClient.getPoints(RectangleClient.getRectangleByCenterPoint(vectorPoint, shapeProperty.width, shapeProperty.height)); ++ if (isSwimlanePointer) { ++ const swimlane = createDefaultSwimlane(shape, points); ++ insertElement(board, swimlane); ++ return swimlane; ++ } ++ return insertGeometry(board, points, shape); ++ } ++ return null; ++}; ++ ++const DrawTransforms = { ++ setText, ++ setDrawTexts, ++ insertGeometry, ++ resizeGeometry, ++ insertText, ++ setTextSize, ++ resizeArrowLine, ++ setArrowLineTexts, ++ removeArrowLineText, ++ setArrowLineMark, ++ setArrowLineShape, ++ setVectorLineShape, ++ insertImage, ++ connectArrowLineToDraw, ++ switchGeometryShape, ++ setTableText, ++ addSwimlaneRow, ++ addSwimlaneColumn, ++ removeSwimlaneRow, ++ removeSwimlaneColumn, ++ updateSwimlaneCount, ++ setTableFill, ++ insertDrawByVector ++}; ++ ++const getHitRectangleResizeHandleRef = (board, rectangle, point, angle = 0) => { ++ const centerPoint = RectangleClient.getCenterPoint(rectangle); ++ const resizeHandleRefs = getRectangleResizeHandleRefs(rectangle, RESIZE_HANDLE_DIAMETER); ++ if (angle) { ++ const rotatedPoint = rotatePoints([point], centerPoint, -angle)[0]; ++ let result = resizeHandleRefs.find((resizeHandleRef) => { ++ return RectangleClient.isHit(RectangleClient.getRectangleByPoints([rotatedPoint, rotatedPoint]), resizeHandleRef.rectangle); ++ }); ++ if (result) { ++ result.cursorClass = getRotatedResizeCursorClassByAngle(result.cursorClass, angle); ++ } ++ return result; ++ } ++ else { ++ return resizeHandleRefs.find((resizeHandleRef) => { ++ return RectangleClient.isHit(RectangleClient.getRectangleByPoints([point, point]), resizeHandleRef.rectangle); ++ }); ++ } ++}; ++const getRotateHandleRectangle = (rectangle) => { ++ return { ++ x: rectangle.x - ROTATE_HANDLE_DISTANCE_TO_ELEMENT - ROTATE_HANDLE_SIZE, ++ y: rectangle.y + rectangle.height + ROTATE_HANDLE_DISTANCE_TO_ELEMENT, ++ width: ROTATE_HANDLE_SIZE, ++ height: ROTATE_HANDLE_SIZE ++ }; ++}; ++ ++const debugKey$2 = 'debug:plait:resize-for-rotation'; ++const debugGenerator$3 = createDebugGenerator(debugKey$2); ++function withDrawResize(board) { ++ const { afterChange, drawSelectionRectangle } = board; ++ let snapG; ++ let handleG; ++ let needCustomActiveRectangle = false; ++ let resizeActivePoints = null; ++ const canResize = () => { ++ const elements = getSelectedElements(board); ++ return (elements.length >= 1 && ++ elements.every((el) => (PlaitDrawElement.isDrawElement(el) || (PlaitDrawElement.isCustomGeometryElement(board, el) && el.points.length > 1)) && ++ !isSingleSelectLine(board) && ++ !isSingleSelectSwimlane(board))); ++ }; ++ const options = { ++ key: 'draw-elements', ++ canResize, ++ hitTest: (point) => { ++ const elements = getSelectedElements(board); ++ const boundingRectangle = getRectangleByElements(board, elements, false); ++ const angle = getSelectionAngle(elements); ++ const handleRef = getHitRectangleResizeHandleRef(board, boundingRectangle, point, angle); ++ if (handleRef) { ++ return { ++ element: [...elements], ++ rectangle: boundingRectangle, ++ handle: handleRef.handle, ++ cursorClass: handleRef.cursorClass ++ }; ++ } ++ return null; ++ }, ++ onResize: (resizeRef, resizeState) => { ++ snapG?.remove(); ++ debugGenerator$3.isDebug() && debugGenerator$3.clear(); ++ const isFromCorner = isCornerHandle(board, resizeRef.handle); ++ const isAspectRatio = resizeState.isShift || (resizeRef.element.length === 1 && PlaitDrawElement.isImage(resizeRef.element[0])); ++ const centerPoint = RectangleClient.getCenterPoint(resizeRef.rectangle); ++ const handleIndex = getIndexByResizeHandle(resizeRef.handle); ++ const { originPoint, handlePoint } = getResizeOriginPointAndHandlePoint(board, handleIndex, resizeRef.rectangle); ++ const angle = getSelectionAngle(resizeRef.element); ++ let bulkRotationRef; ++ if (angle) { ++ bulkRotationRef = { ++ angle: angle, ++ offsetX: 0, ++ offsetY: 0, ++ newCenterPoint: [0, 0] ++ }; ++ const [rotatedStartPoint, rotateEndPoint] = rotatePoints([resizeState.startPoint, resizeState.endPoint], centerPoint, -bulkRotationRef.angle); ++ resizeState.startPoint = rotatedStartPoint; ++ resizeState.endPoint = rotateEndPoint; ++ } ++ const resizeSnapRefOptions = getSnapResizingRefOptions(board, resizeRef, resizeState, { ++ originPoint, ++ handlePoint ++ }, isAspectRatio, isFromCorner); ++ const resizeSnapRef = getSnapResizingRef(board, resizeRef.element, resizeSnapRefOptions); ++ resizeActivePoints = resizeSnapRef.activePoints; ++ snapG = resizeSnapRef.snapG; ++ PlaitBoard.getElementTopHost(board).append(snapG); ++ if (bulkRotationRef) { ++ const boundingBoxCornerPoints = RectangleClient.getPoints(resizeRef.rectangle); ++ const resizedBoundingBoxCornerPoints = boundingBoxCornerPoints.map((p) => { ++ return movePointByZoomAndOriginPoint(p, originPoint, resizeSnapRef.xZoom, resizeSnapRef.yZoom); ++ }); ++ const newBoundingBox = RectangleClient.getRectangleByPoints(resizedBoundingBoxCornerPoints); ++ debugGenerator$3.isDebug() && debugGenerator$3.drawRectangle(board, newBoundingBox, { stroke: 'blue' }); ++ const newBoundingBoxCenter = RectangleClient.getCenterPoint(newBoundingBox); ++ const adjustedNewBoundingBoxPoints = resetPointsAfterResize(RectangleClient.getRectangleByPoints(boundingBoxCornerPoints), RectangleClient.getRectangleByPoints(resizedBoundingBoxCornerPoints), centerPoint, newBoundingBoxCenter, bulkRotationRef.angle); ++ const newCenter = RectangleClient.getCenterPoint(RectangleClient.getRectangleByPoints(adjustedNewBoundingBoxPoints)); ++ bulkRotationRef = Object.assign(bulkRotationRef, { ++ offsetX: newCenter[0] - newBoundingBoxCenter[0], ++ offsetY: newCenter[1] - newBoundingBoxCenter[1], ++ newCenterPoint: newCenter ++ }); ++ debugGenerator$3.isDebug() && debugGenerator$3.drawRectangle(board, adjustedNewBoundingBoxPoints); ++ } ++ resizeRef.element.forEach((target) => { ++ const path = PlaitBoard.findPath(board, target); ++ let points; ++ if (bulkRotationRef) { ++ const reversedPoints = rotatedDataPoints(target.points, centerPoint, -bulkRotationRef.angle); ++ points = reversedPoints.map((p) => { ++ return movePointByZoomAndOriginPoint(p, originPoint, resizeSnapRef.xZoom, resizeSnapRef.yZoom); ++ }); ++ const adjustTargetPoints = points.map((p) => [ ++ p[0] + bulkRotationRef.offsetX, ++ p[1] + bulkRotationRef.offsetY ++ ]); ++ points = rotatedDataPoints(adjustTargetPoints, bulkRotationRef.newCenterPoint, bulkRotationRef.angle); ++ } ++ else { ++ if (hasValidAngle(target)) { ++ needCustomActiveRectangle = true; ++ } ++ if (hasValidAngle(target) && isAxisChangedByAngle(target.angle)) { ++ points = getResizePointsByOtherwiseAxis(board, target.points, originPoint, resizeSnapRef.xZoom, resizeSnapRef.yZoom); ++ } ++ else { ++ points = target.points.map((p) => { ++ return movePointByZoomAndOriginPoint(p, originPoint, resizeSnapRef.xZoom, resizeSnapRef.yZoom); ++ }); ++ } ++ } ++ if (PlaitDrawElement.isGeometry(target)) { ++ DrawTransforms.resizeGeometry(board, points, path); ++ } ++ else if (PlaitDrawElement.isLine(target) || ++ PlaitDrawElement.isCustomGeometryElement(board, target) || ++ PlaitDrawElement.isVectorLine(target)) { ++ Transforms.setNode(board, { points }, path); ++ } ++ else if (PlaitDrawElement.isImage(target)) { ++ if (isAspectRatio) { ++ Transforms.setNode(board, { points }, path); ++ } ++ else { ++ // The image element does not follow the resize, but moves based on the center point. ++ const targetRectangle = RectangleClient.getRectangleByPoints(target.points); ++ const centerPoint = RectangleClient.getCenterPoint(targetRectangle); ++ const newCenterPoint = movePointByZoomAndOriginPoint(centerPoint, originPoint, resizeSnapRef.xZoom, resizeSnapRef.yZoom); ++ const newTargetRectangle = RectangleClient.getRectangleByCenterPoint(newCenterPoint, targetRectangle.width, targetRectangle.height); ++ Transforms.setNode(board, { points: RectangleClient.getPoints(newTargetRectangle) }, path); ++ } ++ } ++ }); ++ }, ++ afterResize: (resizeRef) => { ++ snapG?.remove(); ++ snapG = null; ++ if (needCustomActiveRectangle) { ++ needCustomActiveRectangle = false; ++ resizeActivePoints = null; ++ const selectedElements = getSelectedElements(board); ++ Transforms.addSelectionWithTemporaryElements(board, selectedElements); ++ } ++ } ++ }; ++ withResize(board, options); ++ board.afterChange = () => { ++ afterChange(); ++ if (handleG) { ++ handleG.remove(); ++ handleG = null; ++ } ++ const selectedElements = getSelectedElements(board); ++ if (canResize() && !isSelectionMoving(board) && selectedElements.length > 1) { ++ handleG = generatorResizeHandles(board, resizeActivePoints, needCustomActiveRectangle); ++ PlaitBoard.getActiveHost(board).append(handleG); ++ } ++ }; ++ board.drawSelectionRectangle = () => { ++ if (needCustomActiveRectangle) { ++ const rectangle = RectangleClient.getRectangleByPoints(resizeActivePoints); ++ const activeRectangle = toActiveRectangleFromViewBoxRectangle(board, rectangle); ++ return drawRectangle(board, RectangleClient.inflate(activeRectangle, ACTIVE_STROKE_WIDTH), { ++ stroke: SELECTION_BORDER_COLOR, ++ strokeWidth: ACTIVE_STROKE_WIDTH ++ }); ++ } ++ return drawSelectionRectangle(); ++ }; ++ return board; ++} ++const getResizeOriginPointAndHandlePoint = (board, handleIndex, rectangle) => { ++ const symmetricHandleIndex = getSymmetricHandleIndex(board, handleIndex); ++ const originPoint = getResizeHandlePointByIndex(rectangle, symmetricHandleIndex); ++ const handlePoint = getResizeHandlePointByIndex(rectangle, handleIndex); ++ return { ++ originPoint, ++ handlePoint ++ }; ++}; ++const getResizeZoom = (resizeStartAndEnd, resizeOriginPoint, resizeHandlePoint, isFromCorner, isAspectRatio) => { ++ const [startPoint, endPoint] = resizeStartAndEnd; ++ let xZoom = 0; ++ let yZoom = 0; ++ if (isFromCorner) { ++ if (isAspectRatio) { ++ let normalizedOffsetX = Point.getOffsetX(startPoint, endPoint); ++ xZoom = normalizedOffsetX / (resizeHandlePoint[0] - resizeOriginPoint[0]); ++ yZoom = xZoom; ++ } ++ else { ++ let normalizedOffsetX = Point.getOffsetX(startPoint, endPoint); ++ let normalizedOffsetY = Point.getOffsetY(startPoint, endPoint); ++ xZoom = normalizedOffsetX / (resizeHandlePoint[0] - resizeOriginPoint[0]); ++ yZoom = normalizedOffsetY / (resizeHandlePoint[1] - resizeOriginPoint[1]); ++ } ++ } ++ else { ++ const isHorizontal = Point.isHorizontal(resizeOriginPoint, resizeHandlePoint, 0.1) || false; ++ let normalizedOffset = isHorizontal ? Point.getOffsetX(startPoint, endPoint) : Point.getOffsetY(startPoint, endPoint); ++ let benchmarkOffset = isHorizontal ? resizeHandlePoint[0] - resizeOriginPoint[0] : resizeHandlePoint[1] - resizeOriginPoint[1]; ++ const zoom = normalizedOffset / benchmarkOffset; ++ if (isAspectRatio) { ++ xZoom = zoom; ++ yZoom = zoom; ++ } ++ else { ++ if (isHorizontal) { ++ xZoom = zoom; ++ } ++ else { ++ yZoom = zoom; ++ } ++ } ++ } ++ return { xZoom, yZoom }; ++}; ++const movePointByZoomAndOriginPoint = (p, resizeOriginPoint, xZoom, yZoom) => { ++ const offsetX = (p[0] - resizeOriginPoint[0]) * xZoom; ++ const offsetY = (p[1] - resizeOriginPoint[1]) * yZoom; ++ return [p[0] + offsetX, p[1] + offsetY]; ++}; ++/** ++ * 1. Rotate 90° ++ * 2. Scale based on the rotated points ++ * 3. Reverse rotate the scaled points by 90° ++ */ ++const getResizePointsByOtherwiseAxis = (board, points, resizeOriginPoint, xZoom, yZoom) => { ++ const currentRectangle = RectangleClient.getRectangleByPoints(points); ++ debugGenerator$3.isDebug() && debugGenerator$3.drawRectangle(board, currentRectangle, { stroke: 'black' }); ++ let resultPoints = points; ++ resultPoints = rotatePoints(resultPoints, RectangleClient.getCenterPoint(currentRectangle), (1 / 2) * Math.PI); ++ debugGenerator$3.isDebug() && debugGenerator$3.drawRectangle(board, resultPoints, { stroke: 'blue' }); ++ resultPoints = resultPoints.map((p) => { ++ return movePointByZoomAndOriginPoint(p, resizeOriginPoint, xZoom, yZoom); ++ }); ++ debugGenerator$3.isDebug() && debugGenerator$3.drawRectangle(board, resultPoints); ++ const newRectangle = RectangleClient.getRectangleByPoints(resultPoints); ++ return rotatePoints(resultPoints, RectangleClient.getCenterPoint(newRectangle), -(1 / 2) * Math.PI); ++}; ++const generatorResizeHandles = (board, resizeActivePoints, needCustomActiveRectangle) => { ++ const handleG = createG(); ++ const elements = getSelectedElements(board); ++ const boundingRectangle = needCustomActiveRectangle ++ ? RectangleClient.getRectangleByPoints(resizeActivePoints) ++ : getRectangleByElements(board, elements, false); ++ const boundingActiveRectangle = toActiveRectangleFromViewBoxRectangle(board, boundingRectangle); ++ let corners = RectangleClient.getCornerPoints(boundingActiveRectangle); ++ const angle = getSelectionAngle(elements); ++ if (angle) { ++ const centerPoint = RectangleClient.getCenterPoint(boundingActiveRectangle); ++ corners = rotatePoints(corners, centerPoint, angle); ++ } ++ corners.forEach((corner) => { ++ const g = drawHandle(board, corner); ++ handleG.append(g); ++ }); ++ return handleG; ++}; ++ ++const debugKey$1 = 'debug:plait:point-for-geometry'; ++const debugGenerator$2 = createDebugGenerator(debugKey$1); ++const EQUAL_SPACING = 10; ++function getSnapResizingRefOptions(board, resizeRef, resizeState, resizeOriginPointAndHandlePoint, isAspectRatio, isFromCorner) { ++ const { originPoint, handlePoint } = resizeOriginPointAndHandlePoint; ++ const resizePoints = [resizeState.startPoint, resizeState.endPoint]; ++ const { xZoom, yZoom } = getResizeZoom(resizePoints, originPoint, handlePoint, isFromCorner, isAspectRatio); ++ let activeElements; ++ let resizeOriginPoint = []; ++ if (Array.isArray(resizeRef.element)) { ++ activeElements = resizeRef.element; ++ const rectangle = getRectangleByElements(board, resizeRef.element, false); ++ resizeOriginPoint = RectangleClient.getPoints(rectangle); ++ } ++ else { ++ activeElements = [resizeRef.element]; ++ resizeOriginPoint = resizeRef.element.points; ++ } ++ const points = resizeOriginPoint.map(p => { ++ return movePointByZoomAndOriginPoint(p, originPoint, xZoom, yZoom); ++ }); ++ const rectangle = RectangleClient.getRectangleByPoints(points); ++ const activeRectangle = getRectangleByAngle(rectangle, getSelectionAngle(activeElements)); ++ const resizeHandlePoint = movePointByZoomAndOriginPoint(handlePoint, originPoint, xZoom, yZoom); ++ const [x, y] = getUnitVectorByPointAndPoint(originPoint, resizeHandlePoint); ++ return { ++ resizePoints, ++ resizeOriginPoint, ++ activeRectangle, ++ originPoint, ++ handlePoint, ++ directionFactors: [getDirectionFactorByDirectionComponent(x), getDirectionFactorByDirectionComponent(y)], ++ isAspectRatio, ++ isFromCorner ++ }; ++} ++function getSnapResizingRef(board, activeElements, resizeSnapOptions) { ++ const snapG = createG(); ++ const snapRectangles = getSnapRectangles(board, activeElements); ++ let snapLineDelta = getIsometricLineDelta(snapRectangles, resizeSnapOptions); ++ if (snapLineDelta.deltaX === 0 && snapLineDelta.deltaY === 0) { ++ snapLineDelta = getSnapPointDelta(snapRectangles, resizeSnapOptions); ++ } ++ const angle = getSelectionAngle(activeElements); ++ const activePointAndZoom = getActivePointAndZoom(snapLineDelta, resizeSnapOptions, angle); ++ const isometricLinesG = drawIsometricSnapLines(board, activePointAndZoom.activePoints, snapRectangles, resizeSnapOptions, angle); ++ const pointLinesG = drawResizingPointSnapLines(board, activePointAndZoom.activePoints, snapRectangles, resizeSnapOptions, angle); ++ snapG.append(isometricLinesG, pointLinesG); ++ return { ...activePointAndZoom, ...snapLineDelta, snapG }; ++} ++function getSnapPointDelta(snapRectangles, resizeSnapOptions) { ++ let pointLineDelta = { ++ deltaX: 0, ++ deltaY: 0 ++ }; ++ const { isAspectRatio, activeRectangle, directionFactors } = resizeSnapOptions; ++ const drawHorizontal = directionFactors[0] !== 0 || isAspectRatio; ++ const drawVertical = directionFactors[1] !== 0 || isAspectRatio; ++ if (drawHorizontal) { ++ const xSnapAxis = getTripleAxis(activeRectangle, true); ++ const pointX = directionFactors[0] === -1 ? xSnapAxis[0] : xSnapAxis[2]; ++ const deltaX = getMinPointDelta(snapRectangles, pointX, true); ++ if (Math.abs(deltaX) < SNAP_TOLERANCE) { ++ pointLineDelta.deltaX = deltaX; ++ if (pointLineDelta.deltaX !== 0 && isAspectRatio) { ++ pointLineDelta.deltaY = pointLineDelta.deltaX / (activeRectangle.width / activeRectangle.height); ++ return pointLineDelta; ++ } ++ } ++ } ++ if (drawVertical) { ++ const ySnapAxis = getTripleAxis(activeRectangle, false); ++ const pointY = directionFactors[1] === -1 ? ySnapAxis[0] : ySnapAxis[2]; ++ const deltaY = getMinPointDelta(snapRectangles, pointY, false); ++ if (Math.abs(deltaY) < SNAP_TOLERANCE) { ++ pointLineDelta.deltaY = deltaY; ++ if (pointLineDelta.deltaY !== 0 && isAspectRatio) { ++ pointLineDelta.deltaX = pointLineDelta.deltaY * (activeRectangle.width / activeRectangle.height); ++ return pointLineDelta; ++ } ++ } ++ } ++ return pointLineDelta; ++} ++function getActivePointAndZoom(resizeSnapDelta, resizeSnapOptions, angle) { ++ const { deltaX, deltaY } = resizeSnapDelta; ++ const { resizePoints, isCreate } = resizeSnapOptions; ++ const newResizePoints = [resizePoints[0], [resizePoints[1][0] + deltaX, resizePoints[1][1] + deltaY]]; ++ let activePoints = newResizePoints; ++ let xZoom = 0; ++ let yZoom = 0; ++ if (!isCreate) { ++ const { originPoint, handlePoint, isFromCorner, isAspectRatio, resizeOriginPoint } = resizeSnapOptions; ++ const resizeZoom = getResizeZoom(newResizePoints, originPoint, handlePoint, isFromCorner, isAspectRatio); ++ xZoom = resizeZoom.xZoom; ++ yZoom = resizeZoom.yZoom; ++ activePoints = resizeOriginPoint.map(p => { ++ return movePointByZoomAndOriginPoint(p, originPoint, xZoom, yZoom); ++ }); ++ if (angle) { ++ activePoints = resetPointsAfterResize(RectangleClient.getRectangleByPoints(resizeOriginPoint), RectangleClient.getRectangleByPoints(activePoints), RectangleClient.getCenterPoint(RectangleClient.getRectangleByPoints(resizeOriginPoint)), RectangleClient.getCenterPoint(RectangleClient.getRectangleByPoints(activePoints)), angle); ++ } ++ } ++ return { ++ xZoom, ++ yZoom, ++ activePoints ++ }; ++} ++function getIsometricLineDelta(snapRectangles, resizeSnapOptions) { ++ let isometricLineDelta = { ++ deltaX: 0, ++ deltaY: 0 ++ }; ++ const { isAspectRatio, activeRectangle } = resizeSnapOptions; ++ const widthSnapRectangle = snapRectangles.find(item => Math.abs(item.width - activeRectangle.width) < SNAP_TOLERANCE); ++ if (widthSnapRectangle) { ++ const deltaWidth = widthSnapRectangle.width - activeRectangle.width; ++ isometricLineDelta.deltaX = deltaWidth * resizeSnapOptions.directionFactors[0]; ++ if (isAspectRatio) { ++ const deltaHeight = deltaWidth / (activeRectangle.width / activeRectangle.height); ++ isometricLineDelta.deltaY = deltaHeight * resizeSnapOptions.directionFactors[1]; ++ return isometricLineDelta; ++ } ++ } ++ const heightSnapRectangle = snapRectangles.find(item => Math.abs(item.height - activeRectangle.height) < SNAP_TOLERANCE); ++ if (heightSnapRectangle) { ++ const deltaHeight = heightSnapRectangle.height - activeRectangle.height; ++ isometricLineDelta.deltaY = deltaHeight * resizeSnapOptions.directionFactors[1]; ++ if (isAspectRatio) { ++ const deltaWidth = deltaHeight * (activeRectangle.width / activeRectangle.height); ++ isometricLineDelta.deltaX = deltaWidth * resizeSnapOptions.directionFactors[0]; ++ return isometricLineDelta; ++ } ++ } ++ return isometricLineDelta; ++} ++function getIsometricLinePoints(rectangle, isHorizontal) { ++ return isHorizontal ++ ? [ ++ [rectangle.x, rectangle.y - EQUAL_SPACING], ++ [rectangle.x + rectangle.width, rectangle.y - EQUAL_SPACING] ++ ] ++ : [ ++ [rectangle.x - EQUAL_SPACING, rectangle.y], ++ [rectangle.x - EQUAL_SPACING, rectangle.y + rectangle.height] ++ ]; ++} ++function drawResizingPointSnapLines(board, activePoints, snapRectangles, resizeSnapOptions, angle) { ++ debugGenerator$2.isDebug() && debugGenerator$2.clear(); ++ const rectangle = RectangleClient.getRectangleByPoints(activePoints); ++ const activeRectangle = getRectangleByAngle(rectangle, angle); ++ const { isAspectRatio, directionFactors } = resizeSnapOptions; ++ const drawHorizontal = directionFactors[0] !== 0 || isAspectRatio; ++ const drawVertical = directionFactors[1] !== 0 || isAspectRatio; ++ return drawPointSnapLines(board, activeRectangle, snapRectangles, drawHorizontal, drawVertical); ++} ++function drawIsometricSnapLines(board, activePoints, snapRectangles, resizeSnapOptions, angle) { ++ let widthIsometricPoints = []; ++ let heightIsometricPoints = []; ++ const drawHorizontalLine = resizeSnapOptions.directionFactors[0] !== 0 || resizeSnapOptions.isAspectRatio; ++ const drawVerticalLine = resizeSnapOptions.directionFactors[1] !== 0 || resizeSnapOptions.isAspectRatio; ++ const rectangle = RectangleClient.getRectangleByPoints(activePoints); ++ const activeRectangle = getRectangleByAngle(rectangle, angle); ++ for (let snapRectangle of snapRectangles) { ++ if (activeRectangle.width === snapRectangle.width && drawHorizontalLine) { ++ widthIsometricPoints.push(getIsometricLinePoints(snapRectangle, true)); ++ } ++ if (activeRectangle.height === snapRectangle.height && drawVerticalLine) { ++ heightIsometricPoints.push(getIsometricLinePoints(snapRectangle, false)); ++ } ++ } ++ if (widthIsometricPoints.length && drawHorizontalLine) { ++ widthIsometricPoints.push(getIsometricLinePoints(activeRectangle, true)); ++ } ++ if (heightIsometricPoints.length && drawVerticalLine) { ++ heightIsometricPoints.push(getIsometricLinePoints(activeRectangle, false)); ++ } ++ const isometricLines = [...widthIsometricPoints, ...heightIsometricPoints]; ++ return drawSolidLines(board, isometricLines); ++} ++ ++const buildClipboardData = (board, elements, startPoint) => { ++ return buildClipboardData$1(board, elements, startPoint, (element) => { ++ if (PlaitDrawElement.isArrowLine(element)) { ++ let source = { ...element.source }; ++ let target = { ...element.target }; ++ let points = [...element.points]; ++ if (element.source.boundId) { ++ points[0] = getConnectionPoint(getElementById(board, element.source.boundId), element.source.connection); ++ if (!getElementById(board, element.source.boundId, elements)) { ++ delete source.boundId; ++ delete source.connection; ++ } ++ } ++ if (element.target.boundId) { ++ points[points.length - 1] = getConnectionPoint(getElementById(board, element.target.boundId), element.target.connection); ++ if (!getElementById(board, element.target.boundId, elements)) { ++ delete target.boundId; ++ delete target.connection; ++ } ++ } ++ points = points.map(point => [point[0] - startPoint[0], point[1] - startPoint[1]]); ++ return { ...element, points, source, target }; ++ } ++ return undefined; ++ }); ++}; ++const insertClipboardData = (board, elements, startPoint) => { ++ insertClipboardData$1(board, elements, startPoint, (element, idsMap) => { ++ if (PlaitDrawElement.isArrowLine(element)) { ++ if (element.source.boundId) { ++ const boundElement = elements.find(item => [element.source.boundId, idsMap[element.source.boundId]].includes(item.id)); ++ if (boundElement) { ++ element.source.boundId = idsMap[element.source.boundId]; ++ } ++ } ++ if (element.target.boundId) { ++ const boundElement = elements.find(item => [element.target.boundId, idsMap[element.target.boundId]].includes(item.id)); ++ if (boundElement) { ++ element.target.boundId = idsMap[element.target.boundId]; ++ } ++ } ++ } ++ if (PlaitDrawElement.isElementByTable(element)) { ++ updateRowOrColumnIds(element, 'row'); ++ updateRowOrColumnIds(element, 'column'); ++ updateCellIds(element.cells); ++ } ++ }); ++}; ++ ++const heightRatio$1 = 3 / 4; ++const CommentEngine = { ++ draw(board, rectangle, options) { ++ const points = getCommentPoints(rectangle); ++ const rs = PlaitBoard.getRoughSVG(board); ++ const polygon = rs.polygon(points, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(polygon, 'round'); ++ return polygon; ++ }, ++ isInsidePoint(rectangle, point) { ++ const parallelogramPoints = getCommentPoints(rectangle); ++ return isPointInPolygon(point, parallelogramPoints); ++ }, ++ getCornerPoints(rectangle) { ++ return getCommentPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ return getNearestPointBetweenPointAndSegments(point, getCommentPoints(rectangle)); ++ }, ++ getEdgeByConnectionPoint(rectangle, pointOfRectangle) { ++ const corners = getCommentPoints(rectangle); ++ const point = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ return getPolygonEdgeByConnectionPoint(corners, point); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle: (board, element) => { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const textRectangle = getTextRectangle$1(board, element); ++ textRectangle.y = elementRectangle.y + (elementRectangle.height * heightRatio$1 - textRectangle.height) / 2; ++ return textRectangle; ++ } ++}; ++const getCommentPoints = (rectangle) => { ++ return [ ++ [rectangle.x, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height * heightRatio$1], ++ [rectangle.x + (rectangle.width * 3) / 5, rectangle.y + rectangle.height * heightRatio$1], ++ [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height], ++ [rectangle.x + (rectangle.width * 2) / 5, rectangle.y + rectangle.height * heightRatio$1], ++ [rectangle.x, rectangle.y + rectangle.height * heightRatio$1] ++ ]; ++}; ++ ++function createPolygonEngine(options) { ++ const getPoints = options.getPolygonPoints; ++ const engine = { ++ draw(board, rectangle, options) { ++ const points = getPoints(rectangle); ++ const rs = PlaitBoard.getRoughSVG(board); ++ const polygon = rs.polygon(points, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(polygon, 'round'); ++ return polygon; ++ }, ++ isInsidePoint(rectangle, point) { ++ const points = getPoints(rectangle); ++ return isPointInPolygon(point, points); ++ }, ++ getCornerPoints(rectangle) { ++ return getPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ return getNearestPointBetweenPointAndSegments(point, getPoints(rectangle)); ++ }, ++ getNearestCrossingPoint(rectangle, point) { ++ const corners = getPoints(rectangle); ++ const crossingPoints = getCrossingPointBetweenPointAndPolygon(corners, point); ++ let nearestPoint = crossingPoints[0]; ++ let nearestDistance = distanceBetweenPointAndPoint(point[0], point[1], nearestPoint[0], nearestPoint[1]); ++ crossingPoints ++ .filter((v, index) => index > 0) ++ .forEach(crossingPoint => { ++ let distance = distanceBetweenPointAndPoint(point[0], point[1], crossingPoint[0], crossingPoint[1]); ++ if (distance < nearestDistance) { ++ nearestDistance = distance; ++ nearestPoint = crossingPoint; ++ } ++ }); ++ return nearestPoint; ++ }, ++ getEdgeByConnectionPoint(rectangle, pointOfRectangle) { ++ const corners = getPoints(rectangle); ++ const point = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ return getPolygonEdgeByConnectionPoint(corners, point); ++ }, ++ getConnectorPoints(rectangle) { ++ if (options.getConnectorPoints) { ++ return options.getConnectorPoints(rectangle); ++ } ++ return getPoints(rectangle); ++ } ++ }; ++ if (options.getTextRectangle) { ++ engine.getTextRectangle = options.getTextRectangle; ++ } ++ return engine; ++} ++ ++const getCrossPoints = (rectangle) => { ++ return [ ++ [rectangle.x + rectangle.width / 4, rectangle.y], ++ [rectangle.x + (rectangle.width * 3) / 4, rectangle.y], ++ [rectangle.x + (rectangle.width * 3) / 4, rectangle.y + rectangle.height / 4], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height / 4], ++ [rectangle.x + rectangle.width, rectangle.y + (rectangle.height * 3) / 4], ++ [rectangle.x + (rectangle.width * 3) / 4, rectangle.y + (rectangle.height * 3) / 4], ++ [rectangle.x + (rectangle.width * 3) / 4, rectangle.y + rectangle.height], ++ [rectangle.x + rectangle.width / 4, rectangle.y + rectangle.height], ++ [rectangle.x + rectangle.width / 4, rectangle.y + (rectangle.height * 3) / 4], ++ [rectangle.x, rectangle.y + (rectangle.height * 3) / 4], ++ [rectangle.x, rectangle.y + rectangle.height / 4], ++ [rectangle.x + rectangle.width / 4, rectangle.y + rectangle.height / 4] ++ ]; ++}; ++const CrossEngine = createPolygonEngine({ ++ getPolygonPoints: getCrossPoints, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle: (board, element) => { ++ const widthRatio = 1 / 2; ++ return getCustomTextRectangle(board, element, widthRatio); ++ } ++}); ++ ++const DiamondEngine = createPolygonEngine({ ++ getPolygonPoints: RectangleClient.getEdgeCenterPoints, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle: (board, element) => { ++ return getCustomTextRectangle(board, element, 1 / 2); ++ } ++}); ++ ++function createEllipseEngine(createOptions) { ++ const engine = { ++ draw(board, rectangle, options) { ++ const centerPoint = [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height / 2]; ++ const rs = PlaitBoard.getRoughSVG(board); ++ const shape = rs.ellipse(centerPoint[0], centerPoint[1], rectangle.width, rectangle.height, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ const centerPoint = [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height / 2]; ++ return isPointInEllipse(point, centerPoint, rectangle.width / 2, rectangle.height / 2); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ const centerPoint = [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height / 2]; ++ return getNearestPointBetweenPointAndEllipse(point, centerPoint, rectangle.width / 2, rectangle.height / 2); ++ }, ++ getTangentVectorByConnectionPoint(rectangle, pointOfRectangle) { ++ const connectionPoint = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ const centerPoint = [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height / 2]; ++ const point = [connectionPoint[0] - centerPoint[0], -(connectionPoint[1] - centerPoint[1])]; ++ const a = rectangle.width / 2; ++ const b = rectangle.height / 2; ++ const slope = getEllipseTangentSlope(point[0], point[1], a, b); ++ const vector = getVectorFromPointAndSlope(point[0], point[1], slope); ++ return vector; ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle: (board, element) => { ++ return getCustomTextRectangle(board, element, 3 / 4); ++ } ++ }; ++ if (createOptions?.draw) { ++ engine.draw = createOptions.draw; ++ } ++ if (createOptions?.getTextRectangle) { ++ engine.getTextRectangle = createOptions.getTextRectangle; ++ } ++ return engine; ++} ++const EllipseEngine = createEllipseEngine(); ++ ++const getHexagonPoints = (rectangle) => { ++ return [ ++ [rectangle.x + rectangle.width / 4, rectangle.y], ++ [rectangle.x + (rectangle.width * 3) / 4, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2], ++ [rectangle.x + (rectangle.width * 3) / 4, rectangle.y + rectangle.height], ++ [rectangle.x + rectangle.width / 4, rectangle.y + rectangle.height], ++ [rectangle.x, rectangle.y + rectangle.height / 2] ++ ]; ++}; ++const HexagonEngine = createPolygonEngine({ ++ getPolygonPoints: getHexagonPoints, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle: (board, element) => { ++ return getCustomTextRectangle(board, element, 3 / 4); ++ } ++}); ++ ++const getLeftArrowPoints = (rectangle) => { ++ return [ ++ [rectangle.x, rectangle.y + rectangle.height / 2], ++ [rectangle.x + rectangle.width * 0.32, rectangle.y], ++ [rectangle.x + rectangle.width * 0.32, rectangle.y + rectangle.height * 0.2], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height * 0.2], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height * 0.8], ++ [rectangle.x + rectangle.width * 0.32, rectangle.y + rectangle.height * 0.8], ++ [rectangle.x + rectangle.width * 0.32, rectangle.y + rectangle.height] ++ ]; ++}; ++const LeftArrowEngine = createPolygonEngine({ ++ getPolygonPoints: getLeftArrowPoints, ++ getConnectorPoints: (rectangle) => { ++ return [ ++ [rectangle.x, rectangle.y + rectangle.height / 2], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2] ++ ]; ++ }, ++ getTextRectangle: (board, element) => { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const customTextRectangle = getCustomTextRectangle(board, element, 1 - 0.32); ++ customTextRectangle.x = elementRectangle.x + elementRectangle.width * 0.32 + ShapeDefaultSpace.rectangleAndText; ++ return customTextRectangle; ++ } ++}); ++ ++const getOctagonPoints = (rectangle) => { ++ return [ ++ [rectangle.x + (rectangle.width * 3) / 10, rectangle.y], ++ [rectangle.x + (rectangle.width * 7) / 10, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y + (rectangle.height * 3) / 10], ++ [rectangle.x + rectangle.width, rectangle.y + (rectangle.height * 7) / 10], ++ [rectangle.x + (rectangle.width * 7) / 10, rectangle.y + rectangle.height], ++ [rectangle.x + (rectangle.width * 3) / 10, rectangle.y + rectangle.height], ++ [rectangle.x, rectangle.y + (rectangle.height * 7) / 10], ++ [rectangle.x, rectangle.y + (rectangle.height * 3) / 10] ++ ]; ++}; ++const OctagonEngine = createPolygonEngine({ ++ getPolygonPoints: getOctagonPoints, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle: (board, element) => { ++ return getCustomTextRectangle(board, element, 3 / 4); ++ } ++}); ++ ++const getParallelogramPoints = (rectangle) => { ++ return [ ++ [rectangle.x + rectangle.width / 4, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y], ++ [rectangle.x + (rectangle.width * 3) / 4, rectangle.y + rectangle.height], ++ [rectangle.x, rectangle.y + rectangle.height] ++ ]; ++}; ++const ParallelogramEngine = createPolygonEngine({ ++ getPolygonPoints: getParallelogramPoints, ++ getConnectorPoints: (rectangle) => { ++ const cornerPoints = getParallelogramPoints(rectangle); ++ return getCenterPointsOnPolygon$1(cornerPoints); ++ }, ++ getTextRectangle: (board, element) => { ++ return getCustomTextRectangle(board, element, 1 / 2); ++ } ++}); ++ ++const getPentagonPoints = (rectangle) => { ++ return [ ++ [rectangle.x + rectangle.width / 2, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y + (rectangle.height * 2) / 5], ++ [rectangle.x + (rectangle.width * 4) / 5, rectangle.y + rectangle.height], ++ [rectangle.x + rectangle.width / 5, rectangle.y + rectangle.height], ++ [rectangle.x, rectangle.y + (rectangle.height * 2) / 5] ++ ]; ++}; ++const PentagonEngine = createPolygonEngine({ ++ getPolygonPoints: getPentagonPoints, ++ getTextRectangle: (board, element) => { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const customTextRectangle = getCustomTextRectangle(board, element, 3 / 5); ++ const startY = elementRectangle.y + elementRectangle.height / 5; ++ const endY = elementRectangle.y + elementRectangle.height; ++ customTextRectangle.y = startY + (endY - startY - customTextRectangle.height) / 2; ++ return customTextRectangle; ++ } ++}); ++ ++const getPentagonArrowPoints = (rectangle) => { ++ const wider = rectangle.width > rectangle.height / 2; ++ return [ ++ [rectangle.x, rectangle.y], ++ [rectangle.x + (wider ? rectangle.width - rectangle.height / 2 : 0), rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2], ++ [rectangle.x + (wider ? rectangle.width - rectangle.height / 2 : 0), rectangle.y + rectangle.height], ++ [rectangle.x, rectangle.y + rectangle.height] ++ ]; ++}; ++const PentagonArrowEngine = createPolygonEngine({ ++ getPolygonPoints: getPentagonArrowPoints, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle: (board, element) => { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const customTextRectangle = getCustomTextRectangle(board, element, 3 / 4); ++ customTextRectangle.x = elementRectangle.x + ShapeDefaultSpace.rectangleAndText; ++ return customTextRectangle; ++ } ++}); ++ ++const getProcessArrowPoints = (rectangle) => { ++ const wider = rectangle.width > rectangle.height / 2; ++ return [ ++ [rectangle.x, rectangle.y], ++ [rectangle.x + (wider ? rectangle.width - rectangle.height / 2 : 0), rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2], ++ [rectangle.x + (wider ? rectangle.width - rectangle.height / 2 : 0), rectangle.y + rectangle.height], ++ [rectangle.x, rectangle.y + rectangle.height], ++ [rectangle.x + (wider ? rectangle.height / 2 : rectangle.width), rectangle.y + rectangle.height / 2] ++ ]; ++}; ++const ProcessArrowEngine = createPolygonEngine({ ++ getPolygonPoints: getProcessArrowPoints, ++ getTextRectangle: (board, element) => { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const wider = elementRectangle.width > elementRectangle.height + 20; ++ const widthRatio = wider ? (elementRectangle.width - elementRectangle.height) / elementRectangle.width : 3 / 4; ++ const customTextRectangle = getCustomTextRectangle(board, element, widthRatio); ++ return customTextRectangle; ++ } ++}); ++ ++const getRightArrowPoints = (rectangle) => { ++ return [ ++ [rectangle.x, rectangle.y + rectangle.height * 0.2], ++ [rectangle.x + rectangle.width * 0.68, rectangle.y + rectangle.height * 0.2], ++ [rectangle.x + rectangle.width * 0.68, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2], ++ [rectangle.x + rectangle.width * 0.68, rectangle.y + rectangle.height], ++ [rectangle.x + rectangle.width * 0.68, rectangle.y + rectangle.height * 0.8], ++ [rectangle.x, rectangle.y + rectangle.height * 0.8] ++ ]; ++}; ++const RightArrowEngine = createPolygonEngine({ ++ getPolygonPoints: getRightArrowPoints, ++ getConnectorPoints: (rectangle) => { ++ return [ ++ [rectangle.x, rectangle.y + rectangle.height / 2], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2] ++ ]; ++ }, ++ getTextRectangle: (board, element) => { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const customTextRectangle = getCustomTextRectangle(board, element, 1 - 0.32); ++ customTextRectangle.x = elementRectangle.x + ShapeDefaultSpace.rectangleAndText; ++ return customTextRectangle; ++ } ++}); ++ ++const RectangleEngine = { ++ draw(board, rectangle, options) { ++ return drawRectangle(board, rectangle, { ...options, fillStyle: 'solid' }); ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ return getNearestPointBetweenPointAndSegments(point, RectangleEngine.getCornerPoints(rectangle)); ++ }, ++ getEdgeByConnectionPoint(rectangle, pointOfRectangle) { ++ const corners = RectangleEngine.getCornerPoints(rectangle); ++ const point = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ return getPolygonEdgeByConnectionPoint(corners, point); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ } ++}; ++ ++const RoundRectangleEngine = { ++ draw(board, rectangle, options) { ++ return drawRoundRectangle(PlaitBoard.getRoughSVG(board), rectangle.x, rectangle.y, rectangle.x + rectangle.width, rectangle.y + rectangle.height, { ...options, fillStyle: 'solid' }, false, getRoundRectangleRadius(rectangle)); ++ }, ++ isInsidePoint(rectangle, point) { ++ return isPointInRoundRectangle(point, rectangle, getRoundRectangleRadius(rectangle)); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ return getNearestPointBetweenPointAndRoundRectangle$1(point, rectangle, getRoundRectangleRadius(rectangle)); ++ }, ++ getEdgeByConnectionPoint(rectangle, pointOfRectangle) { ++ const corners = RectangleEngine.getCornerPoints(rectangle); ++ const point = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ return getPolygonEdgeByConnectionPoint(corners, point); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ } ++}; ++const getRoundRectangleRadius = (rectangle) => { ++ return Math.min(rectangle.width * 0.1, rectangle.height * 0.1); ++}; ++function getNearestPointBetweenPointAndRoundRectangle$1(point, rectangle, radius) { ++ const { x: rectX, y: rectY, width, height } = rectangle; ++ const cornerPoints = RectangleClient.getCornerPoints(rectangle); ++ let result = getNearestPointBetweenPointAndSegments(point, cornerPoints); ++ let circleCenter = null; ++ const inLeftTop = point[0] >= rectX && point[0] <= rectX + radius && point[1] >= rectY && point[1] <= rectY + radius; ++ if (inLeftTop) { ++ circleCenter = [rectX + radius, rectY + radius]; ++ } ++ const inLeftBottom = point[0] >= rectX && point[0] <= rectX + radius && point[1] >= rectY + height && point[1] <= rectY + height - radius; ++ if (inLeftBottom) { ++ circleCenter = [rectX + radius, rectY + height - radius]; ++ } ++ const inRightTop = point[0] >= rectX + width - radius && point[0] <= rectX + width && point[1] >= rectY && point[1] <= rectY + radius; ++ if (inRightTop) { ++ circleCenter = [rectX + width - radius, rectY + radius]; ++ } ++ const inRightBottom = point[0] >= rectX + width - radius && ++ point[0] <= rectX + width && ++ point[1] >= rectY + height - radius && ++ point[1] <= rectY + height; ++ if (inRightBottom) { ++ circleCenter = [rectX + width - radius, rectY + height - radius]; ++ } ++ if (circleCenter) { ++ result = getNearestPointBetweenPointAndEllipse(point, circleCenter, radius, radius); ++ } ++ return result; ++} ++ ++const heightRatio = 3 / 4; ++const RoundCommentEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const x1 = rectangle.x; ++ const y1 = rectangle.y; ++ const x2 = rectangle.x + rectangle.width; ++ const y2 = rectangle.y + rectangle.height * heightRatio; ++ const radius = getRoundRectangleRadius(rectangle); ++ const point1 = [x1 + radius, y1]; ++ const point2 = [x2 - radius, y1]; ++ const point3 = [x2, y1 + radius]; ++ const point4 = [x2, y2 - radius]; ++ const point5 = [x2 - radius, y2]; ++ const point6 = [x1 + radius, y2]; ++ const point7 = [x1, y2 - radius]; ++ const point8 = [x1, y1 + radius]; ++ const point9 = [x1 + rectangle.width / 4, y2]; ++ const point10 = [x1 + rectangle.width / 4, rectangle.y + rectangle.height]; ++ const point11 = [x1 + rectangle.width / 2, y2]; ++ const shape = rs.path(`M${point2[0]} ${point2[1]} A ${radius} ${radius}, 0, 0, 1, ${point3[0]} ${point3[1]} L ${point4[0]} ${point4[1]} A ${radius} ${radius}, 0, 0, 1, ${point5[0]} ${point5[1]} L ${point11[0]} ${point11[1]} ${point10[0]} ${point10[1]} ${point9[0]} ${point9[1]} ${point6[0]} ${point6[1]} A ${radius} ${radius}, 0, 0, 1, ${point7[0]} ${point7[1]} L ${point8[0]} ${point8[1]} A ${radius} ${radius}, 0, 0, 1, ${point1[0]} ${point1[1]} Z`, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ const points = [ ++ [rectangle.x + rectangle.width / 4, rectangle.y + (rectangle.height * 3) / 4], ++ [rectangle.x + rectangle.width / 4, rectangle.y + rectangle.height], ++ [rectangle.x + rectangle.width / 2, rectangle.y + (rectangle.height * 3) / 4] ++ ]; ++ rectangle.height = (rectangle.height * 3) / 4; ++ return isPointInPolygon(point, points) || isPointInRoundRectangle(point, rectangle, getRoundRectangleRadius(rectangle)); ++ }, ++ getCornerPoints(rectangle) { ++ return getRoundCommentPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ return getNearestPointBetweenPointAndSegments(point, getRoundCommentPoints(rectangle)); ++ }, ++ getEdgeByConnectionPoint(rectangle, pointOfRectangle) { ++ const corners = getRoundCommentPoints(rectangle); ++ const point = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ return getPolygonEdgeByConnectionPoint(corners, point); ++ }, ++ getConnectorPoints(rectangle) { ++ return [ ++ [rectangle.x + rectangle.width / 2, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2], ++ [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height * heightRatio], ++ [rectangle.x, rectangle.y + rectangle.height / 2] ++ ]; ++ }, ++ getTextRectangle: (board, element) => { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const textRectangle = getTextRectangle$1(board, element); ++ textRectangle.y = elementRectangle.y + (elementRectangle.height * heightRatio - textRectangle.height) / 2; ++ return textRectangle; ++ } ++}; ++const getRoundCommentPoints = (rectangle) => { ++ return [ ++ [rectangle.x, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height * heightRatio], ++ [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height * heightRatio], ++ [rectangle.x + rectangle.width / 4, rectangle.y + rectangle.height], ++ [rectangle.x + rectangle.width / 4, rectangle.y + rectangle.height * heightRatio], ++ [rectangle.x, rectangle.y + rectangle.height * heightRatio] ++ ]; ++}; ++ ++const getTrapezoidPoints = (rectangle) => { ++ return [ ++ [rectangle.x + rectangle.width * 0.15, rectangle.y], ++ [rectangle.x + rectangle.width * 0.85, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height], ++ [rectangle.x, rectangle.y + rectangle.height] ++ ]; ++}; ++const TrapezoidEngine = createPolygonEngine({ ++ getPolygonPoints: getTrapezoidPoints, ++ getConnectorPoints(rectangle) { ++ const points = getTrapezoidPoints(rectangle); ++ return getCenterPointsOnPolygon$1(points); ++ }, ++ getTextRectangle(board, element) { ++ return getCustomTextRectangle(board, element, 3 / 4); ++ } ++}); ++ ++const getTrianglePoints = (rectangle) => { ++ return [ ++ [rectangle.x + rectangle.width / 2, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height], ++ [rectangle.x, rectangle.y + rectangle.height] ++ ]; ++}; ++const TriangleEngine = createPolygonEngine({ ++ getPolygonPoints: getTrianglePoints, ++ getConnectorPoints(rectangle) { ++ const cornerPoints = getTrianglePoints(rectangle); ++ const lineCenterPoints = getCenterPointsOnPolygon$1(cornerPoints); ++ return [...lineCenterPoints, ...cornerPoints]; ++ }, ++ getTextRectangle: (board, element) => { ++ const customTextRectangle = getCustomTextRectangle(board, element, 1 / 2); ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ customTextRectangle.y = ++ elementRectangle.y + ++ (elementRectangle.height * 2.5) / 5 + ++ (elementRectangle.height - (elementRectangle.height * 2.5) / 5 - customTextRectangle.height) / 2; ++ return customTextRectangle; ++ } ++}); ++ ++const getTwoWayArrowPoints = (rectangle) => { ++ return [ ++ [rectangle.x, rectangle.y + rectangle.height / 2], ++ [rectangle.x + (rectangle.width * 8) / 25, rectangle.y], ++ [rectangle.x + (rectangle.width * 8) / 25, rectangle.y + rectangle.height / 5], ++ [rectangle.x + (rectangle.width * 17) / 25, rectangle.y + rectangle.height / 5], ++ [rectangle.x + (rectangle.width * 17) / 25, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2], ++ [rectangle.x + (rectangle.width * 17) / 25, rectangle.y + rectangle.height], ++ [rectangle.x + (rectangle.width * 17) / 25, rectangle.y + (rectangle.height * 4) / 5], ++ [rectangle.x + (rectangle.width * 8) / 25, rectangle.y + (rectangle.height * 4) / 5], ++ [rectangle.x + (rectangle.width * 8) / 25, rectangle.y + rectangle.height] ++ ]; ++}; ++const TwoWayArrowEngine = createPolygonEngine({ ++ getPolygonPoints: getTwoWayArrowPoints, ++ getConnectorPoints: (rectangle) => { ++ return [ ++ [rectangle.x, rectangle.y + rectangle.height / 2], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2] ++ ]; ++ } ++}); ++ ++const getStarPoints = (rectangle) => { ++ return [ ++ [rectangle.x + rectangle.width / 2, rectangle.y + (rectangle.height * 75) / 91], ++ [rectangle.x + (rectangle.width * 18.61) / 96, rectangle.y + rectangle.height], ++ [rectangle.x + (rectangle.width * 24.2235871) / 96, rectangle.y + (rectangle.height * 57.7254249) / 91], ++ [rectangle.x, rectangle.y + (rectangle.height * 34.5491503) / 91], ++ [rectangle.x + (rectangle.width * 33.3053687) / 96, rectangle.y + (rectangle.height * 29.7745751) / 91], ++ [rectangle.x + rectangle.width / 2, rectangle.y], ++ [rectangle.x + (rectangle.width * 62.6946313) / 96, rectangle.y + (rectangle.height * 29.7745751) / 91], ++ [rectangle.x + rectangle.width, rectangle.y + (rectangle.height * 34.5491503) / 91], ++ [rectangle.x + (rectangle.width * 71.7764129) / 96, rectangle.y + (rectangle.height * 57.7254249) / 91], ++ [rectangle.x + (rectangle.width * 77.3892626) / 96, rectangle.y + rectangle.height] ++ ]; ++}; ++const StarEngine = createPolygonEngine({ ++ getPolygonPoints: getStarPoints, ++ getConnectorPoints: (rectangle) => { ++ const points = getStarPoints(rectangle); ++ return [points[1], points[3], points[5], points[7], points[9]]; ++ }, ++ getTextRectangle: (board, element) => { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const customTextRectangle = getCustomTextRectangle(board, element, 1 / 2); ++ customTextRectangle.y = ++ elementRectangle.y + ++ elementRectangle.height / 5 + ++ (elementRectangle.height - elementRectangle.height / 5 - customTextRectangle.height) / 2; ++ return customTextRectangle; ++ } ++}); ++ ++const TerminalEngine = { ++ draw(board, rectangle, options) { ++ return drawRoundRectangle(PlaitBoard.getRoughSVG(board), rectangle.x, rectangle.y, rectangle.x + rectangle.width, rectangle.y + rectangle.height, { ...options, fillStyle: 'solid' }, false, getStartEndRadius(rectangle)); ++ }, ++ isInsidePoint(rectangle, point) { ++ return isPointInRoundRectangle(point, rectangle, getStartEndRadius(rectangle)); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ return getNearestPointBetweenPointAndRoundRectangle(point, rectangle, getStartEndRadius(rectangle)); ++ }, ++ getTangentVectorByConnectionPoint(rectangle, pointOfRectangle) { ++ const connectionPoint = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ const radius = getStartEndRadius(rectangle); ++ const center = getBoundCenterOfRoundRectangle(rectangle, radius, connectionPoint); ++ if (center) { ++ const point = [connectionPoint[0] - center[0], -(connectionPoint[1] - center[1])]; ++ const a = radius; ++ const b = radius; ++ const slope = getEllipseTangentSlope(point[0], point[1], a, b); ++ return getVectorFromPointAndSlope(point[0], point[1], slope); ++ } ++ return null; ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ } ++}; ++const getStartEndRadius = (rectangle) => { ++ return Math.min(rectangle.width / 2, rectangle.height / 2); ++}; ++function getNearestPointBetweenPointAndRoundRectangle(point, rectangle, radius) { ++ let result = null; ++ let boundCenter = getBoundCenterOfRoundRectangle(rectangle, radius, point); ++ if (boundCenter) { ++ result = getNearestPointBetweenPointAndEllipse(point, boundCenter, radius, radius); ++ } ++ else { ++ const cornerPoints = RectangleClient.getCornerPoints(rectangle); ++ result = getNearestPointBetweenPointAndSegments(point, cornerPoints); ++ } ++ return result; ++} ++function getBoundCenterOfRoundRectangle(rectangle, radius, point) { ++ const { x, y, width, height } = rectangle; ++ let center = null; ++ const inLeftTop = point[0] >= x && point[0] <= x + radius && point[1] >= y && point[1] <= y + radius; ++ if (inLeftTop) { ++ center = [x + radius, y + radius]; ++ } ++ const inLeftBottom = point[0] >= x && point[0] <= x + radius && point[1] >= y + height - radius && point[1] <= y + height; ++ if (inLeftBottom) { ++ center = [x + radius, y + height - radius]; ++ } ++ const inRightTop = point[0] >= x + width - radius && point[0] <= x + width && point[1] >= y && point[1] <= y + radius; ++ if (inRightTop) { ++ center = [x + width - radius, y + radius]; ++ } ++ const inRightBottom = point[0] >= x + width - radius && point[0] <= x + width && point[1] >= y + height - radius && point[1] <= y + height; ++ if (inRightBottom) { ++ center = [x + width - radius, y + height - radius]; ++ } ++ return center; ++} ++ ++const getManualInputPoints = (rectangle) => { ++ return [ ++ [rectangle.x, rectangle.y + rectangle.height / 4], ++ [rectangle.x + rectangle.width, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height], ++ [rectangle.x, rectangle.y + rectangle.height] ++ ]; ++}; ++const ManualInputEngine = createPolygonEngine({ ++ getPolygonPoints: getManualInputPoints, ++ getConnectorPoints: (rectangle) => { ++ const cornerPoints = getManualInputPoints(rectangle); ++ return getCenterPointsOnPolygon$1(cornerPoints); ++ }, ++ getTextRectangle: (board, element) => { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const strokeWidth = getStrokeWidthByElement(element); ++ const width = elementRectangle.width - ShapeDefaultSpace.rectangleAndText * 2 - strokeWidth * 2; ++ const text = element.text; ++ const textSize = getTextSize(board, text, width); ++ return { ++ height: textSize.height, ++ width: width > 0 ? width : 0, ++ x: elementRectangle.x + ShapeDefaultSpace.rectangleAndText + strokeWidth, ++ y: elementRectangle.y + elementRectangle.height / 4 + ((elementRectangle.height * 3) / 4 - textSize.height) / 2 ++ }; ++ } ++}); ++ ++const getPreparationPoints = (rectangle) => { ++ return [ ++ [rectangle.x, rectangle.y + rectangle.height / 2], ++ [rectangle.x + rectangle.width / 6, rectangle.y], ++ [rectangle.x + (rectangle.width * 5) / 6, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2], ++ [rectangle.x + (rectangle.width * 5) / 6, rectangle.y + rectangle.height], ++ [rectangle.x + rectangle.width / 6, rectangle.y + rectangle.height] ++ ]; ++}; ++const PreparationEngine = createPolygonEngine({ ++ getPolygonPoints: getPreparationPoints, ++ getConnectorPoints: (rectangle) => { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle(board, element) { ++ const rectangle = getCustomTextRectangle(board, element, 2 / 3); ++ return rectangle; ++ } ++}); ++ ++const getManualLoopPoints = (rectangle) => { ++ return [ ++ [rectangle.x, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y], ++ [rectangle.x + (rectangle.width * 7) / 8, rectangle.y + rectangle.height], ++ [rectangle.x + rectangle.width / 8, rectangle.y + rectangle.height] ++ ]; ++}; ++const ManualLoopEngine = createPolygonEngine({ ++ getPolygonPoints: getManualLoopPoints, ++ getConnectorPoints: (rectangle) => { ++ const cornerPoints = getManualLoopPoints(rectangle); ++ return getCenterPointsOnPolygon$1(cornerPoints); ++ }, ++ getTextRectangle: (board, element) => { ++ const rectangle = getCustomTextRectangle(board, element, 3 / 4); ++ return rectangle; ++ } ++}); ++ ++const getMergePoints = (rectangle) => { ++ return [ ++ [rectangle.x, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y], ++ [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height] ++ ]; ++}; ++const MergeEngine = createPolygonEngine({ ++ getPolygonPoints: getMergePoints, ++ getConnectorPoints: (rectangle) => { ++ const cornerPoints = getMergePoints(rectangle); ++ const lineCenterPoints = getCenterPointsOnPolygon$1(cornerPoints); ++ return [...lineCenterPoints, ...cornerPoints]; ++ }, ++ getTextRectangle: (board, element) => { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const strokeWidth = getStrokeWidthByElement(element); ++ const originWidth = elementRectangle.width - ShapeDefaultSpace.rectangleAndText * 2 - strokeWidth * 2; ++ const width = (originWidth * 2) / 3; ++ const text = element.text; ++ const textSize = getTextSize(board, text, width); ++ return { ++ height: textSize.height, ++ width: width > 0 ? width : 0, ++ x: elementRectangle.x + ShapeDefaultSpace.rectangleAndText + strokeWidth + originWidth / 6, ++ y: elementRectangle.y + ((elementRectangle.height * 2) / 3 - textSize.height) / 2 ++ }; ++ } ++}); ++ ++const DelayEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const shape = rs.path(`M${rectangle.x} ${rectangle.y} L${rectangle.x + (rectangle.width * 3) / 4} ${rectangle.y} A ${rectangle.width / ++ 4} ${rectangle.height / 2}, 0, 0, 1,${rectangle.x + (rectangle.width * 3) / 4} ${rectangle.y + rectangle.height} L${rectangle.x} ${rectangle.y + rectangle.height} Z`, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ //split shape to rectangle and a half ellipse ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ const isInRectangle = RectangleClient.isHit({ ++ ...rectangle, ++ width: (rectangle.width * 3) / 4 ++ }, rangeRectangle); ++ const isInEllipse = isPointInEllipse(point, [rectangle.x + (rectangle.width * 3) / 4, rectangle.y + rectangle.height / 2], rectangle.width / 4, rectangle.height / 2); ++ return isInRectangle || isInEllipse; ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ const nearestPoint = getNearestPointBetweenPointAndSegments(point, RectangleEngine.getCornerPoints(rectangle)); ++ if (nearestPoint[0] > rectangle.x + (rectangle.width * 3) / 4) { ++ return getNearestPointBetweenPointAndEllipse(point, [rectangle.x + (rectangle.width * 3) / 4, rectangle.y + rectangle.height / 2], rectangle.width / 4, rectangle.height / 2); ++ } ++ return nearestPoint; ++ }, ++ getTangentVectorByConnectionPoint(rectangle, pointOfRectangle) { ++ const connectionPoint = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ const centerPoint = [rectangle.x + (rectangle.width * 3) / 4, rectangle.y + rectangle.height / 2]; ++ const point = [connectionPoint[0] - centerPoint[0], -(connectionPoint[1] - centerPoint[1])]; ++ const a = rectangle.width / 4; ++ const b = rectangle.height / 2; ++ const slope = getEllipseTangentSlope(point[0], point[1], a, b); ++ return getVectorFromPointAndSlope(point[0], point[1], slope); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ } ++}; ++ ++const StoredDataEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const shape = rs.path(`M${rectangle.x + rectangle.width / 10} ${rectangle.y} L${rectangle.x + rectangle.width} ${rectangle.y} A ${rectangle.width / 10} ${rectangle.height / 2}, 0, 0, 0,${rectangle.x + rectangle.width} ${rectangle.y + rectangle.height} L${rectangle.x + rectangle.width / 10} ${rectangle.y + rectangle.height}A ${rectangle.width / 10} ${rectangle.height / 2}, 0, 0, 1,${rectangle.x + rectangle.width / 10} ${rectangle.y}`, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ //split shape to rectangle and a half ellipse ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ const isInRectangle = RectangleClient.isHit({ ++ ...rectangle, ++ x: rectangle.x + rectangle.width / 10, ++ width: (rectangle.width * 9) / 10 ++ }, rangeRectangle); ++ const isInFrontEllipse = isPointInEllipse(point, [rectangle.x + rectangle.width / 10, rectangle.y + rectangle.height / 2], rectangle.width / 10, rectangle.height / 2); ++ const notInBackEllipse = !isPointInEllipse(point, [rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2], rectangle.width / 10, rectangle.height / 2); ++ return (isInRectangle && notInBackEllipse) || isInFrontEllipse; ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ const nearestPoint = getNearestPointBetweenPointAndSegments(point, RectangleEngine.getCornerPoints(rectangle)); ++ if (nearestPoint[0] < rectangle.x + rectangle.width / 10) { ++ const centerPoint = [rectangle.x + rectangle.width / 10, rectangle.y + rectangle.height / 2]; ++ const nearestPoint = getNearestPointBetweenPointAndEllipse(point, centerPoint, rectangle.width / 10, rectangle.height / 2); ++ if (nearestPoint[0] > centerPoint[0]) { ++ nearestPoint[0] = centerPoint[0] * 2 - nearestPoint[0]; ++ } ++ return nearestPoint; ++ } ++ if (nearestPoint[0] > rectangle.x + (rectangle.width * 9) / 10) { ++ const centerPoint = [rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2]; ++ const nearestPoint = getNearestPointBetweenPointAndEllipse(point, centerPoint, rectangle.width / 10, rectangle.height / 2); ++ if (nearestPoint[0] > centerPoint[0]) { ++ nearestPoint[0] = centerPoint[0] * 2 - nearestPoint[0]; ++ } ++ return nearestPoint; ++ } ++ return nearestPoint; ++ }, ++ getTangentVectorByConnectionPoint(rectangle, pointOfRectangle) { ++ const connectionPoint = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ let centerPoint = [rectangle.x + rectangle.width / 10, rectangle.y + rectangle.height / 2]; ++ let a = rectangle.width / 10; ++ let b = rectangle.height / 2; ++ const isBackEllipse = connectionPoint[0] > rectangle.x + (rectangle.width * 9) / 10 && connectionPoint[1] > rectangle.y; ++ if (isBackEllipse) { ++ centerPoint = [rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2]; ++ } ++ const point = [connectionPoint[0] - centerPoint[0], -(connectionPoint[1] - centerPoint[1])]; ++ const slope = getEllipseTangentSlope(point[0], point[1], a, b); ++ const vector = getVectorFromPointAndSlope(point[0], point[1], slope); ++ return isBackEllipse ? vector.map((num) => -num) : vector; ++ }, ++ getConnectorPoints(rectangle) { ++ return [ ++ [rectangle.x + rectangle.width / 2, rectangle.y], ++ [rectangle.x + (rectangle.width * 9) / 10, rectangle.y + rectangle.height / 2], ++ [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height], ++ [rectangle.x, rectangle.y + rectangle.height / 2] ++ ]; ++ }, ++ getTextRectangle(board, element) { ++ const widthRatio = 3 / 4; ++ return getCustomTextRectangle(board, element, widthRatio); ++ } ++}; ++ ++const PredefinedProcessEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const shape = rs.path(`M${rectangle.x} ${rectangle.y} H${rectangle.x + rectangle.width} V${rectangle.y + rectangle.height} H${rectangle.x} Z M${rectangle.x + rectangle.width * 0.06} ${rectangle.y} L${rectangle.x + rectangle.width * 0.06} ${rectangle.y + ++ rectangle.height} M${rectangle.x + rectangle.width - rectangle.width * 0.06} ${rectangle.y} L${rectangle.x + ++ rectangle.width - ++ rectangle.width * 0.06} ${rectangle.y + rectangle.height}`, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ return getNearestPointBetweenPointAndSegments(point, RectangleEngine.getCornerPoints(rectangle)); ++ }, ++ getEdgeByConnectionPoint(rectangle, pointOfRectangle) { ++ const corners = RectangleEngine.getCornerPoints(rectangle); ++ const point = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ return getPolygonEdgeByConnectionPoint(corners, point); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle: (board, element) => { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const strokeWidth = getStrokeWidthByElement(element); ++ const width = elementRectangle.width - ShapeDefaultSpace.rectangleAndText * 2 - strokeWidth * 2 - elementRectangle.width * 0.06 * 2; ++ const text = element.text; ++ const textSize = getTextSize(board, text, width); ++ return { ++ height: textSize.height, ++ width: width > 0 ? width : 0, ++ x: elementRectangle.x + ShapeDefaultSpace.rectangleAndText + strokeWidth + elementRectangle.width * 0.06, ++ y: elementRectangle.y + (elementRectangle.height - textSize.height) / 2 ++ }; ++ } ++}; ++ ++const getOffPagePoints = (rectangle) => { ++ return [ ++ [rectangle.x, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2], ++ [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height], ++ [rectangle.x, rectangle.y + rectangle.height / 2] ++ ]; ++}; ++const OffPageEngine = createPolygonEngine({ ++ getPolygonPoints: getOffPagePoints, ++ getConnectorPoints: (rectangle) => { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle: (board, element) => { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const strokeWidth = getStrokeWidthByElement(element); ++ const width = elementRectangle.width - ShapeDefaultSpace.rectangleAndText * 2 - strokeWidth * 2; ++ const text = element.text; ++ const textSize = getTextSize(board, text, width); ++ return { ++ width: width > 0 ? width : 0, ++ height: textSize.height, ++ x: elementRectangle.x + ShapeDefaultSpace.rectangleAndText + strokeWidth, ++ y: elementRectangle.y + (elementRectangle.height - elementRectangle.height / 2 - textSize.height) / 2 ++ }; ++ } ++}); ++ ++const OrEngine = createEllipseEngine({ ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const rx = rectangle.width / 2; ++ const ry = rectangle.height / 2; ++ const startPoint = [rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2]; ++ const shape = rs.path(`M${startPoint[0]} ${startPoint[1]} ++ A${rx},${ry} 0 1,1 ${startPoint[0]} ${startPoint[1] - 0.01} ++ M${rectangle.x} ${rectangle.y + rectangle.height / 2} ++ L${rectangle.x + rectangle.width} ${rectangle.y + rectangle.height / 2} ++ M${rectangle.x + rectangle.width / 2} ${rectangle.y} ++ L${rectangle.x + rectangle.width / 2} ${rectangle.y + rectangle.height} ++ `, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ } ++}); ++ ++const SummingJunctionEngine = createEllipseEngine({ ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const rx = rectangle.width / 2; ++ const ry = rectangle.height / 2; ++ const startPoint = [rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2]; ++ const centerPoint = [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height / 2]; ++ const line1Points = getCrossingPointsBetweenEllipseAndSegment([rectangle.x, rectangle.y], [rectangle.x + rectangle.width, rectangle.y + rectangle.height], centerPoint[0], centerPoint[1], rx, ry); ++ const line2Points = getCrossingPointsBetweenEllipseAndSegment([rectangle.x + rectangle.width, rectangle.y], [rectangle.x, rectangle.y + rectangle.height], centerPoint[0], centerPoint[1], rx, ry); ++ const shape = rs.path(`M${startPoint[0]} ${startPoint[1]} ++ A${rx},${ry} 0 1,1 ${startPoint[0]} ${startPoint[1] - 0.01} ++ M${line1Points[0][0]} ${line1Points[0][1]} ++ L${line1Points[1][0]} ${line1Points[1][1]} ++ M${line2Points[0][0]} ${line2Points[0][1]} ++ L${line2Points[1][0]} ${line2Points[1][1]} ++ `, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ } ++}); ++ ++const DocumentEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const shape = rs.path(`M${rectangle.x} ${rectangle.y + rectangle.height - rectangle.height / 9} V${rectangle.y} H${rectangle.x + ++ rectangle.width} V${rectangle.y + rectangle.height - rectangle.height / 9} ++ Q${rectangle.x + rectangle.width - rectangle.width / 4} ${rectangle.y + ++ rectangle.height - ++ (rectangle.height / 9) * 3}, ${rectangle.x + rectangle.width / 2} ${rectangle.y + ++ rectangle.height - ++ rectangle.height / 9} T${rectangle.x} ${rectangle.y + rectangle.height - rectangle.height / 9} ++ `, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ let nearestPoint = getNearestPointBetweenPointAndSegments(point, RectangleEngine.getCornerPoints(rectangle)); ++ let curvePoints = catmullRomFitting([ ++ [rectangle.x, rectangle.y + rectangle.height - rectangle.height / 9], ++ [rectangle.x + rectangle.width / 4, rectangle.y + rectangle.height], ++ [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height - rectangle.height / 9], ++ [rectangle.x + (rectangle.width / 4) * 3, rectangle.y + rectangle.height - (rectangle.height / 9) * 2], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height - rectangle.height / 9] ++ ]); ++ curvePoints = pointsOnBezierCurves(curvePoints); ++ if (nearestPoint[1] > rectangle.y + rectangle.height - rectangle.height / 9) { ++ if (nearestPoint[0] === rectangle.x + rectangle.width / 2) { ++ nearestPoint[1] = rectangle.y + rectangle.height - rectangle.height / 9; ++ return nearestPoint; ++ } ++ nearestPoint = getNearestPointBetweenPointAndSegments(point, curvePoints, false); ++ } ++ return nearestPoint; ++ }, ++ getConnectorPoints(rectangle) { ++ return [ ++ [rectangle.x + rectangle.width / 2, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2], ++ [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height - rectangle.height / 9], ++ [rectangle.x, rectangle.y + rectangle.height / 2] ++ ]; ++ }, ++ getTangentVectorByConnectionPoint(rectangle, pointOfRectangle) { ++ const connectionPoint = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ if (connectionPoint[0] > rectangle.x && connectionPoint[0] < rectangle.x + rectangle.width / 4) { ++ return getUnitVectorByPointAndPoint([rectangle.x + rectangle.width / 4, rectangle.y + rectangle.height], connectionPoint); ++ } ++ if (connectionPoint[0] > rectangle.x + rectangle.width / 4 && connectionPoint[0] < rectangle.x + (rectangle.width / 4) * 3) { ++ return getUnitVectorByPointAndPoint([rectangle.x + (rectangle.width / 4) * 3, rectangle.y + rectangle.height - rectangle.height / 9], [rectangle.x + rectangle.width / 4, rectangle.y + rectangle.height]); ++ } ++ if (connectionPoint[0] > rectangle.x + (rectangle.width / 4) * 3) { ++ return getUnitVectorByPointAndPoint([rectangle.x + rectangle.width, rectangle.y + rectangle.height - rectangle.height / 9], connectionPoint); ++ } ++ return getUnitVectorByPointAndPoint(connectionPoint, [rectangle.x + rectangle.width / 4, rectangle.y + rectangle.height]); ++ }, ++ getTextRectangle: (board, element) => { ++ return getCustomTextRectangle(board, element, 0.88); ++ } ++}; ++ ++const getMultiDocumentPoints = (rectangle) => { ++ const linePoints = [ ++ [rectangle.x, rectangle.y + 10], ++ [rectangle.x + 5, rectangle.y + 10], ++ [rectangle.x + 5, rectangle.y + 5], ++ [rectangle.x + 10, rectangle.y + 5], ++ [rectangle.x + 10, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height - rectangle.height / 9 - 10 - 3], ++ [rectangle.x + rectangle.width - 5, rectangle.y + rectangle.height - rectangle.height / 9 - 10 - 3 - 4], ++ [rectangle.x + rectangle.width - 5, rectangle.y + rectangle.height - rectangle.height / 9 - 5 - 3], ++ [rectangle.x + rectangle.width - 10, rectangle.y + rectangle.height - rectangle.height / 9 - 5 - 3 - 4], ++ [rectangle.x + rectangle.width - 10, rectangle.y + rectangle.height - rectangle.height / 9] ++ ]; ++ let curvePoints = catmullRomFitting([ ++ [rectangle.x + rectangle.width - 10, rectangle.y + rectangle.height - rectangle.height / 9], ++ [rectangle.x + rectangle.width - 10 - (rectangle.width - 10) / 4, rectangle.y + rectangle.height - (rectangle.height / 9) * 2], ++ [rectangle.x + (rectangle.width - 10) / 2, rectangle.y + rectangle.height - rectangle.height / 9], ++ [rectangle.x + (rectangle.width - 10) / 4, rectangle.y + rectangle.height], ++ [rectangle.x, rectangle.y + rectangle.height - rectangle.height / 9] ++ ]); ++ curvePoints = pointsOnBezierCurves(curvePoints); ++ return [...linePoints, ...curvePoints]; ++}; ++const MultiDocumentEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const shape = rs.path(`M${rectangle.x} ${rectangle.y + rectangle.height - rectangle.height / 9} V${rectangle.y + 10} H${rectangle.x + 5} V${rectangle.y + 5} H${rectangle.x + 10} V${rectangle.y} H${rectangle.x + rectangle.width} V${rectangle.y + rectangle.height - rectangle.height / 9 - 10 - 3} L${rectangle.x + rectangle.width - 5} ${rectangle.y + rectangle.height - rectangle.height / 9 - 10 - 3 - 4} V${rectangle.y + rectangle.height - rectangle.height / 9 - 5 - 3} ++ L${rectangle.x + rectangle.width - 10} ${rectangle.y + rectangle.height - rectangle.height / 9 - 5 - 3 - 4} V${rectangle.y + rectangle.height - rectangle.height / 9} ++ ++ Q${rectangle.x + rectangle.width - 10 - (rectangle.width - 10) / 4} ${rectangle.y + rectangle.height - (rectangle.height / 9) * 3}, ${rectangle.x + (rectangle.width - 10) / 2} ${rectangle.y + rectangle.height - rectangle.height / 9} T${rectangle.x} ${rectangle.y + rectangle.height - rectangle.height / 9} ++ ++ M${rectangle.x + 5} ${rectangle.y + 10} H${rectangle.x + rectangle.width - 10} V${rectangle.y + rectangle.height - rectangle.height / 9} ++ ++ M${rectangle.x + 10} ${rectangle.y + 5} H${rectangle.x + rectangle.width - 5} V${rectangle.y + rectangle.height - rectangle.height / 9 - 10 - 3} ++ `, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ return getNearestPointBetweenPointAndSegments(point, getMultiDocumentPoints(rectangle), false); ++ }, ++ getConnectorPoints(rectangle) { ++ let curvePoints = catmullRomFitting([ ++ [rectangle.x, rectangle.y + rectangle.height - rectangle.height / 9], ++ [rectangle.x + (rectangle.width - 10) / 4, rectangle.y + rectangle.height], ++ [rectangle.x + (rectangle.width - 10) / 2, rectangle.y + rectangle.height - rectangle.height / 9], ++ [rectangle.x + ((rectangle.width - 10) / 4) * 3, rectangle.y + rectangle.height - (rectangle.height / 9) * 2], ++ [rectangle.x + rectangle.width - 10, rectangle.y + rectangle.height - rectangle.height / 9] ++ ]); ++ curvePoints = pointsOnBezierCurves(curvePoints); ++ const crossingPoint = getNearestPointBetweenPointAndSegments([rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height], curvePoints); ++ return [ ++ [rectangle.x + rectangle.width / 2, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2], ++ [crossingPoint[0], crossingPoint[1]], ++ [rectangle.x, rectangle.y + rectangle.height / 2] ++ ]; ++ }, ++ getNearestCrossingPoint(rectangle, point) { ++ const crossingPoints = getCrossingPointBetweenPointAndPolygon(getMultiDocumentPoints(rectangle), point); ++ let nearestPoint = crossingPoints[0]; ++ let nearestDistance = distanceBetweenPointAndPoint(point[0], point[1], nearestPoint[0], nearestPoint[1]); ++ crossingPoints ++ .filter((v, index) => index > 0) ++ .forEach((crossingPoint) => { ++ let distance = distanceBetweenPointAndPoint(point[0], point[1], crossingPoint[0], crossingPoint[1]); ++ if (distance < nearestDistance) { ++ nearestDistance = distance; ++ nearestPoint = crossingPoint; ++ } ++ }); ++ return nearestPoint; ++ }, ++ getTangentVectorByConnectionPoint(rectangle, pointOfRectangle) { ++ const connectionPoint = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ if (connectionPoint[0] > rectangle.x && connectionPoint[0] < rectangle.x + (rectangle.width - 10) / 4) { ++ return getUnitVectorByPointAndPoint([rectangle.x + (rectangle.width - 10) / 4, rectangle.y + rectangle.height], connectionPoint); ++ } ++ if (connectionPoint[0] > rectangle.x + (rectangle.width - 10) / 4 && ++ connectionPoint[0] < rectangle.x + ((rectangle.width - 10) / 4) * 3) { ++ return getUnitVectorByPointAndPoint([rectangle.x + ((rectangle.width - 10) / 4) * 3, rectangle.y + rectangle.height - rectangle.height / 9], [rectangle.x + (rectangle.width - 10) / 4, rectangle.y + rectangle.height]); ++ } ++ if (connectionPoint[0] > rectangle.x + ((rectangle.width - 10) / 4) * 3 && ++ connectionPoint[0] < rectangle.x + rectangle.width - 10) { ++ return getUnitVectorByPointAndPoint([rectangle.x + (rectangle.width - 10), rectangle.y + rectangle.height - rectangle.height / 9], connectionPoint); ++ } ++ const direction = getDirectionByPointOfRectangle(pointOfRectangle) || Direction.bottom; ++ const factor = getDirectionFactor(direction); ++ return [factor.x, factor.y]; ++ }, ++ getTextRectangle: (board, element) => { ++ return getCustomTextRectangle(board, element, 0.88); ++ } ++}; ++ ++const DatabaseEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const shape = rs.path(`M${rectangle.x} ${rectangle.y + rectangle.height * 0.15} ++ A${rectangle.width / 2} ${rectangle.height * 0.15}, 0, 0, 0,${rectangle.x + rectangle.width} ${rectangle.y + rectangle.height * 0.15} ++ A${rectangle.width / 2} ${rectangle.height * 0.15}, 0, 0, 0,${rectangle.x} ${rectangle.y + rectangle.height * 0.15} ++ V${rectangle.y + rectangle.height - rectangle.height * 0.15} ++ A${rectangle.width / 2} ${rectangle.height * 0.15}, 0, 0, 0, ${rectangle.x + rectangle.width} ${rectangle.y + rectangle.height - rectangle.height * 0.15} ++ V${rectangle.y + rectangle.height * 0.15}`, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ const isInRectangle = RectangleClient.isHit({ ++ ...rectangle, ++ y: rectangle.y + rectangle.height * 0.15, ++ height: rectangle.height - rectangle.height * 0.3 ++ }, rangeRectangle); ++ const isInTopEllipse = isPointInEllipse(point, [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height * 0.15], rectangle.width / 2, rectangle.height * 0.15); ++ const isInBottomEllipse = isPointInEllipse(point, [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height - rectangle.height * 0.15], rectangle.width / 2, rectangle.height * 0.15); ++ return isInRectangle || isInTopEllipse || isInBottomEllipse; ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ const nearestPoint = getNearestPointBetweenPointAndSegments(point, RectangleEngine.getCornerPoints(rectangle)); ++ if (nearestPoint[1] < rectangle.y + rectangle.height * 0.15) { ++ const centerPoint = [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height * 0.15]; ++ const nearestPoint = getNearestPointBetweenPointAndEllipse(point, centerPoint, rectangle.width / 2, rectangle.height * 0.15); ++ if (nearestPoint[1] > centerPoint[1]) { ++ nearestPoint[1] = centerPoint[1] * 2 - nearestPoint[1]; ++ } ++ return nearestPoint; ++ } ++ if (nearestPoint[1] > rectangle.y + rectangle.height - rectangle.height * 0.15) { ++ const centerPoint = [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height - rectangle.height * 0.15]; ++ const nearestPoint = getNearestPointBetweenPointAndEllipse(point, centerPoint, rectangle.width / 2, rectangle.height * 0.15); ++ if (nearestPoint[1] < centerPoint[1]) { ++ nearestPoint[1] = centerPoint[0] * 2 - nearestPoint[1]; ++ } ++ return nearestPoint; ++ } ++ return nearestPoint; ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTangentVectorByConnectionPoint(rectangle, pointOfRectangle) { ++ const connectionPoint = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ let centerPoint = [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height - rectangle.height * 0.15]; ++ let a = rectangle.width / 2; ++ let b = rectangle.height * 0.15; ++ const isInTopEllipse = connectionPoint[1] < rectangle.y + rectangle.height * 0.15 && connectionPoint[0] > rectangle.x; ++ if (isInTopEllipse) { ++ centerPoint = [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height * 0.15]; ++ } ++ const point = [connectionPoint[0] - centerPoint[0], -(connectionPoint[1] - centerPoint[1])]; ++ const slope = getEllipseTangentSlope(point[0], point[1], a, b); ++ const vector = getVectorFromPointAndSlope(point[0], point[1], slope); ++ return vector; ++ }, ++ getTextRectangle: (board, element) => { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const textRectangle = getCustomTextRectangle(board, element, 1); ++ textRectangle.y += getStrokeWidthByElement(element); ++ const startY = elementRectangle.y + elementRectangle.height * 0.45; ++ const endY = elementRectangle.y + elementRectangle.height - elementRectangle.height * 0.3; ++ textRectangle.y = startY + (endY - startY - textRectangle.height) / 2; ++ return textRectangle; ++ } ++}; ++ ++const HardDiskEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const shape = rs.path(`M${rectangle.x + rectangle.width - rectangle.width * 0.15} ${rectangle.y} ++ A${rectangle.width * 0.15} ${rectangle.height / 2}, 0, 0, 0,${rectangle.x + ++ rectangle.width - ++ rectangle.width * 0.15} ${rectangle.y + rectangle.height} ++ A${rectangle.width * 0.15} ${rectangle.height / 2}, 0, 0, 0,${rectangle.x + rectangle.width - rectangle.width * 0.15} ${rectangle.y} ++ H${rectangle.x + rectangle.width * 0.15} ++ A${rectangle.width * 0.15} ${rectangle.height / 2}, 0, 0, 0, ${rectangle.x + rectangle.width * 0.15} ${rectangle.y + ++ rectangle.height} ++ H${rectangle.x + rectangle.width - rectangle.width * 0.15}`, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ const isInRectangle = RectangleClient.isHit({ ++ ...rectangle, ++ x: rectangle.x + rectangle.width * 0.15, ++ width: rectangle.width - rectangle.width * 0.3 ++ }, rangeRectangle); ++ const isInLeftEllipse = isPointInEllipse(point, [rectangle.x + rectangle.width * 0.15, rectangle.y + rectangle.height / 2], rectangle.width * 0.15, rectangle.height / 2); ++ const isInRightEllipse = isPointInEllipse(point, [rectangle.x + rectangle.width - rectangle.width * 0.15, rectangle.y + rectangle.height / 2], rectangle.width * 0.15, rectangle.height / 2); ++ return isInRectangle || isInLeftEllipse || isInRightEllipse; ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ const nearestPoint = getNearestPointBetweenPointAndSegments(point, RectangleEngine.getCornerPoints(rectangle)); ++ if (nearestPoint[0] < rectangle.x + rectangle.width * 0.15) { ++ const centerPoint = [rectangle.x + rectangle.width * 0.15, rectangle.y + rectangle.height / 2]; ++ const nearestPoint = getNearestPointBetweenPointAndEllipse(point, centerPoint, rectangle.width * 0.15, rectangle.height / 2); ++ if (nearestPoint[0] > centerPoint[0]) { ++ nearestPoint[0] = centerPoint[0] * 2 - nearestPoint[0]; ++ } ++ return nearestPoint; ++ } ++ if (nearestPoint[0] > rectangle.x + rectangle.width - rectangle.width * 0.15) { ++ const centerPoint = [rectangle.x + rectangle.width - rectangle.width * 0.15, rectangle.y + rectangle.height / 2]; ++ const nearestPoint = getNearestPointBetweenPointAndEllipse(point, centerPoint, rectangle.width * 0.15, rectangle.height / 2); ++ if (nearestPoint[0] < centerPoint[0]) { ++ nearestPoint[0] = centerPoint[0] * 2 - nearestPoint[0]; ++ } ++ return nearestPoint; ++ } ++ return nearestPoint; ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTangentVectorByConnectionPoint(rectangle, pointOfRectangle) { ++ const connectionPoint = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ let centerPoint = [rectangle.x + rectangle.width * 0.15, rectangle.y + rectangle.height / 2]; ++ let a = rectangle.width * 0.15; ++ let b = rectangle.height / 2; ++ const isInRightEllipse = connectionPoint[0] > rectangle.x + rectangle.width - rectangle.width * 0.15 && connectionPoint[1] > rectangle.y; ++ if (isInRightEllipse) { ++ centerPoint = [rectangle.x + rectangle.width - rectangle.width * 0.15, rectangle.y + rectangle.height / 2]; ++ } ++ const point = [connectionPoint[0] - centerPoint[0], -(connectionPoint[1] - centerPoint[1])]; ++ const slope = getEllipseTangentSlope(point[0], point[1], a, b); ++ const vector = getVectorFromPointAndSlope(point[0], point[1], slope); ++ return vector; ++ }, ++ getTextRectangle: (board, element) => { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const strokeWidth = getStrokeWidthByElement(element); ++ const width = elementRectangle.width - elementRectangle.width * 0.45 - ShapeDefaultSpace.rectangleAndText * 2 - strokeWidth * 2; ++ const text = element.text; ++ const textSize = getTextSize(board, text, width); ++ return { ++ height: textSize.height, ++ width: width > 0 ? width : 0, ++ x: elementRectangle.x + elementRectangle.width * 0.15 + ShapeDefaultSpace.rectangleAndText + strokeWidth, ++ y: elementRectangle.y + (elementRectangle.height - textSize.height) / 2 ++ }; ++ } ++}; ++ ++const InternalStorageEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const shape = rs.path(`M${rectangle.x} ${rectangle.y} h${rectangle.width} v${rectangle.height} h${-rectangle.width} v${-rectangle.height} ++ M${rectangle.x} ${rectangle.y + rectangle.height / 10} h${rectangle.width} ++ M${rectangle.x + rectangle.width / 10} ${rectangle.y} v${rectangle.height} ++ `, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ return getNearestPointBetweenPointAndSegments(point, RectangleEngine.getCornerPoints(rectangle)); ++ }, ++ getEdgeByConnectionPoint(rectangle, pointOfRectangle) { ++ const corners = RectangleEngine.getCornerPoints(rectangle); ++ const point = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ return getPolygonEdgeByConnectionPoint(corners, point); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle: (board, element) => { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const strokeWidth = getStrokeWidthByElement(element); ++ const width = elementRectangle.width - elementRectangle.width * 0.1 - ShapeDefaultSpace.rectangleAndText * 2 - strokeWidth * 2; ++ const text = element.text; ++ const textSize = getTextSize(board, text, width); ++ return { ++ height: textSize.height, ++ width: width > 0 ? width : 0, ++ x: elementRectangle.x + elementRectangle.width * 0.1 + ShapeDefaultSpace.rectangleAndText + strokeWidth, ++ y: elementRectangle.y + elementRectangle.height * 0.1 + (elementRectangle.height - elementRectangle.height * 0.1 - textSize.height) / 2 ++ }; ++ } ++}; ++ ++function generateNoteCurlyLeftPath(rectangle) { ++ const curlyWidth = rectangle.width * 0.09; ++ const rightX = rectangle.x + rectangle.width; ++ const centerY = rectangle.y + rectangle.height / 2; ++ return { ++ startPoint: [rightX, rectangle.y], ++ upperCurve: { ++ controlPoint1: [rightX - curlyWidth, rectangle.y], ++ controlPoint2: [rightX, centerY], ++ endPoint: [rightX - curlyWidth, centerY] ++ }, ++ lowerCurve: { ++ controlPoint1: [rightX, centerY], ++ controlPoint2: [rightX - curlyWidth, rectangle.y + rectangle.height], ++ endPoint: [rightX, rectangle.y + rectangle.height] ++ } ++ }; ++} ++const NoteCurlyLeftEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const { startPoint, upperCurve, lowerCurve } = generateNoteCurlyLeftPath(rectangle); ++ const pathData = [ ++ `M${startPoint[0]} ${startPoint[1]}`, ++ `C${upperCurve.controlPoint1[0]} ${upperCurve.controlPoint1[1]}, ++ ${upperCurve.controlPoint2[0]} ${upperCurve.controlPoint2[1]}, ++ ${upperCurve.endPoint[0]} ${upperCurve.endPoint[1]}`, ++ `C${lowerCurve.controlPoint1[0]} ${lowerCurve.controlPoint1[1]}, ++ ${lowerCurve.controlPoint2[0]} ${lowerCurve.controlPoint2[1]}, ++ ${lowerCurve.endPoint[0]} ${lowerCurve.endPoint[1]}` ++ ].join(' '); ++ const shape = rs.path(pathData, { ...options, fillStyle: 'solid', fill: 'transparent' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ getNearestPoint(rectangle, point) { ++ const { startPoint, upperCurve, lowerCurve } = generateNoteCurlyLeftPath(rectangle); ++ const upperBezierPoints = pointsOnBezierCurves([startPoint, upperCurve.controlPoint1, upperCurve.controlPoint2, upperCurve.endPoint], 0.001); ++ const lowerBezierPoints = pointsOnBezierCurves([upperCurve.endPoint, lowerCurve.controlPoint1, lowerCurve.controlPoint2, lowerCurve.endPoint], 0.001); ++ const allPoints = [...upperBezierPoints, ...lowerBezierPoints]; ++ let minDistance = Infinity; ++ let nearestPoint = point; ++ for (const curvePoint of allPoints) { ++ const distance = distanceBetweenPointAndPoint(point[0], point[1], curvePoint[0], curvePoint[1]); ++ if (distance < minDistance) { ++ minDistance = distance; ++ nearestPoint = [...curvePoint]; ++ } ++ } ++ return nearestPoint; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getEdgeByConnectionPoint(rectangle, pointOfRectangle) { ++ const corners = RectangleEngine.getCornerPoints(rectangle); ++ const point = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ return getPolygonEdgeByConnectionPoint(corners, point); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle: (board, element) => { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const textRectangle = getCustomTextRectangle(board, element, 0.9); ++ textRectangle.x = elementRectangle.x + getStrokeWidthByElement(element) + ShapeDefaultSpace.rectangleAndText; ++ return textRectangle; ++ } ++}; ++ ++function generateNoteCurlyRightPath(rectangle) { ++ const curlyWidth = rectangle.width * 0.09; ++ const centerY = rectangle.y + rectangle.height / 2; ++ return { ++ startPoint: [rectangle.x, rectangle.y], ++ upperCurve: { ++ controlPoint1: [rectangle.x + curlyWidth, rectangle.y], ++ controlPoint2: [rectangle.x, centerY], ++ endPoint: [rectangle.x + curlyWidth, centerY] ++ }, ++ lowerCurve: { ++ controlPoint1: [rectangle.x, centerY], ++ controlPoint2: [rectangle.x + curlyWidth, rectangle.y + rectangle.height], ++ endPoint: [rectangle.x, rectangle.y + rectangle.height] ++ } ++ }; ++} ++const NoteCurlyRightEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const { startPoint, upperCurve, lowerCurve } = generateNoteCurlyRightPath(rectangle); ++ const pathData = [ ++ `M${startPoint[0]} ${startPoint[1]}`, ++ `C${upperCurve.controlPoint1[0]} ${upperCurve.controlPoint1[1]}, ++ ${upperCurve.controlPoint2[0]} ${upperCurve.controlPoint2[1]}, ++ ${upperCurve.endPoint[0]} ${upperCurve.endPoint[1]}`, ++ `C${lowerCurve.controlPoint1[0]} ${lowerCurve.controlPoint1[1]}, ++ ${lowerCurve.controlPoint2[0]} ${lowerCurve.controlPoint2[1]}, ++ ${lowerCurve.endPoint[0]} ${lowerCurve.endPoint[1]}` ++ ].join(' '); ++ const shape = rs.path(pathData, { ...options, fillStyle: 'solid', fill: 'transparent' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ const { startPoint, upperCurve, lowerCurve } = generateNoteCurlyRightPath(rectangle); ++ // 生成上部贝塞尔曲线的点 ++ const upperBezierPoints = pointsOnBezierCurves([startPoint, upperCurve.controlPoint1, upperCurve.controlPoint2, upperCurve.endPoint], 0.001); ++ // 生成下部贝塞尔曲线的点 ++ const lowerBezierPoints = pointsOnBezierCurves([upperCurve.endPoint, lowerCurve.controlPoint1, lowerCurve.controlPoint2, lowerCurve.endPoint], 0.001); ++ // 合并所有点 ++ const allPoints = [...upperBezierPoints, ...lowerBezierPoints]; ++ // 找到最近的点 ++ let minDistance = Infinity; ++ let nearestPoint = [...point]; ++ for (const curvePoint of allPoints) { ++ const distance = distanceBetweenPointAndPoint(point[0], point[1], curvePoint[0], curvePoint[1]); ++ if (distance < minDistance) { ++ minDistance = distance; ++ nearestPoint = [...curvePoint]; ++ } ++ } ++ return nearestPoint; ++ }, ++ getEdgeByConnectionPoint(rectangle, pointOfRectangle) { ++ const corners = RectangleEngine.getCornerPoints(rectangle); ++ const point = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ return getPolygonEdgeByConnectionPoint(corners, point); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle: (board, element) => { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const textRectangle = getCustomTextRectangle(board, element, 0.9); ++ textRectangle.x = ++ elementRectangle.x + getStrokeWidthByElement(element) + ShapeDefaultSpace.rectangleAndText + elementRectangle.width * 0.1; ++ return textRectangle; ++ } ++}; ++ ++const NoteSquareEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const shape = rs.path(`M${rectangle.x + rectangle.width * 0.075} ${rectangle.y + rectangle.height} H${rectangle.x} V${rectangle.y} H${rectangle.x + ++ rectangle.width * 0.075} ++ `, { ...options, fillStyle: 'solid', fill: 'transparent' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ return getNearestPointBetweenPointAndSegments(point, RectangleEngine.getCornerPoints(rectangle)); ++ }, ++ getEdgeByConnectionPoint(rectangle, pointOfRectangle) { ++ const corners = RectangleEngine.getCornerPoints(rectangle); ++ const point = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ return getPolygonEdgeByConnectionPoint(corners, point); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle: (board, element) => { ++ return getCustomTextRectangle(board, element, 0.88); ++ } ++}; ++ ++const getDisplayPoints = (rectangle) => { ++ return [ ++ [rectangle.x, rectangle.y + rectangle.height / 2], ++ [rectangle.x + rectangle.width * 0.15, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height], ++ [rectangle.x + rectangle.width * 0.15, rectangle.y + rectangle.height] ++ ]; ++}; ++const DisplayEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const shape = rs.path(`M${rectangle.x + rectangle.width * 0.15} ${rectangle.y} ++ H${rectangle.x + rectangle.width - rectangle.width * 0.1} ++ A ${rectangle.width * 0.1} ${rectangle.height / 2}, 0, 0, 1,${rectangle.x + ++ rectangle.width - ++ rectangle.width * 0.1} ${rectangle.y + rectangle.height} ++ H${rectangle.x + rectangle.width * 0.15} ++ L${rectangle.x} ${rectangle.y + rectangle.height / 2} ++ Z ++ `, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ const polygonPoints = [ ++ [rectangle.x, rectangle.y + rectangle.height / 2], ++ [rectangle.x + rectangle.width * 0.15, rectangle.y], ++ [rectangle.x + rectangle.width - rectangle.width * 0.1, rectangle.y], ++ [rectangle.x + rectangle.width - rectangle.width * 0.1, rectangle.y + rectangle.height], ++ [rectangle.x + rectangle.width * 0.15, rectangle.y + rectangle.height] ++ ]; ++ const isInPolygon = isPointInPolygon(point, polygonPoints); ++ const isInEllipse = isPointInEllipse(point, [rectangle.x + rectangle.width - rectangle.width * 0.1, rectangle.y + rectangle.height / 2], rectangle.width * 0.1, rectangle.height / 2); ++ return isInPolygon || isInEllipse; ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ const nearestPoint = getNearestPointBetweenPointAndSegments(point, getDisplayPoints(rectangle)); ++ if (nearestPoint[0] > rectangle.x + rectangle.width - rectangle.width * 0.1) { ++ return getNearestPointBetweenPointAndEllipse(point, [rectangle.x + rectangle.width - rectangle.width * 0.1, rectangle.y + rectangle.height / 2], rectangle.width * 0.1, rectangle.height / 2); ++ } ++ return nearestPoint; ++ }, ++ getTangentVectorByConnectionPoint(rectangle, pointOfRectangle) { ++ const connectionPoint = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ const centerPoint = [rectangle.x + rectangle.width - rectangle.width * 0.1, rectangle.y + rectangle.height / 2]; ++ const point = [connectionPoint[0] - centerPoint[0], -(connectionPoint[1] - centerPoint[1])]; ++ const a = rectangle.width * 0.1; ++ const b = rectangle.height / 2; ++ const slope = getEllipseTangentSlope(point[0], point[1], a, b); ++ return getVectorFromPointAndSlope(point[0], point[1], slope); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle: (board, element) => { ++ return getCustomTextRectangle(board, element, 0.75); ++ } ++}; ++ ++const TableEngine = { ++ draw(board, rectangle, roughOptions, options) { ++ const g = createG(); ++ try { ++ const pointCells = getCellsWithPoints(board, { ...options?.element }); ++ if (pointCells) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const { x, y, width, height } = rectangle; ++ const tableTopBorder = drawLine(rs, [x, y], [x + width, y], roughOptions); ++ const tableLeftBorder = drawLine(rs, [x, y], [x, y + height], roughOptions); ++ g.append(tableTopBorder, tableLeftBorder); ++ pointCells.forEach((cell) => { ++ const rectangle = RectangleClient.getRectangleByPoints(cell.points); ++ const { x, y, width, height } = rectangle; ++ const cellRectangle = drawRectangle(board, { ++ x: x + ACTIVE_STROKE_WIDTH, ++ y: y + ACTIVE_STROKE_WIDTH, ++ width: width - ACTIVE_STROKE_WIDTH * 2, ++ height: height - ACTIVE_STROKE_WIDTH * 2 ++ }, { fill: cell.fill, fillStyle: 'solid', strokeWidth: 0 }); ++ const cellRightBorder = drawLine(rs, [x + width, y], [x + width, y + height], roughOptions); ++ const cellBottomBorder = drawLine(rs, [x, y + height], [x + width, y + height], roughOptions); ++ g.append(cellRectangle, cellRightBorder, cellBottomBorder); ++ }); ++ setStrokeLinecap(g, 'round'); ++ } ++ } ++ catch (error) { ++ console.error(error); ++ } ++ return g; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ return getNearestPointBetweenPointAndRoundRectangle$1(point, rectangle, getRoundRectangleRadius(rectangle)); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle(board, element, options) { ++ try { ++ if (options && options.id) { ++ const cell = getCellWithPoints(board, element, options.id); ++ if (cell) { ++ if (PlaitTableElement.isVerticalText(cell)) { ++ return getVerticalTextRectangle(board, cell); ++ } ++ else { ++ return getHorizontalTextRectangle(board, cell); ++ } ++ } ++ } ++ } ++ catch (error) { ++ console.error(error); ++ } ++ return { ++ x: 0, ++ y: 0, ++ width: 0, ++ height: 0 ++ }; ++ } ++}; ++function getVerticalTextRectangle(board, cell) { ++ const cellRectangle = RectangleClient.getRectangleByPoints(cell.points); ++ const strokeWidth = getStrokeWidthByElement(cell); ++ const width = cellRectangle.height - ShapeDefaultSpace.rectangleAndText * 2 - strokeWidth * 2; ++ return getTextRectangle(board, cell, width, cellRectangle); ++} ++function getHorizontalTextRectangle(board, cell) { ++ const cellRectangle = RectangleClient.getRectangleByPoints(cell.points); ++ const strokeWidth = getStrokeWidthByElement(cell); ++ const width = cellRectangle.width - ShapeDefaultSpace.rectangleAndText * 2 - strokeWidth * 2; ++ return getTextRectangle(board, cell, width, cellRectangle); ++} ++function getTextRectangle(board, cell, width, cellRectangle) { ++ const text = cell.text; ++ if (text) { ++ const textSize = getTextSize(board, text, width); ++ return { ++ width: width > 0 ? width : 0, ++ height: textSize.height, ++ x: cellRectangle.x - width / 2 + cellRectangle.width / 2, ++ y: cellRectangle.y + (cellRectangle.height - textSize.height) / 2 ++ }; ++ } ++ else { ++ return { ++ width: 0, ++ height: 0, ++ x: cellRectangle.x, ++ y: cellRectangle.y ++ }; ++ } ++} ++const getCellTextHeight = (board, cell, isVertical = false) => { ++ if (cell.text) { ++ const cellRectangle = RectangleClient.getRectangleByPoints(cell.points); ++ const strokeWidth = getStrokeWidthByElement(cell); ++ let width = cellRectangle.width - ShapeDefaultSpace.rectangleAndText * 2 - strokeWidth * 2; ++ if (isVertical) { ++ width = cellRectangle.height - ShapeDefaultSpace.rectangleAndText * 2 - strokeWidth * 2; ++ } ++ return getTextSize(board, cell.text, width).height; ++ } ++ return 0; ++}; ++ ++function generateActorPath(rectangle) { ++ const centerX = rectangle.x + rectangle.width / 2; ++ const headRadius = { width: rectangle.width / 3 / 2, height: rectangle.height / 4 / 2 }; ++ const centerY = rectangle.y + rectangle.height / 4 / 2; ++ return { ++ headArcCommand: { ++ rx: headRadius.width, ++ ry: headRadius.height, ++ xAxisRotation: 0, ++ largeArcFlag: 0, ++ sweepFlag: 1, ++ endX: centerX, ++ endY: rectangle.y ++ }, ++ bodyLine: [ ++ [centerX, rectangle.y + rectangle.height / 4], ++ [centerX, rectangle.y + (rectangle.height / 4) * 3] ++ ], ++ armsLine: [ ++ [rectangle.x, rectangle.y + rectangle.height / 2], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2] ++ ], ++ leftLegLine: [ ++ [centerX, rectangle.y + (rectangle.height / 4) * 3], ++ [rectangle.x + rectangle.width / 12, rectangle.y + rectangle.height] ++ ], ++ rightLegLine: [ ++ [centerX, rectangle.y + (rectangle.height / 4) * 3], ++ [rectangle.x + (rectangle.width / 12) * 11, rectangle.y + rectangle.height] ++ ] ++ }; ++} ++const ActorEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const { headArcCommand, bodyLine, armsLine, leftLegLine, rightLegLine } = generateActorPath(rectangle); ++ const pathData = [ ++ // 头部(从中间开始画) ++ `M${bodyLine[0][0]} ${bodyLine[0][1]}`, ++ `A${headArcCommand.rx} ${headArcCommand.ry} ${headArcCommand.xAxisRotation} ${headArcCommand.largeArcFlag} ${headArcCommand.sweepFlag} ${headArcCommand.endX} ${headArcCommand.endY}`, ++ `A${headArcCommand.rx} ${headArcCommand.ry} ${headArcCommand.xAxisRotation} ${headArcCommand.largeArcFlag} ${headArcCommand.sweepFlag} ${bodyLine[0][0]} ${bodyLine[0][1]}`, ++ // 身体 ++ `V${bodyLine[1][1]}`, ++ // 手臂 ++ `M${armsLine[0][0]} ${armsLine[0][1]} H${armsLine[1][0]}`, ++ // 腿 ++ `M${leftLegLine[0][0]} ${leftLegLine[0][1]} L${leftLegLine[1][0]} ${leftLegLine[1][1]}`, ++ `M${rightLegLine[0][0]} ${rightLegLine[0][1]} L${rightLegLine[1][0]} ${rightLegLine[1][1]}` ++ ].join(' '); ++ const shape = rs.path(pathData, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ getNearestPoint(rectangle, point) { ++ const { headArcCommand, bodyLine, armsLine, leftLegLine, rightLegLine } = generateActorPath(rectangle); ++ // 检查头部椭圆 ++ const headCenter = [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height / 4 / 2]; ++ const nearestPointForHead = getNearestPointBetweenPointAndEllipse(point, headCenter, headArcCommand.rx, headArcCommand.ry); ++ const distanceForHead = distanceBetweenPointAndPoint(...point, ...nearestPointForHead); ++ // 检查所有线段 ++ const allSegments = [bodyLine, armsLine, leftLegLine, rightLegLine]; ++ const nearestPointForLines = getNearestPointBetweenPointAndDiscreteSegments(point, allSegments); ++ const distanceForLines = distanceBetweenPointAndPoint(...point, ...nearestPointForLines); ++ return distanceForHead < distanceForLines ? nearestPointForHead : nearestPointForLines; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTangentVectorByConnectionPoint(rectangle, pointOfRectangle) { ++ const connectionPoint = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ if (connectionPoint[1] >= rectangle.y && connectionPoint[1] <= rectangle.y + rectangle.height / 4) { ++ const centerPoint = [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height / 4 / 2]; ++ const point = [connectionPoint[0] - centerPoint[0], -(connectionPoint[1] - centerPoint[1])]; ++ const a = rectangle.width / 2; ++ const b = rectangle.height / 2; ++ const slope = getEllipseTangentSlope(point[0], point[1], a, b); ++ const vector = getVectorFromPointAndSlope(point[0], point[1], slope); ++ return vector; ++ } ++ if (connectionPoint[1] >= rectangle.y + rectangle.height / 4 && connectionPoint[1] < rectangle.y + (rectangle.height / 4) * 3) { ++ if (connectionPoint[0] < rectangle.x + rectangle.width / 2) { ++ return rotateVector(getUnitVectorByPointAndPoint([rectangle.x, rectangle.y + rectangle.height / 2], connectionPoint), -(Math.PI / 2)); ++ } ++ else { ++ return rotateVector(getUnitVectorByPointAndPoint([rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2], connectionPoint), -(Math.PI / 2)); ++ } ++ } ++ if (connectionPoint[1] >= rectangle.y + (rectangle.height / 4) * 3) { ++ if (connectionPoint[0] < rectangle.x + rectangle.width / 2) { ++ return getUnitVectorByPointAndPoint(connectionPoint, [rectangle.x + rectangle.width / 12, rectangle.y + rectangle.height]); ++ } ++ else { ++ return getUnitVectorByPointAndPoint([rectangle.x + (rectangle.width / 12) * 11, rectangle.y + rectangle.height], connectionPoint); ++ } ++ } ++ return getUnitVectorByPointAndPoint(connectionPoint, [rectangle.x + rectangle.width / 4, rectangle.y + rectangle.height]); ++ }, ++ getTextRectangle: (board, element) => { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const width = elementRectangle.width + 40; ++ const text = element.text; ++ const textSize = getTextSize(board, text, width); ++ return { ++ height: textSize.height, ++ width: width > 0 ? width : 0, ++ x: elementRectangle.x - 20, ++ y: elementRectangle.y + elementRectangle.height + 4 ++ }; ++ } ++}; ++ ++const ContainerEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const shape = rs.path(`M${rectangle.x} ${rectangle.y} H${rectangle.x + rectangle.width} V${rectangle.y + rectangle.height} H${rectangle.x} Z M${rectangle.x + 40} ${rectangle.y} L${rectangle.x + 40} ${rectangle.y + rectangle.height} `, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ return getNearestPointBetweenPointAndSegments(point, RectangleEngine.getCornerPoints(rectangle)); ++ }, ++ getEdgeByConnectionPoint(rectangle, pointOfRectangle) { ++ const corners = RectangleEngine.getCornerPoints(rectangle); ++ const point = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ return getPolygonEdgeByConnectionPoint(corners, point); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle(board, element) { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const strokeWidth = getStrokeWidthByElement(element); ++ const width = 40 - ShapeDefaultSpace.rectangleAndText * 2 - strokeWidth * 2; ++ const text = element.text; ++ const textSize = getTextSize(board, text, width); ++ return { ++ height: textSize.height, ++ width: width > 0 ? width : 0, ++ x: elementRectangle.x + ShapeDefaultSpace.rectangleAndText + strokeWidth, ++ y: elementRectangle.y + (elementRectangle.height - textSize.height) / 2 ++ }; ++ } ++}; ++ ++function generatePackagePath(rectangle) { ++ const headerHeight = 25; ++ const topWidth = rectangle.width * 0.7; ++ const cornerX = rectangle.x + rectangle.width * 0.8; ++ return { ++ headerHeight, ++ points: { ++ leftTop: [rectangle.x, rectangle.y + headerHeight], ++ topStart: [rectangle.x, rectangle.y], ++ topEnd: [rectangle.x + topWidth, rectangle.y], ++ cornerPoint: [cornerX, rectangle.y + headerHeight], ++ rightTop: [rectangle.x + rectangle.width, rectangle.y + headerHeight], ++ rightBottom: [rectangle.x + rectangle.width, rectangle.y + rectangle.height], ++ leftBottom: [rectangle.x, rectangle.y + rectangle.height], ++ leftMiddle: [rectangle.x, rectangle.y + headerHeight], ++ middlePoint: [cornerX, rectangle.y + headerHeight] ++ } ++ }; ++} ++const PackageEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const { points } = generatePackagePath(rectangle); ++ const pathData = [ ++ `M${points.leftTop[0]} ${points.leftTop[1]}`, ++ `V${points.topStart[1]}`, ++ `H${points.topEnd[0]}`, ++ `L${points.cornerPoint[0]} ${points.cornerPoint[1]}`, ++ `H${points.rightTop[0]}`, ++ `V${points.rightBottom[1]}`, ++ `H${points.leftBottom[0]}`, ++ `V${points.leftMiddle[1]}`, ++ `H${points.middlePoint[0]}` ++ ].join(' '); ++ const shape = rs.path(pathData, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ const { points } = generatePackagePath(rectangle); ++ const segments = [ ++ // 左边竖线 ++ [points.topStart, points.leftTop], ++ [points.leftTop, points.leftBottom], ++ // 底边 ++ [points.leftBottom, points.rightBottom], ++ // 右边竖线 ++ [points.rightBottom, points.rightTop], ++ // 顶部折线 ++ [points.topStart, points.topEnd], ++ [points.topEnd, points.cornerPoint], ++ [points.cornerPoint, points.rightTop], ++ // 中间横线 ++ [points.leftMiddle, points.middlePoint] ++ ]; ++ return getNearestPointBetweenPointAndDiscreteSegments(point, segments); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTangentVectorByConnectionPoint(rectangle, pointOfRectangle) { ++ const connectionPoint = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ if (connectionPoint[0] > rectangle.x + rectangle.width * 0.7 && connectionPoint[1] < rectangle.y + 25) { ++ return getUnitVectorByPointAndPoint([rectangle.x + rectangle.width * 0.7, rectangle.y], connectionPoint); ++ } ++ return getUnitVectorByPointAndPoint([rectangle.x + rectangle.width * 0.8, rectangle.y + 25], connectionPoint); ++ }, ++ getTextRectangle(board, element, options) { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const strokeWidth = getStrokeWidthByElement(element); ++ const textInfo = element.texts?.find((item) => item.id === options?.id); ++ if (options?.id === GeometryCommonTextKeys.name && textInfo) { ++ const width = elementRectangle.width * 0.7 - ShapeDefaultSpace.rectangleAndText - strokeWidth; ++ const textSize = getTextSize(board, textInfo.text, width); ++ return { ++ height: textSize.height, ++ width: width > 0 ? width : 0, ++ x: elementRectangle.x + ShapeDefaultSpace.rectangleAndText + strokeWidth, ++ y: elementRectangle.y + (25 - textSize.height) / 2 ++ }; ++ } ++ if (options?.id === GeometryCommonTextKeys.content && textInfo) { ++ const width = elementRectangle.width - ShapeDefaultSpace.rectangleAndText * 2 - strokeWidth * 2; ++ const textSize = getTextSize(board, textInfo.text, width); ++ return { ++ height: textSize.height, ++ width: width > 0 ? width : 0, ++ x: elementRectangle.x + ShapeDefaultSpace.rectangleAndText + strokeWidth, ++ y: elementRectangle.y + 25 + (elementRectangle.height - 25 - textSize.height) / 2 ++ }; ++ } ++ return elementRectangle; ++ } ++}; ++ ++const CombinedFragmentEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const shape = rs.path(`M${rectangle.x} ${rectangle.y + 25} ++ V${rectangle.y} ++ H${rectangle.x + rectangle.width} ++ V${rectangle.y + rectangle.height} ++ H${rectangle.x} ++ V${rectangle.y + 25} ++ H${rectangle.x + rectangle.width / 3 - 8} ++ L${rectangle.x + rectangle.width / 3} ${rectangle.y + 16} ++ V${rectangle.y} ++ `, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ return getNearestPointBetweenPointAndSegments(point, RectangleEngine.getCornerPoints(rectangle)); ++ }, ++ getEdgeByConnectionPoint(rectangle, pointOfRectangle) { ++ const corners = RectangleEngine.getCornerPoints(rectangle); ++ const point = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ return getPolygonEdgeByConnectionPoint(corners, point); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle(board, element, options) { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const strokeWidth = getStrokeWidthByElement(element); ++ const textInfo = element.texts?.find((item) => item.id === options?.id); ++ if (options?.id === GeometryCommonTextKeys.name && textInfo) { ++ const width = elementRectangle.width / 3 - 8 - ShapeDefaultSpace.rectangleAndText - strokeWidth; ++ const textSize = getTextSize(board, textInfo.text, width); ++ return { ++ height: textSize.height, ++ width: width > 0 ? width : 0, ++ x: elementRectangle.x + ShapeDefaultSpace.rectangleAndText + strokeWidth, ++ y: elementRectangle.y + (25 - textSize.height) / 2 ++ }; ++ } ++ if (options?.id === GeometryCommonTextKeys.content && textInfo) { ++ const width = elementRectangle.width - ShapeDefaultSpace.rectangleAndText * 2 - strokeWidth * 2; ++ const textSize = getTextSize(board, textInfo.text, width); ++ return { ++ height: textSize.height, ++ width: width > 0 ? width : 0, ++ x: elementRectangle.x + ShapeDefaultSpace.rectangleAndText + strokeWidth, ++ y: elementRectangle.y + 25 + ShapeDefaultSpace.rectangleAndText + strokeWidth ++ }; ++ } ++ return elementRectangle; ++ } ++}; ++ ++function getDeletionLines(rectangle) { ++ return [ ++ [ ++ [rectangle.x, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height] ++ ], ++ [ ++ [rectangle.x + rectangle.width, rectangle.y], ++ [rectangle.x, rectangle.y + rectangle.height] ++ ] ++ ]; ++} ++const DeletionEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const lines = getDeletionLines(rectangle); ++ const shape = rs.path(lines.map(([from, to]) => `M${from[0]} ${from[1]} L${to[0]} ${to[1]}`).join(' '), { ...options, fillStyle: 'solid', strokeWidth: 4 }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ const lines = getDeletionLines(rectangle); ++ let minDistance = Infinity; ++ let nearestPoint = point; ++ lines.forEach(line => { ++ const currentPoint = getNearestPointBetweenPointAndSegment(point, line); ++ const distance = distanceBetweenPointAndPoint(point[0], point[1], currentPoint[0], currentPoint[1]); ++ if (distance < minDistance) { ++ minDistance = distance; ++ nearestPoint = currentPoint; ++ } ++ }); ++ return nearestPoint; ++ }, ++ getEdgeByConnectionPoint(rectangle, pointOfRectangle) { ++ const corners = RectangleEngine.getCornerPoints(rectangle); ++ const point = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ return getPolygonEdgeByConnectionPoint(corners, point); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ } ++}; ++ ++const ActiveClassEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const shape = rs.path(`M${rectangle.x} ${rectangle.y} H${rectangle.x + rectangle.width} V${rectangle.y + rectangle.height} H${rectangle.x} Z M${rectangle.x + rectangle.width * 0.125} ${rectangle.y} L${rectangle.x + rectangle.width * 0.125} ${rectangle.y + ++ rectangle.height} M${rectangle.x + rectangle.width - rectangle.width * 0.125} ${rectangle.y} L${rectangle.x + ++ rectangle.width - ++ rectangle.width * 0.125} ${rectangle.y + rectangle.height}`, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ return getNearestPointBetweenPointAndSegments(point, RectangleEngine.getCornerPoints(rectangle)); ++ }, ++ getEdgeByConnectionPoint(rectangle, pointOfRectangle) { ++ const corners = RectangleEngine.getCornerPoints(rectangle); ++ const point = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ return getPolygonEdgeByConnectionPoint(corners, point); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle: (board, element) => { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const strokeWidth = getStrokeWidthByElement(element); ++ const width = elementRectangle.width - ShapeDefaultSpace.rectangleAndText * 2 - strokeWidth * 2 - elementRectangle.width * 0.125 * 2; ++ const text = element.text; ++ const textSize = getTextSize(board, text, width); ++ return { ++ height: textSize.height, ++ width: width > 0 ? width : 0, ++ x: elementRectangle.x + ShapeDefaultSpace.rectangleAndText + strokeWidth + elementRectangle.width * 0.125, ++ y: elementRectangle.y + (elementRectangle.height - textSize.height) / 2 ++ }; ++ } ++}; ++ ++const NoteEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const shape = rs.path(`M${rectangle.x} ${rectangle.y} ++ h${rectangle.width - 16} ++ v16 ++ h16 ++ v${rectangle.height - 16} ++ h${-rectangle.width} ++ Z ++ M${rectangle.x + rectangle.width - 16} ${rectangle.y} ++ A16 16, 0,0,1, ${rectangle.x + rectangle.width} ${rectangle.y + 16} ++ `, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ const nearestPoint = getNearestPointBetweenPointAndSegments(point, RectangleEngine.getCornerPoints(rectangle)); ++ if (nearestPoint[0] > rectangle.x + rectangle.width - 16 && nearestPoint[1] < rectangle.y + 16) { ++ return getNearestPointBetweenPointAndEllipse(point, [rectangle.x + rectangle.width - 16, rectangle.y + 16], 16, 16); ++ } ++ return nearestPoint; ++ }, ++ getTangentVectorByConnectionPoint(rectangle, pointOfRectangle) { ++ const connectionPoint = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ const centerPoint = [rectangle.x + (rectangle.width * 3) / 4, rectangle.y + rectangle.height / 2]; ++ const point = [connectionPoint[0] - centerPoint[0], -(connectionPoint[1] - centerPoint[1])]; ++ const slope = getEllipseTangentSlope(point[0], point[1], 16, 16); ++ return getVectorFromPointAndSlope(point[0], point[1], slope); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle: (board, element) => { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const strokeWidth = getStrokeWidthByElement(element); ++ const width = elementRectangle.width - ShapeDefaultSpace.rectangleAndText * 2 - strokeWidth - 15; ++ const text = element.text; ++ const textSize = getTextSize(board, text, width); ++ return { ++ height: textSize.height, ++ width: width > 0 ? width : 0, ++ x: elementRectangle.x + ShapeDefaultSpace.rectangleAndText + strokeWidth, ++ y: elementRectangle.y + (elementRectangle.height - textSize.height) / 2 ++ }; ++ } ++}; ++ ++function generateAssemblyPath(rectangle) { ++ const centerY = rectangle.y + rectangle.height / 2; ++ const firstLineEndX = rectangle.x + rectangle.width * 0.3; ++ const circleWidth = rectangle.width * 0.13; ++ const circleHeight = rectangle.height * 0.285; ++ const verticalX = firstLineEndX + circleWidth; ++ const verticalRadius = rectangle.width * 0.233; ++ return { ++ startPoint: [rectangle.x, centerY], ++ line1: [ ++ [rectangle.x, centerY], ++ [firstLineEndX, centerY] ++ ], ++ circleArcCommand: { ++ rx: circleWidth, ++ ry: circleHeight, ++ xAxisRotation: 0, ++ largeArcFlag: 1, ++ sweepFlag: 1, ++ endX: firstLineEndX, ++ endY: centerY ++ }, ++ verticalArcCommand: { ++ rx: verticalRadius, ++ ry: rectangle.height / 2, ++ xAxisRotation: 0, ++ largeArcFlag: 0, ++ sweepFlag: 1, ++ endX: verticalX, ++ endY: rectangle.y + rectangle.height ++ }, ++ line2: [ ++ [verticalX + verticalRadius, centerY], ++ [rectangle.x + rectangle.width, centerY] ++ ] ++ }; ++} ++const AssemblyEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const { startPoint, line1, circleArcCommand, verticalArcCommand, line2 } = generateAssemblyPath(rectangle); ++ const pathData = [ ++ `M${startPoint[0]} ${startPoint[1]}`, ++ `H${line1[1][0]}`, ++ // 画完整的圆形:先画一个半圆,再画另一个半圆 ++ `A${circleArcCommand.rx} ${circleArcCommand.ry} ${circleArcCommand.xAxisRotation} ${circleArcCommand.largeArcFlag} ${circleArcCommand.sweepFlag} ${line1[1][0] + circleArcCommand.rx * 2} ${circleArcCommand.endY}`, ++ `A${circleArcCommand.rx} ${circleArcCommand.ry} ${circleArcCommand.xAxisRotation} ${circleArcCommand.largeArcFlag} ${circleArcCommand.sweepFlag} ${circleArcCommand.endX} ${circleArcCommand.endY}`, ++ // 垂直椭圆 ++ `M${verticalArcCommand.endX} ${rectangle.y}`, ++ `A${verticalArcCommand.rx} ${verticalArcCommand.ry} ${verticalArcCommand.xAxisRotation} ${verticalArcCommand.largeArcFlag} ${verticalArcCommand.sweepFlag} ${verticalArcCommand.endX} ${verticalArcCommand.endY}`, ++ // 最后一条线 ++ `M${line2[0][0]} ${line2[0][1]} H${line2[1][0]}` ++ ].join(' '); ++ const shape = rs.path(pathData, { ++ ...options, ++ fillStyle: 'solid' ++ }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ const { line1, line2, circleArcCommand, verticalArcCommand } = generateAssemblyPath(rectangle); ++ // 检查直线段 ++ const nearestPointForLines = getNearestPointBetweenPointAndSegments(point, [...line1, ...line2]); ++ const distanceForLines = distanceBetweenPointAndPoint(...point, ...nearestPointForLines); ++ // 检查中间圆形 ++ const circleCenter = [line1[1][0] + circleArcCommand.rx, line1[1][1]]; ++ const nearestPointForCircle = getNearestPointBetweenPointAndEllipse(point, circleCenter, circleArcCommand.rx, circleArcCommand.ry); ++ const distanceForCircle = distanceBetweenPointAndPoint(...point, ...nearestPointForCircle); ++ // 检查垂直椭圆(使用 getNearestPointBetweenPointAndArc 处理半圆弧) ++ const arcStartPoint = [verticalArcCommand.endX, rectangle.y]; ++ const nearestPointForEllipse = getNearestPointBetweenPointAndArc(point, arcStartPoint, verticalArcCommand); ++ const distanceForEllipse = distanceBetweenPointAndPoint(...point, ...nearestPointForEllipse); ++ // 返回最近的点 ++ const minDistance = Math.min(distanceForLines, distanceForCircle, distanceForEllipse); ++ if (minDistance === distanceForLines) ++ return nearestPointForLines; ++ if (minDistance === distanceForCircle) ++ return nearestPointForCircle; ++ return nearestPointForEllipse; ++ }, ++ getTangentVectorByConnectionPoint(rectangle, pointOfRectangle) { ++ const connectionPoint = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ if (connectionPoint[0] > rectangle.x + rectangle.width * 0.43 && connectionPoint[1] < rectangle.y + rectangle.height / 2) { ++ return rotateVector(getUnitVectorByPointAndPoint(connectionPoint, [rectangle.x, rectangle.y + rectangle.height / 2]), -Math.PI); ++ } ++ if (connectionPoint[0] > rectangle.x + rectangle.width * 0.43 && connectionPoint[1] > rectangle.y + rectangle.height / 2) { ++ return getUnitVectorByPointAndPoint(connectionPoint, [rectangle.x, rectangle.y + rectangle.height / 2]); ++ } ++ return getUnitVectorByPointAndPoint(connectionPoint, [rectangle.x, rectangle.y + rectangle.height / 2]); ++ } ++}; ++ ++function generateRequiredInterfacePath(rectangle) { ++ const arcWidth = rectangle.width * 0.39; ++ const arcHeight = rectangle.height / 2; ++ return { ++ startPoint: [rectangle.x, rectangle.y], ++ leftArcCommand: { ++ rx: arcWidth, ++ ry: arcHeight, ++ xAxisRotation: 0, ++ largeArcFlag: 0, ++ sweepFlag: 1, ++ endX: rectangle.x, ++ endY: rectangle.y + rectangle.height ++ }, ++ line: { ++ startX: rectangle.x + rectangle.width * 0.41, ++ startY: rectangle.y + rectangle.height / 2, ++ endX: rectangle.x + rectangle.width, ++ endY: rectangle.y + rectangle.height / 2 ++ } ++ }; ++} ++const RequiredInterfaceEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const { startPoint, leftArcCommand, line } = generateRequiredInterfacePath(rectangle); ++ const pathData = [ ++ `M${startPoint[0]} ${startPoint[1]}`, ++ `A${leftArcCommand.rx} ${leftArcCommand.ry} ${leftArcCommand.xAxisRotation} ${leftArcCommand.largeArcFlag} ${leftArcCommand.sweepFlag} ${leftArcCommand.endX} ${leftArcCommand.endY}`, ++ `M${line.startX} ${line.startY} H${line.endX}` ++ ].join(' '); ++ const shape = rs.path(pathData, { ++ ...options, ++ fillStyle: 'solid', ++ fill: 'transparent' ++ }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ const { startPoint, leftArcCommand, line } = generateRequiredInterfacePath(rectangle); ++ let minDistance = Infinity; ++ let nearestPoint = point; ++ // 检查圆弧段 ++ const arcNearestPoint = getNearestPointBetweenPointAndArc(point, startPoint, leftArcCommand); ++ const arcDistance = distanceBetweenPointAndPoint(point[0], point[1], arcNearestPoint[0], arcNearestPoint[1]); ++ if (arcDistance < minDistance) { ++ minDistance = arcDistance; ++ nearestPoint = arcNearestPoint; ++ } ++ // 检查直线段 ++ const lineStart = [line.startX, line.startY]; ++ const lineEnd = [line.endX, line.endY]; ++ const lineNearestPoint = getNearestPointBetweenPointAndSegment(point, [lineStart, lineEnd]); ++ const lineDistance = distanceBetweenPointAndPoint(point[0], point[1], lineNearestPoint[0], lineNearestPoint[1]); ++ if (lineDistance < minDistance) { ++ minDistance = lineDistance; ++ nearestPoint = lineNearestPoint; ++ } ++ return nearestPoint; ++ }, ++ getEdgeByConnectionPoint(rectangle, pointOfRectangle) { ++ const corners = RectangleEngine.getCornerPoints(rectangle); ++ const point = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ return getPolygonEdgeByConnectionPoint(corners, point); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ } ++}; ++ ++const percentage = 0.54; ++function generateProvidedInterfacePath(rectangle) { ++ const centerY = rectangle.y + rectangle.height / 2; ++ const rx = (rectangle.width * (1 - percentage)) / 2; ++ const ry = rectangle.height / 2; ++ const startPoint = [rectangle.x, centerY]; ++ const lineEndX = rectangle.x + rectangle.width * percentage; ++ return { ++ startPoint, ++ line: { ++ startX: startPoint[0], ++ startY: centerY, ++ endX: lineEndX, ++ endY: centerY ++ }, ++ arcCommands: [ ++ { ++ rx, ++ ry, ++ xAxisRotation: 0, ++ largeArcFlag: 1, ++ sweepFlag: 1, ++ endX: rectangle.x + rectangle.width, ++ endY: centerY ++ }, ++ { ++ rx, ++ ry, ++ xAxisRotation: 0, ++ largeArcFlag: 1, ++ sweepFlag: 1, ++ endX: lineEndX, ++ endY: centerY ++ } ++ ] ++ }; ++} ++const ProvidedInterfaceEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const { startPoint, line, arcCommands } = generateProvidedInterfacePath(rectangle); ++ const pathData = [ ++ `M${startPoint[0]} ${startPoint[1]}`, ++ `H${line.endX}`, ++ ...arcCommands.map((command) => `A${command.rx} ${command.ry} ${command.xAxisRotation} ${command.largeArcFlag} ${command.sweepFlag} ${command.endX} ${command.endY}`) ++ ].join(' '); ++ const shape = rs.path(pathData, { ++ ...options, ++ fillStyle: 'solid' ++ }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ const { startPoint, line, arcCommands } = generateProvidedInterfacePath(rectangle); ++ // 检查直线段 ++ const lineStart = [line.startX, line.startY]; ++ const lineEnd = [line.endX, line.endY]; ++ const nearestPointForLine = getNearestPointBetweenPointAndSegments(point, [lineStart, lineEnd]); ++ const distanceForLine = distanceBetweenPointAndPoint(...point, ...nearestPointForLine); ++ // 检查圆弧段 ++ const arcCenter = [rectangle.x + (3 * rectangle.width) / 4, line.startY]; ++ const nearestPointForEllipse = getNearestPointBetweenPointAndEllipse(point, arcCenter, arcCommands[0].rx, arcCommands[0].ry); ++ const distanceForEllipse = distanceBetweenPointAndPoint(...point, ...nearestPointForEllipse); ++ return distanceForLine < distanceForEllipse ? nearestPointForLine : nearestPointForEllipse; ++ }, ++ getTangentVectorByConnectionPoint(rectangle, pointOfRectangle) { ++ const connectionPoint = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ const centerPoint = [rectangle.x + (rectangle.width * 3) / 4, rectangle.y + rectangle.height / 2]; ++ if (connectionPoint[0] > rectangle.x + rectangle.width * 0.54) { ++ const point = [connectionPoint[0] - centerPoint[0], -(connectionPoint[1] - centerPoint[1])]; ++ const rx = (rectangle.width * 0.46) / 2; ++ const ry = rectangle.height / 2; ++ const slope = getEllipseTangentSlope(point[0], point[1], rx, ry); ++ return getVectorFromPointAndSlope(point[0], point[1], slope); ++ } ++ return getUnitVectorByPointAndPoint(connectionPoint, [rectangle.x, rectangle.y + rectangle.height / 2]); ++ } ++}; ++ ++function generateComponentPath(rectangle) { ++ const mainLineX = rectangle.x + 12; ++ const boxWidth = rectangle.width > 70 ? 24 : rectangle.width * 0.2; ++ const boxHeight = rectangle.height - 28 - rectangle.height * 0.35 > 1 ? 14 : rectangle.height * 0.175; ++ const topBoxY = rectangle.y + rectangle.height * 0.175; ++ const bottomBoxY = rectangle.y + rectangle.height - rectangle.height * 0.175 - boxHeight; ++ return { ++ boxSize: { ++ width: boxWidth, ++ height: boxHeight ++ }, ++ points: { ++ mainStart: [mainLineX, rectangle.y], ++ topBoxStart: [mainLineX, topBoxY], ++ topBoxEnd: [mainLineX, topBoxY + boxHeight], ++ bottomBoxStart: [mainLineX, bottomBoxY], ++ bottomBoxEnd: [mainLineX, bottomBoxY + boxHeight], ++ mainEnd: [mainLineX, rectangle.y + rectangle.height], ++ rightTop: [rectangle.x + rectangle.width, rectangle.y], ++ rightBottom: [rectangle.x + rectangle.width, rectangle.y + rectangle.height] ++ } ++ }; ++} ++const ComponentEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const { boxSize, points } = generateComponentPath(rectangle); ++ const pathData = [ ++ // 主矩形轮廓 ++ `M${points.mainStart[0]} ${points.mainStart[1]}`, ++ `H${points.rightTop[0]}`, ++ `V${points.rightBottom[1]}`, ++ `H${points.mainEnd[0]}`, ++ // 上方小矩形 ++ `M${points.topBoxStart[0]} ${points.topBoxStart[1]}`, ++ `h${boxSize.width / 2} v${boxSize.height} h${-boxSize.width} v${-boxSize.height} h${boxSize.width / 2}`, ++ // 下方小矩形 ++ `M${points.bottomBoxStart[0]} ${points.bottomBoxStart[1]}`, ++ `h${boxSize.width / 2} v${boxSize.height} h${-boxSize.width} v${-boxSize.height} h${boxSize.width / 2}`, ++ // 连接线 ++ `M${points.mainStart[0]} ${points.mainStart[1]}`, ++ `V${points.topBoxStart[1]}`, ++ `M${points.topBoxEnd[0]} ${points.topBoxEnd[1]}`, ++ `V${points.bottomBoxStart[1]}`, ++ `M${points.bottomBoxEnd[0]} ${points.bottomBoxEnd[1]}`, ++ `V${points.mainEnd[1]}` ++ ].join(' '); ++ const shape = rs.path(pathData, { ...options, fillStyle: 'solid' }); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ getNearestPoint(rectangle, point) { ++ const { boxSize, points } = generateComponentPath(rectangle); ++ const segments = [ ++ // 主矩形轮廓 ++ [points.mainStart, [points.rightTop[0], points.mainStart[1]]], ++ [[points.rightTop[0], points.mainStart[1]], points.rightBottom], ++ [points.rightBottom, [points.mainEnd[0], points.rightBottom[1]]], ++ [[points.mainEnd[0], points.rightBottom[1]], points.mainStart], ++ // 上方小矩形 ++ [points.topBoxStart, [points.topBoxStart[0] + boxSize.width / 2, points.topBoxStart[1]]], ++ [[points.topBoxStart[0] + boxSize.width / 2, points.topBoxStart[1]], [points.topBoxStart[0] + boxSize.width / 2, points.topBoxEnd[1]]], ++ [[points.topBoxStart[0] + boxSize.width / 2, points.topBoxEnd[1]], [points.topBoxStart[0] - boxSize.width / 2, points.topBoxEnd[1]]], ++ [[points.topBoxStart[0] - boxSize.width / 2, points.topBoxEnd[1]], [points.topBoxStart[0] - boxSize.width / 2, points.topBoxStart[1]]], ++ [[points.topBoxStart[0] - boxSize.width / 2, points.topBoxStart[1]], points.topBoxStart], ++ // 下方小矩形 ++ [points.bottomBoxStart, [points.bottomBoxStart[0] + boxSize.width / 2, points.bottomBoxStart[1]]], ++ [[points.bottomBoxStart[0] + boxSize.width / 2, points.bottomBoxStart[1]], [points.bottomBoxStart[0] + boxSize.width / 2, points.bottomBoxEnd[1]]], ++ [[points.bottomBoxStart[0] + boxSize.width / 2, points.bottomBoxEnd[1]], [points.bottomBoxStart[0] - boxSize.width / 2, points.bottomBoxEnd[1]]], ++ [[points.bottomBoxStart[0] - boxSize.width / 2, points.bottomBoxEnd[1]], [points.bottomBoxStart[0] - boxSize.width / 2, points.bottomBoxStart[1]]], ++ [[points.bottomBoxStart[0] - boxSize.width / 2, points.bottomBoxStart[1]], points.bottomBoxStart], ++ // 连接线 ++ [points.mainStart, points.topBoxStart], ++ [points.topBoxEnd, points.bottomBoxStart], ++ [points.bottomBoxEnd, points.mainEnd] ++ ]; ++ return getNearestPointBetweenPointAndDiscreteSegments(point, segments); ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getTangentVectorByConnectionPoint(rectangle, pointOfRectangle) { ++ const { points } = generateComponentPath(rectangle); ++ const connectionPoint = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ return getUnitVectorByPointAndPoint(points.mainStart, connectionPoint); ++ }, ++ getConnectorPoints(rectangle) { ++ const { points } = generateComponentPath(rectangle); ++ return [ ++ [rectangle.x + rectangle.width / 2, rectangle.y], ++ [rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2], ++ [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height], ++ [points.mainStart[0], rectangle.y + rectangle.height / 2] ++ ]; ++ }, ++ getTextRectangle(board, element) { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const strokeWidth = getStrokeWidthByElement(element); ++ const width = elementRectangle.width - 24 - ShapeDefaultSpace.rectangleAndText * 2 - strokeWidth * 2; ++ const text = element.text; ++ const textSize = getTextSize(board, text, width); ++ return { ++ height: textSize.height, ++ width: width > 0 ? width : 0, ++ x: elementRectangle.x + 24 + ShapeDefaultSpace.rectangleAndText + strokeWidth, ++ y: elementRectangle.y + (elementRectangle.height - textSize.height) / 2 ++ }; ++ } ++}; ++ ++const ComponentBoxEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ const componentWidth = rectangle.width - 45 * 2 - 18 > 1 ? 45 : rectangle.width * 0.25; ++ const componentHeight = rectangle.height - 30 - 8 * 2 > 1 ? 30 : rectangle.height * 0.2; ++ const componentRectangle = { ++ x: rectangle.x + rectangle.width - 18 - componentWidth, ++ y: rectangle.y + 8, ++ width: componentWidth, ++ height: componentHeight ++ }; ++ const shape = rs.path(`M${rectangle.x} ${rectangle.y} ++ H${rectangle.x + rectangle.width} ++ V${rectangle.y + rectangle.height} ++ H${rectangle.x} Z ++ ++ `, { ...options, fillStyle: 'solid' }); ++ const componentShape = ComponentEngine.draw(board, componentRectangle, options); ++ shape.append(componentShape); ++ setStrokeLinecap(shape, 'round'); ++ return shape; ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ return getNearestPointBetweenPointAndSegments(point, RectangleEngine.getCornerPoints(rectangle)); ++ }, ++ getEdgeByConnectionPoint(rectangle, pointOfRectangle) { ++ const corners = RectangleEngine.getCornerPoints(rectangle); ++ const point = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ return getPolygonEdgeByConnectionPoint(corners, point); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle(board, element) { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const strokeWidth = getStrokeWidthByElement(element); ++ const componentWidth = elementRectangle.width - 45 * 2 - 18 > 1 ? 45 : elementRectangle.width * 0.25; ++ const width = elementRectangle.width - 18 - componentWidth - ShapeDefaultSpace.rectangleAndText - strokeWidth * 2; ++ const text = element.text; ++ const textSize = getTextSize(board, text, width); ++ return { ++ height: textSize.height, ++ width: width > 0 ? width : 0, ++ x: elementRectangle.x + ShapeDefaultSpace.rectangleAndText + strokeWidth, ++ y: elementRectangle.y + (elementRectangle.height - textSize.height) / 2 ++ }; ++ } ++}; ++ ++const TemplateEngine = { ++ draw(board, rectangle, options) { ++ const rs = PlaitBoard.getRoughSVG(board); ++ return drawRoundRectangle(rs, rectangle.x, rectangle.y, rectangle.x + rectangle.width, rectangle.y + rectangle.height, { ++ ...options, ++ fillStyle: 'solid', ++ dashGap: 10, ++ strokeLineDash: [10, 10] ++ }, false, 4); ++ }, ++ isInsidePoint(rectangle, point) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); ++ return RectangleClient.isHit(rectangle, rangeRectangle); ++ }, ++ getCornerPoints(rectangle) { ++ return RectangleClient.getCornerPoints(rectangle); ++ }, ++ getNearestPoint(rectangle, point) { ++ return getNearestPointBetweenPointAndSegments(point, RectangleEngine.getCornerPoints(rectangle)); ++ }, ++ getEdgeByConnectionPoint(rectangle, pointOfRectangle) { ++ const corners = RectangleEngine.getCornerPoints(rectangle); ++ const point = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle); ++ return getPolygonEdgeByConnectionPoint(corners, point); ++ }, ++ getConnectorPoints(rectangle) { ++ return RectangleClient.getEdgeCenterPoints(rectangle); ++ }, ++ getTextRectangle(board, element) { ++ const elementRectangle = RectangleClient.getRectangleByPoints(element.points); ++ const strokeWidth = getStrokeWidthByElement(element); ++ const width = elementRectangle.width - ShapeDefaultSpace.rectangleAndText * 2 - strokeWidth * 2; ++ const text = element.text; ++ const textSize = getTextSize(board, text, width); ++ return { ++ height: textSize.height, ++ width: width > 0 ? width : 0, ++ x: elementRectangle.x + ShapeDefaultSpace.rectangleAndText + strokeWidth, ++ y: elementRectangle.y + (elementRectangle.height - textSize.height) / 2 ++ }; ++ } ++}; ++ ++const ShapeEngineMap = { ++ [BasicShapes.rectangle]: RectangleEngine, ++ [BasicShapes.diamond]: DiamondEngine, ++ [BasicShapes.ellipse]: EllipseEngine, ++ [BasicShapes.parallelogram]: ParallelogramEngine, ++ [BasicShapes.roundRectangle]: RoundRectangleEngine, ++ [BasicShapes.text]: RectangleEngine, ++ [BasicShapes.triangle]: TriangleEngine, ++ [BasicShapes.leftArrow]: LeftArrowEngine, ++ [BasicShapes.trapezoid]: TrapezoidEngine, ++ [BasicShapes.rightArrow]: RightArrowEngine, ++ [BasicShapes.cross]: CrossEngine, ++ [BasicShapes.star]: StarEngine, ++ [BasicShapes.pentagon]: PentagonEngine, ++ [BasicShapes.hexagon]: HexagonEngine, ++ [BasicShapes.octagon]: OctagonEngine, ++ [BasicShapes.pentagonArrow]: PentagonArrowEngine, ++ [BasicShapes.processArrow]: ProcessArrowEngine, ++ [BasicShapes.twoWayArrow]: TwoWayArrowEngine, ++ [BasicShapes.comment]: CommentEngine, ++ [BasicShapes.roundComment]: RoundCommentEngine, ++ [BasicShapes.cloud]: CloudEngine, ++ [FlowchartSymbols.process]: RectangleEngine, ++ [FlowchartSymbols.decision]: DiamondEngine, ++ [FlowchartSymbols.connector]: EllipseEngine, ++ [FlowchartSymbols.data]: ParallelogramEngine, ++ [FlowchartSymbols.terminal]: TerminalEngine, ++ [FlowchartSymbols.database]: DatabaseEngine, ++ [FlowchartSymbols.hardDisk]: HardDiskEngine, ++ [FlowchartSymbols.internalStorage]: InternalStorageEngine, ++ [FlowchartSymbols.manualInput]: ManualInputEngine, ++ [FlowchartSymbols.preparation]: PreparationEngine, ++ [FlowchartSymbols.manualLoop]: ManualLoopEngine, ++ [FlowchartSymbols.merge]: MergeEngine, ++ [FlowchartSymbols.delay]: DelayEngine, ++ [FlowchartSymbols.storedData]: StoredDataEngine, ++ [FlowchartSymbols.or]: OrEngine, ++ [FlowchartSymbols.summingJunction]: SummingJunctionEngine, ++ [FlowchartSymbols.predefinedProcess]: PredefinedProcessEngine, ++ [FlowchartSymbols.offPage]: OffPageEngine, ++ [FlowchartSymbols.document]: DocumentEngine, ++ [FlowchartSymbols.multiDocument]: MultiDocumentEngine, ++ [FlowchartSymbols.noteCurlyLeft]: NoteCurlyLeftEngine, ++ [FlowchartSymbols.noteCurlyRight]: NoteCurlyRightEngine, ++ [FlowchartSymbols.noteSquare]: NoteSquareEngine, ++ [FlowchartSymbols.display]: DisplayEngine, ++ [SwimlaneSymbols.swimlaneHorizontal]: TableEngine, ++ [SwimlaneSymbols.swimlaneVertical]: TableEngine, ++ [TableSymbols.table]: TableEngine, ++ [UMLSymbols.actor]: ActorEngine, ++ [UMLSymbols.useCase]: EllipseEngine, ++ [UMLSymbols.container]: ContainerEngine, ++ [UMLSymbols.note]: NoteEngine, ++ [UMLSymbols.package]: PackageEngine, ++ [UMLSymbols.combinedFragment]: CombinedFragmentEngine, ++ [UMLSymbols.class]: TableEngine, ++ [UMLSymbols.interface]: TableEngine, ++ [UMLSymbols.activation]: RectangleEngine, ++ [UMLSymbols.object]: RectangleEngine, ++ [UMLSymbols.deletion]: DeletionEngine, ++ [UMLSymbols.activityClass]: ActiveClassEngine, ++ [UMLSymbols.simpleClass]: RectangleEngine, ++ [UMLSymbols.component]: ComponentEngine, ++ [UMLSymbols.componentBox]: ComponentBoxEngine, ++ [UMLSymbols.template]: TemplateEngine, ++ [UMLSymbols.port]: RectangleEngine, ++ [UMLSymbols.branchMerge]: DiamondEngine, ++ [UMLSymbols.assembly]: AssemblyEngine, ++ [UMLSymbols.requiredInterface]: RequiredInterfaceEngine, ++ [UMLSymbols.providedInterface]: ProvidedInterfaceEngine ++}; ++const getEngine = (shape) => { ++ return ShapeEngineMap[shape]; ++}; ++ ++const getArrowLineHandleRefPair = (board, element) => { ++ const strokeWidth = getStrokeWidthByElement(element); ++ const sourceBoundElement = element.source.boundId ? getElementById(board, element.source.boundId) : undefined; ++ const targetBoundElement = element.target.boundId ? getElementById(board, element.target.boundId) : undefined; ++ let sourcePoint = sourceBoundElement ? getConnectionPoint(sourceBoundElement, element.source.connection) : element.points[0]; ++ let targetPoint = targetBoundElement ++ ? getConnectionPoint(targetBoundElement, element.target.connection) ++ : element.points[element.points.length - 1]; ++ let sourceDirection = getDirectionByVector([targetPoint[0] - sourcePoint[0], targetPoint[1] - sourcePoint[1]]) || Direction.right; ++ let targetDirection = getOppositeDirection(sourceDirection); ++ const sourceFactor = getDirectionFactor(sourceDirection); ++ const targetFactor = getDirectionFactor(targetDirection); ++ const sourceHandleRef = { ++ key: ArrowLineHandleKey.source, ++ point: sourcePoint, ++ direction: sourceDirection, ++ vector: [sourceFactor.x, sourceFactor.y] ++ }; ++ const targetHandleRef = { ++ key: ArrowLineHandleKey.target, ++ point: targetPoint, ++ direction: targetDirection, ++ vector: [targetFactor.x, targetFactor.y] ++ }; ++ if (sourceBoundElement) { ++ const connectionOffset = PlaitArrowLine.isSourceMarkOrTargetMark(element, ArrowLineMarkerType.none, ArrowLineHandleKey.source) ++ ? 0 ++ : strokeWidth; ++ const sourceVector = getVectorByConnection(sourceBoundElement, element.source.connection); ++ sourceHandleRef.vector = sourceVector; ++ sourceHandleRef.boundElement = sourceBoundElement; ++ if (hasValidAngle(sourceBoundElement)) { ++ const direction = getDirectionByVector(rotateVector(sourceVector, sourceBoundElement.angle)); ++ sourceDirection = direction ? direction : sourceDirection; ++ } ++ else { ++ const direction = getDirectionByVector(sourceVector); ++ sourceDirection = direction ? direction : sourceDirection; ++ } ++ sourceHandleRef.direction = sourceDirection; ++ sourcePoint = getConnectionPoint(sourceBoundElement, element.source.connection, sourceDirection, connectionOffset); ++ sourceHandleRef.point = rotatePointsByElement(sourcePoint, sourceBoundElement) || sourcePoint; ++ } ++ if (targetBoundElement) { ++ const connectionOffset = PlaitArrowLine.isSourceMarkOrTargetMark(element, ArrowLineMarkerType.none, ArrowLineHandleKey.target) ++ ? 0 ++ : strokeWidth; ++ const targetVector = getVectorByConnection(targetBoundElement, element.target.connection); ++ targetHandleRef.vector = targetVector; ++ targetHandleRef.boundElement = targetBoundElement; ++ if (hasValidAngle(targetBoundElement)) { ++ const direction = getDirectionByVector(rotateVector(targetVector, targetBoundElement.angle)); ++ targetDirection = direction ? direction : targetDirection; ++ } ++ else { ++ const direction = getDirectionByVector(targetVector); ++ targetDirection = direction ? direction : targetDirection; ++ } ++ targetHandleRef.direction = targetDirection; ++ targetPoint = getConnectionPoint(targetBoundElement, element.target.connection, targetDirection, connectionOffset); ++ targetHandleRef.point = rotatePointsByElement(targetPoint, targetBoundElement) || targetPoint; ++ } ++ return { source: sourceHandleRef, target: targetHandleRef }; ++}; ++const getConnectionPoint = (geometry, connection, direction, delta) => { ++ const rectangle = RectangleClient.getRectangleByPoints(geometry.points); ++ if (direction && delta) { ++ const directionFactor = getDirectionFactor(direction); ++ const point = RectangleClient.getConnectionPoint(rectangle, connection); ++ return [point[0] + directionFactor.x * delta, point[1] + directionFactor.y * delta]; ++ } ++ else { ++ return RectangleClient.getConnectionPoint(rectangle, connection); ++ } ++}; ++const getVectorByConnection = (boundElement, connection) => { ++ const rectangle = RectangleClient.getRectangleByPoints(boundElement.points); ++ const shape = getElementShape(boundElement); ++ const engine = getEngine(shape); ++ let vector = [0, 0]; ++ const direction = getDirectionByPointOfRectangle(connection); ++ if (direction && boundElement.shape !== BasicShapes.ellipse) { ++ const factor = getDirectionFactor(direction); ++ return [factor.x, factor.y]; ++ } ++ if (engine.getEdgeByConnectionPoint) { ++ const edge = engine.getEdgeByConnectionPoint(rectangle, connection); ++ if (edge) { ++ const lineVector = [edge[1][0] - edge[0][0], edge[1][1] - edge[0][1]]; ++ return rotateVectorAnti90(lineVector); ++ } ++ } ++ if (engine.getTangentVectorByConnectionPoint) { ++ const lineVector = engine.getTangentVectorByConnectionPoint(rectangle, connection); ++ if (lineVector) { ++ return rotateVectorAnti90(lineVector); ++ } ++ } ++ return vector; ++}; ++const getElbowLineRouteOptions = (board, element, handleRefPair) => { ++ handleRefPair = handleRefPair ?? getArrowLineHandleRefPair(board, element); ++ const { sourceRectangle, targetRectangle } = getSourceAndTargetRectangle(board, element, handleRefPair); ++ const { sourceOuterRectangle, targetOuterRectangle } = getSourceAndTargetOuterRectangle(sourceRectangle, targetRectangle); ++ const sourcePoint = handleRefPair.source.point; ++ const targetPoint = handleRefPair.target.point; ++ const nextSourcePoint = getNextPoint(sourcePoint, sourceOuterRectangle, handleRefPair.source.direction); ++ const nextTargetPoint = getNextPoint(targetPoint, targetOuterRectangle, handleRefPair.target.direction); ++ return { ++ sourcePoint, ++ nextSourcePoint, ++ sourceRectangle, ++ sourceOuterRectangle, ++ targetPoint, ++ nextTargetPoint, ++ targetRectangle, ++ targetOuterRectangle ++ }; ++}; ++const collectArrowLineUpdatedRefsByGeometry = (board, element, refs) => { ++ const lines = findElements(board, { ++ match: (element) => { ++ if (PlaitDrawElement.isArrowLine(element)) { ++ return element.source.boundId === element.id || element.target.boundId === element.id; ++ } ++ return false; ++ }, ++ recursion: element => true ++ }); ++ if (lines.length) { ++ lines.forEach(line => { ++ const isSourceBound = line.source.boundId === element.id; ++ const handle = isSourceBound ? 'source' : 'target'; ++ const object = { ...line[handle] }; ++ const linePoints = getArrowLinePoints(board, line); ++ const point = isSourceBound ? linePoints[0] : linePoints[linePoints.length - 1]; ++ object.connection = getHitConnection(board, point, element); ++ const path = PlaitBoard.findPath(board, line); ++ const index = refs.findIndex(obj => Path.equals(obj.path, path)); ++ if (index === -1) { ++ refs.push({ ++ property: { ++ [handle]: object ++ }, ++ path ++ }); ++ } ++ else { ++ refs[index].property = { ...refs[index].property, [handle]: object }; ++ } ++ }); ++ } ++}; ++ ++var ArrowLineMarkerType; ++(function (ArrowLineMarkerType) { ++ ArrowLineMarkerType["arrow"] = "arrow"; ++ ArrowLineMarkerType["none"] = "none"; ++ ArrowLineMarkerType["openTriangle"] = "open-triangle"; ++ ArrowLineMarkerType["solidTriangle"] = "solid-triangle"; ++ ArrowLineMarkerType["sharpArrow"] = "sharp-arrow"; ++ ArrowLineMarkerType["oneSideUp"] = "one-side-up"; ++ ArrowLineMarkerType["oneSideDown"] = "one-side-down"; ++ ArrowLineMarkerType["hollowTriangle"] = "hollow-triangle"; ++ ArrowLineMarkerType["singleSlash"] = "single-slash"; ++})(ArrowLineMarkerType || (ArrowLineMarkerType = {})); ++var ArrowLineShape; ++(function (ArrowLineShape) { ++ ArrowLineShape["straight"] = "straight"; ++ ArrowLineShape["curve"] = "curve"; ++ ArrowLineShape["elbow"] = "elbow"; ++})(ArrowLineShape || (ArrowLineShape = {})); ++var ArrowLineHandleKey; ++(function (ArrowLineHandleKey) { ++ ArrowLineHandleKey["source"] = "source"; ++ ArrowLineHandleKey["target"] = "target"; ++})(ArrowLineHandleKey || (ArrowLineHandleKey = {})); ++const PlaitArrowLine = { ++ isSourceMarkOrTargetMark(line, markType, handleKey) { ++ if (handleKey === ArrowLineHandleKey.source) { ++ return line.source.marker === markType; ++ } ++ else { ++ return line.target.marker === markType; ++ } ++ }, ++ isSourceMark(line, markType) { ++ return PlaitArrowLine.isSourceMarkOrTargetMark(line, markType, ArrowLineHandleKey.source); ++ }, ++ isTargetMark(line, markType) { ++ return PlaitArrowLine.isSourceMarkOrTargetMark(line, markType, ArrowLineHandleKey.target); ++ }, ++ isBoundElementOfSource(line, element) { ++ return line.source.boundId === element.id; ++ }, ++ isBoundElementOfTarget(line, element) { ++ return line.target.boundId === element.id; ++ }, ++ getPoints(board, line) { ++ let sourcePoint; ++ if (line.source.boundId) { ++ const sourceElement = getElementById(board, line.source.boundId); ++ const sourceConnectionPoint = getConnectionPoint(sourceElement, line.source.connection); ++ sourcePoint = rotatePointsByElement(sourceConnectionPoint, sourceElement) || sourceConnectionPoint; ++ } ++ else { ++ sourcePoint = line.points[0]; ++ } ++ let targetPoint; ++ if (line.target.boundId) { ++ const targetElement = getElementById(board, line.target.boundId); ++ const targetConnectionPoint = getConnectionPoint(targetElement, line.target.connection); ++ targetPoint = rotatePointsByElement(targetConnectionPoint, targetElement) || targetConnectionPoint; ++ } ++ else { ++ targetPoint = line.points[line.points.length - 1]; ++ } ++ const restPoints = line.points.length > 2 ? line.points.slice(1, line.points.length - 1) : []; ++ return [sourcePoint, ...restPoints, targetPoint]; ++ } ++}; ++ ++var MemorizeKey; ++(function (MemorizeKey) { ++ MemorizeKey["basicShape"] = "basicShape"; ++ MemorizeKey["flowchart"] = "flowchart"; ++ MemorizeKey["text"] = "text"; ++ MemorizeKey["arrowLine"] = "arrow-line"; ++ MemorizeKey["UML"] = "UML"; ++})(MemorizeKey || (MemorizeKey = {})); ++ ++var VectorLinePointerType; ++(function (VectorLinePointerType) { ++ VectorLinePointerType["vectorLine"] = "vectorLine"; ++})(VectorLinePointerType || (VectorLinePointerType = {})); ++var VectorLineShape; ++(function (VectorLineShape) { ++ VectorLineShape["straight"] = "straight"; ++ VectorLineShape["curve"] = "curve"; ++})(VectorLineShape || (VectorLineShape = {})); ++ ++const PlaitDrawElement = { ++ isGeometry: (value) => { ++ return value.type === 'geometry'; ++ }, ++ isArrowLine: (value) => { ++ return value.type === 'arrow-line' || value.type === 'line'; ++ }, ++ isVectorLine: (value) => { ++ return value.type === 'vector-line'; ++ }, ++ isLine: (value) => { ++ return PlaitDrawElement.isArrowLine(value) || PlaitDrawElement.isVectorLine(value); ++ }, ++ isText: (value) => { ++ return value.type === 'geometry' && value.shape === BasicShapes.text; ++ }, ++ isImage: (value) => { ++ return value.type === 'image'; ++ }, ++ isTable: (value) => { ++ return PlaitTableElement.isTable(value); ++ }, ++ isDrawElement: (value) => { ++ if (PlaitDrawElement.isGeometry(value) || ++ PlaitDrawElement.isLine(value) || ++ PlaitDrawElement.isImage(value) || ++ PlaitDrawElement.isTable(value) || ++ PlaitDrawElement.isSwimlane(value)) { ++ return true; ++ } ++ else { ++ return false; ++ } ++ }, ++ isCustomGeometryElement: (board, value) => { ++ const options = board.getPluginOptions(WithDrawPluginKey); ++ const customGeometryTypes = options?.customGeometryTypes || []; ++ if (customGeometryTypes.includes(value.type)) { ++ return true; ++ } ++ else { ++ return false; ++ } ++ }, ++ isShapeElement: (value) => { ++ return (PlaitDrawElement.isImage(value) || ++ PlaitDrawElement.isGeometry(value) || ++ PlaitDrawElement.isTable(value) || ++ PlaitDrawElement.isSwimlane(value)); ++ }, ++ isBasicShape: (value) => { ++ return Object.keys(BasicShapes).includes(value.shape); ++ }, ++ isFlowchart: (value) => { ++ return Object.keys(FlowchartSymbols).includes(value.shape); ++ }, ++ isUML: (value) => { ++ return Object.keys(UMLSymbols).includes(value.shape); ++ }, ++ isSwimlane: (value) => { ++ return value.type === 'swimlane'; ++ }, ++ isVerticalSwimlane: (value) => { ++ return PlaitDrawElement.isSwimlane(value) && value.shape === SwimlaneSymbols.swimlaneVertical; ++ }, ++ isHorizontalSwimlane: (value) => { ++ return PlaitDrawElement.isSwimlane(value) && value.shape === SwimlaneSymbols.swimlaneHorizontal; ++ }, ++ isUMLClassOrInterface: (value) => { ++ return Object.keys(UMLSymbols).includes(value.shape) && [UMLSymbols.class, UMLSymbols.interface].includes(value.shape); ++ }, ++ isGeometryByTable: (value) => { ++ return PlaitDrawElement.isUMLClassOrInterface(value); ++ }, ++ isElementByTable: (value) => { ++ return PlaitDrawElement.isTable(value) || PlaitDrawElement.isSwimlane(value) || PlaitDrawElement.isGeometryByTable(value); ++ } ++}; ++ ++class GeometryComponent extends CommonElementFlavour { ++ constructor() { ++ super(); ++ } ++ initializeGenerator() { ++ this.activeGenerator = createActiveGenerator(this.board, { ++ getStrokeWidth: () => { ++ const selectedElements = getSelectedElements(this.board); ++ if (selectedElements.length === 1 && !isSelectionMoving(this.board)) { ++ return ACTIVE_STROKE_WIDTH; ++ } ++ else { ++ return ACTIVE_STROKE_WIDTH; ++ } ++ }, ++ getStrokeOpacity: () => { ++ const selectedElements = getSelectedElements(this.board); ++ if (selectedElements.length === 1 && !isSelectionMoving(this.board)) { ++ return 1; ++ } ++ else { ++ return 0.5; ++ } ++ }, ++ getRectangle: (element) => { ++ return RectangleClient.getRectangleByPoints(element.points); ++ }, ++ hasResizeHandle: () => { ++ return hasResizeHandle(this.board, this.element); ++ } ++ }); ++ this.lineAutoCompleteGenerator = new ArrowLineAutoCompleteGenerator(this.board); ++ this.shapeGenerator = new GeometryShapeGenerator(this.board); ++ if (isGeometryIncludeText(this.element)) { ++ this.initializeTextManage(); ++ } ++ this.getRef().addGenerator(ArrowLineAutoCompleteGenerator.key, this.lineAutoCompleteGenerator); ++ this.getRef().addGenerator(ActiveGenerator.key, this.activeGenerator); ++ this.getRef().updateActiveSection = () => { ++ this.activeGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected ++ }); ++ this.lineAutoCompleteGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected ++ }); ++ }; ++ } ++ initialize() { ++ super.initialize(); ++ this.initializeGenerator(); ++ this.shapeGenerator.processDrawing(this.element, this.getElementG()); ++ this.activeGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected ++ }); ++ this.lineAutoCompleteGenerator.processDrawing(this.element, PlaitBoard.getElementTopHost(this.board), { ++ selected: this.selected ++ }); ++ this.textGenerator && this.textGenerator.draw(this.getElementG()); ++ } ++ onContextChanged(value, previous) { ++ if (value.element !== previous.element || value.hasThemeChanged) { ++ this.shapeGenerator.processDrawing(this.element, this.getElementG()); ++ this.activeGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { selected: this.selected }); ++ this.lineAutoCompleteGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected ++ }); ++ this.textGenerator && this.updateText(previous.element, value.element); ++ } ++ else { ++ const hasSameSelected = value.selected === previous.selected; ++ if (!hasSameSelected || value.selected) { ++ this.activeGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected ++ }); ++ this.lineAutoCompleteGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected ++ }); ++ } ++ } ++ } ++ updateText(previousElement, currentElement) { ++ if (isMultipleTextGeometry(this.element)) { ++ this.textGenerator.update(this.element, previousElement.texts, currentElement.texts, this.getElementG()); ++ } ++ else { ++ this.textGenerator.update(this.element, previousElement.text, currentElement.text, this.getElementG()); ++ } ++ } ++ initializeTextManage() { ++ const onTextChange = (element, textManageChangeData, text) => { ++ if (textManageChangeData.newText) { ++ if (isMultipleTextGeometry(element)) { ++ DrawTransforms.setDrawTexts(this.board, element, { ++ id: text.id, ++ text: textManageChangeData.newText ++ }); ++ } ++ else { ++ DrawTransforms.setText(this.board, element, textManageChangeData.newText, textManageChangeData.width, textManageChangeData.height); ++ } ++ } ++ else { ++ DrawTransforms.setTextSize(this.board, element, textManageChangeData.width, textManageChangeData.height); ++ } ++ textManageChangeData.operations && memorizeLatestText(element, textManageChangeData.operations); ++ }; ++ if (isMultipleTextGeometry(this.element)) { ++ this.textGenerator = new TextGenerator(this.board, this.element, this.element.texts, { ++ onChange: onTextChange ++ }); ++ } ++ else { ++ this.textGenerator = new SingleTextGenerator(this.board, this.element, this.element.text, { ++ onChange: onTextChange, ++ getMaxWidth: () => { ++ let width = getTextRectangle$1(this.board, this.element).width; ++ const getRectangle = getEngine(this.element.shape).getTextRectangle; ++ if (getRectangle) { ++ width = getRectangle(this.board, this.element).width; ++ } ++ return PlaitDrawElement.isText(this.element) && this.element.autoSize ? GeometryThreshold.defaultTextMaxWidth : width; ++ } ++ }); ++ } ++ this.textGenerator.initialize(); ++ } ++ destroy() { ++ super.destroy(); ++ this.activeGenerator.destroy(); ++ this.lineAutoCompleteGenerator.destroy(); ++ this.textGenerator?.destroy(); ++ } ++} ++ ++const debugKey = 'debug:plait:line-turning'; ++const debugGenerator$1 = createDebugGenerator(debugKey); ++class ArrowLineComponent extends CommonElementFlavour { ++ constructor() { ++ super(); ++ this.boundedElements = {}; ++ } ++ initializeGenerator() { ++ this.shapeGenerator = new ArrowLineShapeGenerator(this.board); ++ this.activeGenerator = new LineActiveGenerator(this.board); ++ this.initializeTextManages(); ++ } ++ initialize() { ++ this.initializeGenerator(); ++ this.shapeGenerator.processDrawing(this.element, this.getElementG()); ++ const linePoints = getArrowLinePoints(this.board, this.element); ++ this.activeGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected, ++ linePoints ++ }); ++ super.initialize(); ++ this.boundedElements = this.getBoundedElements(); ++ this.drawText(); ++ this.getRef().updateActiveSection = () => { ++ const linePoints = getArrowLinePoints(this.board, this.element); ++ this.activeGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected, ++ linePoints ++ }); ++ }; ++ debugGenerator$1.isDebug() && debugGenerator$1.drawCircles(this.board, this.element.points.slice(1, -1), 4, true); ++ } ++ getBoundedElements() { ++ const boundedElements = {}; ++ if (this.element.source.boundId) { ++ const boundElement = getElementById(this.board, this.element.source.boundId); ++ if (boundElement) { ++ boundedElements.source = boundElement; ++ } ++ } ++ if (this.element.target.boundId) { ++ const boundElement = getElementById(this.board, this.element.target.boundId); ++ if (boundElement) { ++ boundedElements.target = boundElement; ++ } ++ } ++ return boundedElements; ++ } ++ onContextChanged(value, previous) { ++ const boundedElements = this.getBoundedElements(); ++ const isBoundedElementsChanged = boundedElements.source !== this.boundedElements.source || boundedElements.target !== this.boundedElements.target; ++ this.boundedElements = boundedElements; ++ const linePoints = getArrowLinePoints(this.board, this.element); ++ if (value.element !== previous.element || value.hasThemeChanged) { ++ this.shapeGenerator.processDrawing(this.element, this.getElementG()); ++ this.activeGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected, ++ linePoints ++ }); ++ this.updateText(previous.element.texts, value.element.texts); ++ this.updateTextRectangle(); ++ } ++ else { ++ const needUpdate = value.selected !== previous.selected || this.activeGenerator.needUpdate(); ++ if (needUpdate || value.selected) { ++ this.activeGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected, ++ linePoints ++ }); ++ } ++ } ++ if (isBoundedElementsChanged) { ++ this.shapeGenerator.processDrawing(this.element, this.getElementG()); ++ this.activeGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected, ++ linePoints ++ }); ++ this.updateTextRectangle(); ++ return; ++ } ++ } ++ initializeTextManages() { ++ if (this.element.texts?.length) { ++ const textManages = []; ++ this.element.texts.forEach((text, index) => { ++ const manage = this.createTextManage(text, index); ++ textManages.push(manage); ++ }); ++ this.getRef().initializeTextManage(textManages); ++ } ++ } ++ drawText() { ++ if (this.element.texts?.length) { ++ this.getRef() ++ .getTextManages() ++ .forEach((manage, index) => { ++ manage.draw(this.element.texts[index].text); ++ this.getElementG().append(manage.g); ++ }); ++ } ++ } ++ createTextManage(text, index) { ++ return new TextManage(this.board, { ++ getRectangle: () => { ++ return getArrowLineTextRectangle(this.board, this.element, index); ++ }, ++ onChange: (textManageChangeData) => { ++ const path = PlaitBoard.findPath(this.board, this.element); ++ const node = PlaitNode.get(this.board, path); ++ const texts = [...node.texts]; ++ // const newWidth = textManageChangeData.width < MIN_TEXT_WIDTH ? MIN_TEXT_WIDTH : textManageChangeData.width; ++ texts.splice(index, 1, { ++ text: textManageChangeData.newText ? textManageChangeData.newText : this.element.texts[index].text, ++ position: this.element.texts[index].position ++ }); ++ DrawTransforms.setArrowLineTexts(this.board, this.element, texts); ++ textManageChangeData.operations && memorizeLatestText(this.element, textManageChangeData.operations); ++ }, ++ getMaxWidth: () => GeometryThreshold.defaultTextMaxWidth, ++ textPlugins: [] ++ }); ++ } ++ updateText(previousTexts, currentTexts) { ++ if (previousTexts === currentTexts) ++ return; ++ const previousTextsLength = previousTexts.length; ++ const currentTextsLength = currentTexts.length; ++ const textManages = this.getRef().getTextManages(); ++ if (currentTextsLength === previousTextsLength) { ++ for (let i = 0; i < previousTextsLength; i++) { ++ if (previousTexts[i].text !== currentTexts[i].text) { ++ textManages[i].updateText(currentTexts[i].text); ++ } ++ } ++ } ++ else { ++ this.getRef().destroyTextManage(); ++ this.initializeTextManages(); ++ this.drawText(); ++ } ++ } ++ updateTextRectangle() { ++ const textManages = this.getRef().getTextManages(); ++ textManages.forEach((manage) => { ++ manage.updateRectangle(); ++ }); ++ } ++ destroy() { ++ super.destroy(); ++ this.activeGenerator.destroy(); ++ this.getRef().destroyTextManage(); ++ } ++} ++ ++class VectorLineComponent extends CommonElementFlavour { ++ constructor() { ++ super(); ++ } ++ initializeGenerator() { ++ this.shapeGenerator = new VectorLineShapeGenerator(this.board); ++ this.activeGenerator = new LineActiveGenerator(this.board); ++ this.getRef().updateActiveSection = () => { ++ const linePoints = getVectorLinePoints(this.board, this.element); ++ this.activeGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected, ++ linePoints ++ }); ++ }; ++ } ++ initialize() { ++ this.initializeGenerator(); ++ this.shapeGenerator.processDrawing(this.element, this.getElementG()); ++ const linePoints = getVectorLinePoints(this.board, this.element); ++ this.activeGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected, ++ linePoints ++ }); ++ super.initialize(); ++ } ++ onContextChanged(value, previous) { ++ const linePoints = getVectorLinePoints(this.board, this.element); ++ if (value.element !== previous.element || value.hasThemeChanged) { ++ this.shapeGenerator.processDrawing(this.element, this.getElementG()); ++ this.activeGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected, ++ linePoints ++ }); ++ } ++ else { ++ const needUpdate = value.selected !== previous.selected || this.activeGenerator.needUpdate() || value.selected; ++ if (needUpdate) { ++ this.activeGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected, ++ linePoints ++ }); ++ } ++ } ++ } ++ destroy() { ++ super.destroy(); ++ this.activeGenerator.destroy(); ++ } ++} ++ ++const withDrawHotkey = (board) => { ++ const { keyDown, dblClick } = board; ++ board.keyDown = (event) => { ++ const selectedElements = getSelectedElements(board); ++ const isSingleSelection = selectedElements.length === 1; ++ const targetElement = selectedElements[0]; ++ if (!PlaitBoard.isReadonly(board) && ++ !isVirtualKey(event) && ++ !isDelete(event) && ++ !isSpaceHotkey(event) && ++ isSingleSelection && ++ PlaitDrawElement.isGeometry(targetElement)) { ++ event.preventDefault(); ++ editText(board, targetElement); ++ return; ++ } ++ keyDown(event); ++ }; ++ board.dblClick = (event) => { ++ event.preventDefault(); ++ if (!PlaitBoard.isReadonly(board)) { ++ const point = toViewBoxPoint(board, toHostPoint(board, event.x, event.y)); ++ const hitElement = getHitElementByPoint(board, point, undefined, false); ++ if (hitElement && PlaitDrawElement.isGeometry(hitElement) && isDrawElementIncludeText(hitElement)) { ++ if (isMultipleTextGeometry(hitElement)) { ++ const hitText = getHitMultipleGeometryText(board, hitElement, point) || ++ hitElement.texts.find((item) => item.id.includes(GeometryCommonTextKeys.content)) || ++ hitElement.texts[0]; ++ editText(board, hitElement, hitText); ++ } ++ else { ++ editText(board, hitElement); ++ } ++ return; ++ } ++ } ++ dblClick(event); ++ }; ++ return board; ++}; ++ ++const isGeometryDndMode = (board) => { ++ const geometryPointers = getGeometryPointers(); ++ const isGeometryPointer = PlaitBoard.isInPointer(board, geometryPointers); ++ const dndMode = isGeometryPointer && isDndMode(board); ++ return dndMode; ++}; ++const isGeometryDrawingMode = (board) => { ++ const geometryPointers = getGeometryPointers(); ++ const isGeometryPointer = PlaitBoard.isInPointer(board, geometryPointers); ++ const drawingMode = isGeometryPointer && isDrawingMode(board); ++ return drawingMode; ++}; ++const withGeometryCreateByDrag = (board) => { ++ const { pointerMove, globalPointerUp, pointerUp } = board; ++ let geometryShapeG = null; ++ let temporaryElement = null; ++ let fakeCreateTextRef = null; ++ board.pointerMove = (event) => { ++ geometryShapeG?.remove(); ++ geometryShapeG = createG(); ++ const geometryPointers = getGeometryPointers(); ++ const isGeometryPointer = PlaitBoard.isInPointer(board, geometryPointers); ++ const dragMode = isGeometryPointer && isDndMode(board); ++ const movingPoint = toViewBoxPoint(board, toHostPoint(board, event.x, event.y)); ++ const pointer = PlaitBoard.getPointer(board); ++ const geometryGenerator = getGeometryGeneratorByShape(board, pointer); ++ if (dragMode) { ++ const memorizedLatest = getMemorizedLatestByPointer(pointer); ++ if (pointer === BasicShapes.text) { ++ const property = getTextShapeProperty(board, getDefaultGeometryText(board), memorizedLatest.textProperties['font-size']); ++ const points = RectangleClient.getPoints(RectangleClient.getRectangleByCenterPoint(movingPoint, property.width, property.height)); ++ temporaryElement = createTextElement(board, points, getDefaultGeometryText(board)); ++ if (!fakeCreateTextRef) { ++ const textManage = new TextManage(board, { ++ getRectangle: () => { ++ return getTextRectangle$1(board, temporaryElement); ++ } ++ }); ++ textManage.draw(temporaryElement.text); ++ fakeCreateTextRef = { ++ g: createG(), ++ textManage ++ }; ++ PlaitBoard.getHost(board).append(fakeCreateTextRef.g); ++ fakeCreateTextRef.g.append(textManage.g); ++ } ++ else { ++ fakeCreateTextRef.textManage.updateRectangle(); ++ fakeCreateTextRef.g.append(fakeCreateTextRef.textManage.g); ++ } ++ } ++ else { ++ const points = getDefaultGeometryPoints(pointer, movingPoint); ++ temporaryElement = createDefaultGeometry(board, points, pointer); ++ geometryGenerator.processDrawing(temporaryElement, geometryShapeG); ++ PlaitBoard.getElementTopHost(board).append(geometryShapeG); ++ } ++ } ++ pointerMove(event); ++ }; ++ board.pointerUp = (event) => { ++ if (isGeometryDndMode(board) && temporaryElement) { ++ return; ++ } ++ pointerUp(event); ++ }; ++ board.globalPointerUp = (event) => { ++ if (isGeometryDndMode(board) && temporaryElement) { ++ insertElement(board, temporaryElement); ++ fakeCreateTextRef?.textManage.destroy(); ++ fakeCreateTextRef?.g.remove(); ++ fakeCreateTextRef = null; ++ } ++ temporaryElement = null; ++ geometryShapeG?.remove(); ++ geometryShapeG = null; ++ globalPointerUp(event); ++ }; ++ return board; ++}; ++const withGeometryCreateByDrawing = (board) => { ++ const { pointerDown, pointerMove, pointerUp, keyDown, keyUp, touchMove } = board; ++ let start = null; ++ let geometryShapeG = null; ++ let temporaryElement = null; ++ let isShift = false; ++ let snapG; ++ board.keyDown = (event) => { ++ isShift = isKeyHotkey('shift', event); ++ keyDown(event); ++ }; ++ board.keyUp = (event) => { ++ isShift = false; ++ keyUp(event); ++ }; ++ board.pointerDown = (event) => { ++ if (!PlaitBoard.isReadonly(board) && isGeometryDrawingMode(board)) { ++ const point = toViewBoxPoint(board, toHostPoint(board, event.x, event.y)); ++ start = point; ++ const pointer = PlaitBoard.getPointer(board); ++ if (pointer === BasicShapes.text) { ++ const memorizedLatest = getMemorizedLatestByPointer(pointer); ++ const property = getTextShapeProperty(board, getDefaultGeometryText(board), memorizedLatest.textProperties['font-size']); ++ const points = RectangleClient.getPoints(RectangleClient.getRectangleByCenterPoint(point, property.width, property.height)); ++ const textElement = createTextElement(board, points, getDefaultGeometryText(board)); ++ insertElement(board, textElement); ++ start = null; ++ } ++ } ++ pointerDown(event); ++ }; ++ board.touchMove = (event) => { ++ if (start && isGeometryDrawingMode(board)) { ++ event.preventDefault(); ++ } ++ touchMove(event); ++ }; ++ board.pointerMove = (event) => { ++ geometryShapeG?.remove(); ++ geometryShapeG = createG(); ++ const movingPoint = toViewBoxPoint(board, toHostPoint(board, event.x, event.y)); ++ const pointer = PlaitBoard.getPointer(board); ++ const geometryGenerator = getGeometryGeneratorByShape(board, pointer); ++ snapG?.remove(); ++ if (start && isGeometryDrawingMode(board)) { ++ let points = normalizeShapePoints([start, movingPoint], isShift); ++ const activeRectangle = RectangleClient.getRectangleByPoints(points); ++ const [x, y] = getUnitVectorByPointAndPoint(start, movingPoint); ++ const resizeSnapRef = getSnapResizingRef(board, [], { ++ resizePoints: points, ++ activeRectangle, ++ directionFactors: [getDirectionFactorByDirectionComponent(x), getDirectionFactorByDirectionComponent(y)], ++ isAspectRatio: isShift, ++ isFromCorner: true, ++ isCreate: true ++ }); ++ snapG = resizeSnapRef.snapG; ++ PlaitBoard.getElementTopHost(board).append(snapG); ++ points = normalizeShapePoints(resizeSnapRef.activePoints, isShift); ++ temporaryElement = createDefaultGeometry(board, points, pointer); ++ geometryGenerator.processDrawing(temporaryElement, geometryShapeG); ++ PlaitBoard.getElementTopHost(board).append(geometryShapeG); ++ } ++ pointerMove(event); ++ }; ++ board.pointerUp = (event) => { ++ if (isGeometryDrawingMode(board) && start) { ++ const targetPoint = toViewBoxPoint(board, toHostPoint(board, event.x, event.y)); ++ const { width, height } = RectangleClient.getRectangleByPoints([start, targetPoint]); ++ if (Math.hypot(width, height) < 8) { ++ const pointer = PlaitBoard.getPointer(board); ++ if (pointer !== BasicShapes.text) { ++ const points = getDefaultGeometryPoints(pointer, targetPoint); ++ temporaryElement = createDefaultGeometry(board, points, pointer); ++ } ++ } ++ if (temporaryElement) { ++ insertElement(board, temporaryElement); ++ } ++ snapG?.remove(); ++ geometryShapeG?.remove(); ++ geometryShapeG = null; ++ start = null; ++ temporaryElement = null; ++ return; ++ } ++ pointerUp(event); ++ }; ++ return board; ++}; ++ ++const withDrawFragment = (baseBoard) => { ++ const board = baseBoard; ++ const { getDeletedFragment, buildFragment, insertFragment } = board; ++ board.getDeletedFragment = (data) => { ++ const drawElements = getSelectedDrawElements(board); ++ if (drawElements.length) { ++ const geometryElements = drawElements.filter(value => PlaitDrawElement.isGeometry(value)); ++ const arrowLineElements = drawElements.filter(value => PlaitDrawElement.isArrowLine(value)); ++ const vectorLineElements = drawElements.filter(value => PlaitDrawElement.isVectorLine(value)); ++ const imageElements = drawElements.filter(value => PlaitDrawElement.isImage(value)); ++ const tableElements = drawElements.filter(value => PlaitDrawElement.isTable(value)); ++ const swimlaneElements = drawElements.filter(value => PlaitDrawElement.isSwimlane(value)); ++ const boundLineElements = [ ++ ...getBoundedArrowLineElements(board, geometryElements), ++ ...getBoundedArrowLineElements(board, imageElements), ++ ...getBoundedArrowLineElements(board, tableElements), ++ ...getBoundedArrowLineElements(board, swimlaneElements) ++ ].filter(line => !arrowLineElements.includes(line)); ++ data.push(...[ ++ ...geometryElements, ++ ...arrowLineElements, ++ ...vectorLineElements, ++ ...imageElements, ++ ...tableElements, ++ ...swimlaneElements, ++ ...boundLineElements.filter(line => !arrowLineElements.includes(line)) ++ ]); ++ } ++ return getDeletedFragment(data); ++ }; ++ board.buildFragment = (clipboardContext, rectangle, operationType, originData) => { ++ const targetDrawElements = getSelectedDrawElements(board, originData); ++ let boundLineElements = []; ++ if (targetDrawElements.length) { ++ if (operationType === WritableClipboardOperationType.cut) { ++ const geometryElements = targetDrawElements.filter(value => PlaitDrawElement.isGeometry(value)); ++ const lineElements = targetDrawElements.filter(value => PlaitDrawElement.isArrowLine(value)); ++ boundLineElements = getBoundedArrowLineElements(board, geometryElements).filter(line => !lineElements.includes(line)); ++ } ++ const selectedElements = [...targetDrawElements, ...boundLineElements]; ++ const elements = buildClipboardData(board, selectedElements, rectangle ? [rectangle.x, rectangle.y] : [0, 0]); ++ const text = getElementsText(selectedElements); ++ const addition = { ++ text, ++ type: WritableClipboardType.elements, ++ elements: elements ++ }; ++ clipboardContext = addOrCreateClipboardContext(clipboardContext, addition); ++ } ++ return buildFragment(clipboardContext, rectangle, operationType, originData); ++ }; ++ board.insertFragment = (clipboardData, targetPoint, operationType) => { ++ const selectedElements = getSelectedElements(board); ++ if (clipboardData?.files?.length) { ++ const acceptImageArray = acceptImageTypes.map(type => 'image/' + type); ++ const canInsertionImage = !getElementOfFocusedImage(board) && !(selectedElements.length === 1 && board.isImageBindingAllowed(selectedElements[0])); ++ if (acceptImageArray.includes(clipboardData.files[0].type) && canInsertionImage) { ++ const imageFile = clipboardData.files[0]; ++ buildImage(board, imageFile, DEFAULT_IMAGE_WIDTH, imageItem => { ++ DrawTransforms.insertImage(board, imageItem, targetPoint); ++ }); ++ return; ++ } ++ } ++ if (clipboardData?.elements?.length) { ++ const drawElements = clipboardData.elements?.filter(value => PlaitDrawElement.isDrawElement(value)); ++ if (clipboardData.elements && clipboardData.elements.length > 0 && drawElements.length > 0) { ++ insertClipboardData(board, drawElements, targetPoint); ++ } ++ } ++ if (clipboardData?.text) { ++ if (!clipboardData.elements || clipboardData.elements.length === 0) { ++ // (* ̄︶ ̄) ++ const insertAsChildren = selectedElements.length === 1 && selectedElements[0].children; ++ const insertAsFreeText = !insertAsChildren; ++ if (insertAsFreeText) { ++ DrawTransforms.insertText(board, targetPoint, clipboardData.text); ++ return; ++ } ++ } ++ } ++ insertFragment(clipboardData, targetPoint, operationType); ++ }; ++ return board; ++}; ++const getBoundedArrowLineElements = (board, plaitShapes) => { ++ const lines = getArrowLines(board); ++ return lines.filter(line => plaitShapes.find(shape => PlaitArrowLine.isBoundElementOfSource(line, shape) || PlaitArrowLine.isBoundElementOfTarget(line, shape))); ++}; ++ ++const withArrowLineCreateByDraw = (board) => { ++ const { pointerDown, pointerMove, globalPointerUp, touchStart } = board; ++ let start = null; ++ let sourceElement; ++ let lineShapeG = null; ++ let temporaryElement = null; ++ board.touchStart = (event) => { ++ const linePointers = getArrowLinePointers(); ++ const isLinePointer = PlaitBoard.isInPointer(board, linePointers); ++ if (!PlaitBoard.isReadonly(board) && isLinePointer && isDrawingMode(board)) { ++ return event.preventDefault(); ++ } ++ touchStart(event); ++ }; ++ board.pointerDown = (event) => { ++ const linePointers = getArrowLinePointers(); ++ const isLinePointer = PlaitBoard.isInPointer(board, linePointers); ++ if (!PlaitBoard.isReadonly(board) && isLinePointer && isDrawingMode(board)) { ++ const point = toViewBoxPoint(board, toHostPoint(board, event.x, event.y)); ++ start = point; ++ const hitElement = getSnappingShape(board, point); ++ if (hitElement) { ++ sourceElement = hitElement; ++ const ref = getSnappingRef(board, hitElement, point); ++ start = ref.connectorPoint || ref.edgePoint; ++ } ++ } ++ pointerDown(event); ++ }; ++ board.pointerMove = (event) => { ++ lineShapeG?.remove(); ++ lineShapeG = createG(); ++ let movingPoint = toViewBoxPoint(board, toHostPoint(board, event.x, event.y)); ++ if (start) { ++ const lineShape = PlaitBoard.getPointer(board); ++ temporaryElement = handleArrowLineCreating(board, lineShape, start, movingPoint, sourceElement, lineShapeG); ++ } ++ pointerMove(event); ++ }; ++ board.globalPointerUp = (event) => { ++ if (temporaryElement) { ++ Transforms.insertNode(board, temporaryElement, [board.children.length]); ++ clearSelectedElement(board); ++ addSelectedElement(board, temporaryElement); ++ BoardTransforms.updatePointerType(board, PlaitPointerType.selection); ++ } ++ lineShapeG?.remove(); ++ lineShapeG = null; ++ sourceElement = null; ++ start = null; ++ temporaryElement = null; ++ globalPointerUp(event); ++ }; ++ return board; ++}; ++ ++const debugGenerator = createDebugGenerator('debug:plait:arrow-line-resize'); ++const withArrowLineResize = (board) => { ++ let elbowLineIndex; ++ let elbowLineDeleteCount; ++ let elbowSourcePoint; ++ let elbowTargetPoint; ++ let elbowNextRenderPoints; ++ const options = { ++ key: 'draw-line', ++ canResize: () => { ++ return true; ++ }, ++ hitTest: (point) => { ++ const selectedLineElements = getSelectedArrowLineElements(board); ++ if (selectedLineElements.length > 0) { ++ let result = null; ++ selectedLineElements.forEach((value) => { ++ const handleRef = getHitLineResizeHandleRef(board, value, point); ++ if (handleRef) { ++ result = { ++ element: value, ++ handle: handleRef.handle, ++ handleIndex: handleRef.handleIndex ++ }; ++ } ++ }); ++ return result; ++ } ++ return null; ++ }, ++ beforeResize: (resizeRef) => { ++ if (resizeRef.element.shape === ArrowLineShape.elbow && ++ resizeRef.handle !== LineResizeHandle.source && ++ resizeRef.handle !== LineResizeHandle.target) { ++ const params = getElbowLineRouteOptions(board, resizeRef.element); ++ if (isUseDefaultOrthogonalRoute(resizeRef.element, params)) { ++ return; ++ } ++ const points = [...resizeRef.element.points]; ++ const handleIndex = resizeRef.handleIndex; ++ const pointsOnElbow = getElbowPoints(board, resizeRef.element); ++ elbowSourcePoint = pointsOnElbow[0]; ++ elbowTargetPoint = pointsOnElbow[pointsOnElbow.length - 1]; ++ elbowNextRenderPoints = getNextRenderPoints(board, resizeRef.element, pointsOnElbow); ++ const value = getIndexAndDeleteCountByKeyPoint(board, resizeRef.element, [...points], elbowNextRenderPoints, handleIndex); ++ elbowLineIndex = value.index; ++ elbowLineDeleteCount = value.deleteCount; ++ } ++ }, ++ onResize: (resizeRef, resizeState) => { ++ const drawPoints = getArrowLinePoints(board, resizeRef.element); ++ let points = [...resizeRef.element.points]; ++ points[0] = drawPoints[0]; ++ points[points.length - 1] = drawPoints[drawPoints.length - 1]; ++ let source = { ...resizeRef.element.source }; ++ let target = { ...resizeRef.element.target }; ++ let handleIndex = resizeRef.handleIndex; ++ const hitElement = getSnappingShape(board, resizeState.endPoint); ++ if (resizeRef.handle === LineResizeHandle.source || resizeRef.handle === LineResizeHandle.target) { ++ const handleObject = resizeRef.handle === LineResizeHandle.source ? source : target; ++ if (debugGenerator.isDebug()) { ++ debugGenerator.clear(); ++ debugGenerator.drawCircles(board, points, 3, false); ++ debugGenerator.drawCircles(board, [resizeState.endPoint], 4, false, { fill: 'yellow' }); ++ } ++ points[handleIndex] = resizeState.endPoint; ++ points[handleIndex] = alignPoints(points, points[handleIndex], handleIndex); ++ if (debugGenerator.isDebug()) { ++ debugGenerator.drawCircles(board, [points[handleIndex]], 2, false, { fill: 'green' }); ++ } ++ if (hitElement) { ++ handleObject.connection = getHitConnection(board, points[handleIndex], hitElement); ++ handleObject.boundId = hitElement.id; ++ } ++ else { ++ handleObject.connection = undefined; ++ handleObject.boundId = undefined; ++ } ++ } ++ else { ++ if (resizeRef.element.shape === ArrowLineShape.elbow) { ++ if (elbowNextRenderPoints && elbowSourcePoint && elbowTargetPoint) { ++ const resizedPreviousAndNextPoint = getResizedPreviousAndNextPoint(elbowNextRenderPoints, elbowSourcePoint, elbowTargetPoint, handleIndex); ++ const startKeyPoint = elbowNextRenderPoints[handleIndex]; ++ const endKeyPoint = elbowNextRenderPoints[handleIndex + 1]; ++ const [newStartPoint, newEndPoint] = alignElbowSegment(startKeyPoint, endKeyPoint, resizeState, resizedPreviousAndNextPoint); ++ let midDataPoints = [...points].slice(1, points.length - 1); ++ if (elbowLineIndex !== null && elbowLineDeleteCount !== null) { ++ if (hasIllegalElbowPoint(midDataPoints)) { ++ midDataPoints = [newStartPoint, newEndPoint]; ++ } ++ else { ++ midDataPoints.splice(elbowLineIndex, elbowLineDeleteCount, newStartPoint, newEndPoint); ++ } ++ points = [elbowSourcePoint, ...midDataPoints, elbowTargetPoint]; ++ } ++ } ++ } ++ else { ++ if (resizeRef.handle === LineResizeHandle.addHandle) { ++ points.splice(handleIndex + 1, 0, resizeState.endPoint); ++ } ++ else { ++ points[handleIndex] = resizeState.endPoint; ++ } ++ } ++ if (resizeRef.element.shape !== ArrowLineShape.elbow || ++ (resizeRef.element.shape === ArrowLineShape.elbow && points.length === 2)) { ++ points[handleIndex] = alignPoints(points, points[handleIndex], handleIndex); ++ } ++ } ++ DrawTransforms.resizeArrowLine(board, { points, source, target }, resizeRef.path); ++ }, ++ afterResize: (resizeRef) => { ++ if (resizeRef.element.shape === ArrowLineShape.elbow) { ++ const element = PlaitNode.get(board, resizeRef.path); ++ let points = element && [...element.points]; ++ if (points.length > 2 && elbowNextRenderPoints && elbowSourcePoint && elbowTargetPoint) { ++ const nextSourcePoint = elbowNextRenderPoints[0]; ++ const nextTargetPoint = elbowNextRenderPoints[elbowNextRenderPoints.length - 1]; ++ points.splice(0, 1, nextSourcePoint); ++ points.splice(-1, 1, nextTargetPoint); ++ points = simplifyOrthogonalPoints(points); ++ if (Point.isEquals(points[0], nextSourcePoint)) { ++ points.splice(0, 1); ++ } ++ if (Point.isEquals(points[points.length - 1], nextTargetPoint)) { ++ points.pop(); ++ } ++ if (points.length === 1) { ++ points = []; ++ } ++ points = [elbowSourcePoint, ...points, elbowTargetPoint]; ++ DrawTransforms.resizeArrowLine(board, { points }, resizeRef.path); ++ } ++ } ++ elbowLineIndex = null; ++ elbowLineDeleteCount = null; ++ elbowSourcePoint = null; ++ elbowTargetPoint = null; ++ elbowNextRenderPoints = null; ++ } ++ }; ++ withResize(board, options); ++ return board; ++}; ++ ++/* ++Purpose: Visual feedback for snapping/binding arrow-line endpoints to shapes. ++- Active when using arrow-line pointers or resizing source/target handles. ++- Detects target shape under cursor and whether edge/connector snapping applies. ++- Draws bound outline/mask and a connector marker at the snap point. ++- Applies rotation to reaction graphics for angled (rotated) shapes. ++Lifecycle: pointerMove → detect hit/snapping → render reaction → pointerUp cleanup. ++*/ ++const withArrowLineBoundReaction = (board) => { ++ const { pointerMove, pointerUp } = board; ++ let boundShapeG = null; ++ board.pointerMove = (event) => { ++ boundShapeG?.remove(); ++ if (PlaitBoard.isReadonly(board)) { ++ pointerMove(event); ++ return; ++ } ++ const linePointers = Object.keys(ArrowLineShape); ++ const isLinePointer = PlaitBoard.isInPointer(board, linePointers); ++ const movingPoint = toViewBoxPoint(board, toHostPoint(board, event.x, event.y)); ++ const isLineResizing = isResizingByCondition(board, (resizeRef) => { ++ const { element, handle } = resizeRef; ++ const isSourceOrTarget = handle === LineResizeHandle.target || handle === LineResizeHandle.source; ++ return PlaitDrawElement.isArrowLine(element) && isSourceOrTarget; ++ }); ++ if (isLinePointer || isLineResizing) { ++ const hitElement = getHitShape(board, movingPoint); ++ if (hitElement) { ++ const ref = getSnappingRef(board, hitElement, movingPoint); ++ const isSnapping = ref.isHitEdge || ref.isHitConnector; ++ boundShapeG = drawBoundReaction(board, hitElement, { hasMask: isSnapping, hasConnector: true }); ++ if (isSnapping) { ++ const circleG = drawCircle(PlaitBoard.getRoughSVG(board), ref.connectorPoint || ref.edgePoint, 6, { ++ stroke: SELECTION_BORDER_COLOR, ++ strokeWidth: SNAPPING_STROKE_WIDTH, ++ fill: SELECTION_BORDER_COLOR, ++ fillStyle: 'solid' ++ }); ++ boundShapeG.appendChild(circleG); ++ } ++ if (hasValidAngle(hitElement)) { ++ setAngleForG(boundShapeG, RectangleClient.getCenterPointByPoints(hitElement.points), hitElement.angle); ++ } ++ PlaitBoard.getElementTopHost(board).append(boundShapeG); ++ } ++ } ++ pointerMove(event); ++ }; ++ board.pointerUp = (event) => { ++ boundShapeG?.remove(); ++ boundShapeG = null; ++ pointerUp(event); ++ }; ++ return board; ++}; ++ ++const getDefaultLineText = (board) => { ++ return getI18nValue(board, DrawI18nKey.lineText, LINE_TEXT); ++}; ++const withArrowLineText = (board) => { ++ const { dblClick } = board; ++ board.dblClick = (event) => { ++ if (!PlaitBoard.isReadonly(board)) { ++ const clickPoint = toViewBoxPoint(board, toHostPoint(board, event.x, event.y)); ++ const hitTarget = getHitElementByPoint(board, clickPoint); ++ if (hitTarget && PlaitDrawElement.isArrowLine(hitTarget)) { ++ const hitTargetPath = hitTarget && PlaitBoard.findPath(board, hitTarget); ++ const points = getArrowLinePoints(board, hitTarget); ++ const point = getNearestPointBetweenPointAndSegments(clickPoint, points); ++ const texts = hitTarget.texts?.length ? [...hitTarget.texts] : []; ++ const textIndex = getHitArrowLineTextIndex(board, hitTarget, clickPoint); ++ const isHitText = isHitArrowLineText(board, hitTarget, clickPoint); ++ if (isHitText) { ++ editHandle(board, hitTarget, textIndex); ++ } ++ else { ++ const defaultLineText = getDefaultLineText(board); ++ const textMemory = getMemorizedLatest('arrow-line')?.text || {}; ++ const textElement = buildText(defaultLineText, undefined, textMemory); ++ const ratio = getRatioByPoint(points, point); ++ texts.push({ ++ text: textElement, ++ position: ratio ++ }); ++ DrawTransforms.setArrowLineTexts(board, hitTarget, texts); ++ setTimeout(() => { ++ if (hitTargetPath) { ++ const newHitTarget = PlaitNode.get(board, hitTargetPath); ++ const textManages = getTextManages(newHitTarget); ++ editHandle(board, newHitTarget, textManages.length - 1, true); ++ } ++ }); ++ } ++ return; ++ } ++ } ++ dblClick(event); ++ }; ++ return board; ++}; ++function editHandle(board, element, manageIndex, isFirstEdit = false) { ++ const textManages = getTextManages(element); ++ const textManage = textManages[manageIndex]; ++ textManage.edit(() => { ++ const text = Node.string(textManage.getText()); ++ const defaultLineText = getDefaultLineText(board); ++ const shouldRemove = !text || (isFirstEdit && text === defaultLineText); ++ if (shouldRemove) { ++ DrawTransforms.removeArrowLineText(board, element, manageIndex); ++ } ++ }); ++} ++ ++class ImageComponent extends CommonElementFlavour { ++ constructor() { ++ super(); ++ } ++ initializeGenerator() { ++ this.imageGenerator = new ImageGenerator(this.board, { ++ getRectangle: (element) => { ++ return { ++ x: element.points[0][0], ++ y: element.points[0][1], ++ width: element.points[1][0] - element.points[0][0], ++ height: element.points[1][1] - element.points[0][1] ++ }; ++ }, ++ getImageItem: (element) => { ++ return { ++ url: element.url, ++ width: element.points[1][0] - element.points[0][0], ++ height: element.points[1][1] - element.points[0][1] ++ }; ++ } ++ }); ++ this.lineAutoCompleteGenerator = new ArrowLineAutoCompleteGenerator(this.board); ++ this.getRef().addGenerator(ArrowLineAutoCompleteGenerator.key, this.lineAutoCompleteGenerator); ++ this.getRef().updateActiveSection = () => { ++ this.imageGenerator.setFocus(this.element, this.selected); ++ this.lineAutoCompleteGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected ++ }); ++ }; ++ } ++ initialize() { ++ super.initialize(); ++ this.initializeGenerator(); ++ this.imageGenerator.processDrawing(this.element, this.getElementG()); ++ this.lineAutoCompleteGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected ++ }); ++ } ++ onContextChanged(value, previous) { ++ if (value.element !== previous.element) { ++ this.imageGenerator.updateImage(this.getElementG(), previous.element, value.element); ++ this.imageGenerator.setFocus(this.element, this.selected); ++ this.lineAutoCompleteGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected ++ }); ++ } ++ else { ++ const hasSameSelected = value.selected === previous.selected; ++ if (!hasSameSelected || value.selected) { ++ this.imageGenerator.setFocus(this.element, this.selected); ++ this.lineAutoCompleteGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected ++ }); ++ } ++ } ++ } ++ destroy() { ++ super.destroy(); ++ this.imageGenerator.destroy(); ++ this.lineAutoCompleteGenerator.destroy(); ++ } ++} ++ ++const WithArrowLineAutoCompletePluginKey = 'plait-arrow-line-auto-complete-plugin-key'; ++const BOARD_TO_PRE_COMMIT = new WeakMap(); ++/* ++Purpose: Create arrow line via auto-complete drag from a shape edge. ++- On pointerDown at an auto-complete point, set elbow mode and capture source. ++- On drag (beyond buffer), choose nearest crossing on the source edge if available. ++- Rotate source point; build temporary elbow arrow while moving. ++- On release, insert the arrow and select; otherwise commit preview pair if present. ++Lifecycle: pointerDown → pointerMove (create temp) → pointerUp insert/commit → reset. ++*/ ++const withArrowLineAutoComplete = (board) => { ++ const { pointerDown, pointerMove, globalPointerUp, touchMove } = board; ++ let autoCompletePoint = null; ++ let lineShapeG = null; ++ let sourceElement; ++ let temporaryElement; ++ board.pointerDown = (event) => { ++ const selectedElements = getSelectedDrawElements(board); ++ const targetElement = selectedElements.length === 1 && selectedElements[0]; ++ const activePoint = toActivePoint(board, event.x, event.y); ++ if (!PlaitBoard.isReadonly(board) && targetElement && PlaitDrawElement.isShapeElement(targetElement)) { ++ const points = getAutoCompletePoints(board, targetElement, true); ++ const index = getHitIndexOfAutoCompletePoint(rotateAntiPointsByElement(board, activePoint, targetElement, true) || activePoint, points); ++ const hitPoint = points[index]; ++ if (hitPoint) { ++ temporaryDisableSelection(board); ++ const screenPoint = toScreenPointFromActivePoint(board, hitPoint); ++ autoCompletePoint = toViewBoxPoint(board, toHostPoint(board, screenPoint[0], screenPoint[1])); ++ sourceElement = targetElement; ++ BoardTransforms.updatePointerType(board, ArrowLineShape.elbow); ++ } ++ } ++ pointerDown(event); ++ }; ++ board.touchMove = (event) => { ++ if (autoCompletePoint && sourceElement) { ++ return event.preventDefault(); ++ } ++ touchMove(event); ++ }; ++ board.pointerMove = (event) => { ++ lineShapeG?.remove(); ++ lineShapeG = createG(); ++ let movingPoint = toViewBoxPoint(board, toHostPoint(board, event.x, event.y)); ++ if (autoCompletePoint && sourceElement) { ++ const distance = distanceBetweenPointAndPoint(...(rotateAntiPointsByElement(board, movingPoint, sourceElement) || movingPoint), ...autoCompletePoint); ++ if (distance > PRESS_AND_MOVE_BUFFER * 2) { ++ const rectangle = RectangleClient.getRectangleByPoints(sourceElement.points); ++ const shape = getElementShape(sourceElement); ++ const engine = getEngine(shape); ++ let sourcePoint = autoCompletePoint; ++ if (engine.getNearestCrossingPoint) { ++ const crossingPoint = engine.getNearestCrossingPoint(rectangle, autoCompletePoint); ++ sourcePoint = crossingPoint; ++ } ++ // source point must be click point ++ const rotatedSourcePoint = rotatePointsByElement(sourcePoint, sourceElement) || sourcePoint; ++ temporaryElement = handleArrowLineCreating(board, ArrowLineShape.elbow, rotatedSourcePoint, movingPoint, sourceElement, lineShapeG); ++ Transforms.addSelectionWithTemporaryElements(board, []); ++ } ++ } ++ pointerMove(event); ++ }; ++ board.globalPointerUp = (event) => { ++ if (temporaryElement) { ++ Transforms.insertNode(board, temporaryElement, [board.children.length]); ++ clearSelectedElement(board); ++ addSelectedElement(board, temporaryElement); ++ const afterComplete = board.getPluginOptions(WithArrowLineAutoCompletePluginKey)?.afterComplete; ++ afterComplete && afterComplete(temporaryElement); ++ } ++ else { ++ const preCommitRef = BOARD_TO_PRE_COMMIT.get(board); ++ if (preCommitRef) { ++ Transforms.insertNode(board, preCommitRef.temporaryArrowLineElement, [board.children.length]); ++ insertElement(board, preCommitRef.temporaryShapeElement); ++ BOARD_TO_PRE_COMMIT.delete(board); ++ } ++ } ++ if (autoCompletePoint) { ++ BoardTransforms.updatePointerType(board, PlaitPointerType.selection); ++ autoCompletePoint = null; ++ } ++ lineShapeG?.remove(); ++ lineShapeG = null; ++ sourceElement = null; ++ temporaryElement = null; ++ globalPointerUp(event); ++ }; ++ return board; ++}; ++ ++const PREVIEW_ARROW_LINE_DISTANCE = 100; ++/* ++Purpose: Hover-driven auto-complete preview for arrow lines. ++- When hovering a selected shape’s auto-complete point, show a hint circle. ++- Offset a temporary shape and build an elbow arrow from the hit edge. ++- Rotate points by element angle; compute target connection; bind target id. ++- Store temporary arrow/shape for pre-commit; commit on click via auto-complete plugin. ++Lifecycle: pointerMove (hit/preview) → pointerLeave/globalPointerUp cleanup. ++*/ ++const withArrowLineAutoCompleteReaction = (board) => { ++ const { pointerMove, pointerLeave, globalPointerUp } = board; ++ let reactionG = null; ++ let temporaryArrowLineElement = null; ++ let temporaryShapeElement = null; ++ let temporaryArrowLineG = null; ++ let temporaryShapeG = null; ++ board.pointerMove = (event) => { ++ reactionG?.remove(); ++ PlaitBoard.getBoardContainer(board).classList.remove(CursorClass.crosshair); ++ const selectedElements = getSelectedDrawElements(board); ++ temporaryArrowLineG?.remove(); ++ temporaryShapeG?.remove(); ++ const originElement = selectedElements.length === 1 && selectedElements[0]; ++ const activePoint = toActivePoint(board, event.x, event.y); ++ if (!PlaitBoard.isReadonly(board) && !isSelectionMoving(board) && originElement && PlaitDrawElement.isShapeElement(originElement)) { ++ const points = getAutoCompletePoints(board, originElement, true); ++ const hitIndex = getHitIndexOfAutoCompletePoint(rotateAntiPointsByElement(board, activePoint, originElement, true) || activePoint, points); ++ const hitPoint = points[hitIndex]; ++ const ref = PlaitElement.getElementRef(originElement); ++ const lineAutoCompleteGenerator = ref.getGenerator(ArrowLineAutoCompleteGenerator.key); ++ lineAutoCompleteGenerator.recoverAutoCompleteG(); ++ if (hitPoint) { ++ // function 1: dnd ++ reactionG = drawCircle(PlaitBoard.getRoughSVG(board), hitPoint, LINE_AUTO_COMPLETE_HOVERED_DIAMETER, { ++ stroke: 'none', ++ fill: rgbaToHEX(PRIMARY_COLOR, LINE_AUTO_COMPLETE_HOVERED_OPACITY), ++ fillStyle: 'solid' ++ }); ++ PlaitBoard.getActiveHost(board).append(reactionG); ++ PlaitBoard.getBoardContainer(board).classList.add(CursorClass.crosshair); ++ if (hasValidAngle(originElement)) { ++ const rectangle = board.getRectangle(originElement); ++ const activeRectangle = toActiveRectangleFromViewBoxRectangle(board, rectangle); ++ setAngleForG(reactionG, RectangleClient.getCenterPoint(activeRectangle), originElement.angle); ++ } ++ // function 2: hover to preview and click to commit ++ if (PlaitDrawElement.isGeometry(originElement) && !PlaitDrawElement.isText(originElement)) { ++ const originRect = RectangleClient.getRectangleByPoints(originElement.points); ++ let arrowLineStartPoint = RectangleClient.getEdgeCenterPoints(originRect)[hitIndex]; ++ const arrowLineDirection = getDirectionByIndex(hitIndex); ++ let arrowLineEndPoint = moveXOfPoint(arrowLineStartPoint, PREVIEW_ARROW_LINE_DISTANCE, arrowLineDirection); ++ const geometryGenerator = getGeometryGeneratorByShape(board, originElement.shape); ++ const temporaryShapePoints = originElement.points.map((point) => moveXOfPoint(point, PREVIEW_ARROW_LINE_DISTANCE + ++ getXDistanceBetweenPoint(originElement.points[0], originElement.points[1], isHorizontalDirection(arrowLineDirection)), arrowLineDirection)); ++ temporaryArrowLineG = createG(); ++ temporaryShapeG = createG(); ++ temporaryArrowLineG.style.opacity = '0.6'; ++ temporaryShapeG.style.opacity = '0.6'; ++ temporaryShapeElement = createDefaultGeometry(board, temporaryShapePoints, originElement.shape); ++ temporaryShapeElement.angle = originElement.angle; ++ temporaryShapeElement.fill = originElement.fill; ++ temporaryShapeElement.strokeColor = originElement.strokeColor; ++ temporaryShapeElement.strokeStyle = originElement.strokeStyle; ++ temporaryShapeElement.strokeWidth = originElement.strokeWidth; ++ temporaryShapeElement.groupId = originElement.groupId; ++ const rotatedArrowLineStartPoint = rotatePointsByElement(arrowLineStartPoint, originElement) || arrowLineStartPoint; ++ const rotatedArrowLineEndPoint = rotatePointsByElement(arrowLineEndPoint, temporaryShapeElement) || arrowLineEndPoint; ++ temporaryArrowLineElement = handleArrowLineCreating(board, ArrowLineShape.elbow, rotatedArrowLineStartPoint, rotatedArrowLineEndPoint, originElement, temporaryArrowLineG); ++ BOARD_TO_PRE_COMMIT.set(board, { temporaryArrowLineElement, temporaryShapeElement }); ++ const connectionInfo = getHitConnection(board, rotatedArrowLineEndPoint, temporaryShapeElement); ++ temporaryArrowLineElement.target.boundId = temporaryShapeElement.id; ++ temporaryArrowLineElement.target.connection = connectionInfo; ++ geometryGenerator.processDrawing(temporaryShapeElement, temporaryShapeG); ++ PlaitBoard.getElementTopHost(board).append(temporaryShapeG); ++ } ++ return; ++ } ++ } ++ BOARD_TO_PRE_COMMIT.delete(board); ++ pointerMove(event); ++ }; ++ board.pointerLeave = (pointer) => { ++ clearRef(); ++ pointerLeave(pointer); ++ }; ++ const clearRef = () => { ++ if (reactionG) { ++ reactionG?.remove(); ++ PlaitBoard.getBoardContainer(board).classList.remove(CursorClass.crosshair); ++ temporaryArrowLineG?.remove(); ++ temporaryShapeG?.remove(); ++ } ++ if (BOARD_TO_PRE_COMMIT.get(board)) { ++ BOARD_TO_PRE_COMMIT.delete(board); ++ } ++ }; ++ board.globalPointerUp = (event) => { ++ globalPointerUp(event); ++ clearRef(); ++ }; ++ return board; ++}; ++ ++const withArrowLineTextMove = (board) => { ++ let textIndex = 0; ++ const movableBuffer = 100; ++ const options = { ++ key: 'line-text', ++ canResize: () => { ++ return true; ++ }, ++ hitTest: (point) => { ++ let result = null; ++ const line = getHitElementByPoint(board, point, (element) => { ++ return PlaitDrawElement.isArrowLine(element); ++ }); ++ if (line) { ++ const index = getHitArrowLineTextIndex(board, line, point); ++ const textManages = getTextManages(line); ++ const textManage = textManages[index]; ++ if (index !== -1 && !textManage.isEditing) { ++ textIndex = index; ++ return { element: line, handle: ResizeHandle.e }; ++ } ++ } ++ return result; ++ }, ++ onResize: (resizeRef, resizeState) => { ++ const element = resizeRef.element; ++ if (element) { ++ const movingPoint = resizeState.endPoint; ++ const points = getArrowLinePoints(board, element); ++ const distance = distanceBetweenPointAndSegments(movingPoint, points); ++ if (distance <= movableBuffer) { ++ const point = getNearestPointBetweenPointAndSegments(movingPoint, points, false); ++ const position = getRatioByPoint(points, point); ++ const texts = [...element.texts]; ++ texts[textIndex] = { ++ ...texts[textIndex], ++ position ++ }; ++ DrawTransforms.setArrowLineTexts(board, element, texts); ++ } ++ } ++ } ++ }; ++ withResize(board, options); ++ return board; ++}; ++ ++const withDrawRotate = (board) => { ++ const { pointerDown, pointerMove, globalPointerUp, afterChange, drawSelectionRectangle } = board; ++ let rotateRef = null; ++ let rotateHandleG; ++ let needCustomActiveRectangle = false; ++ const canRotate = () => { ++ const elements = getSelectedElements(board); ++ return (elements.length > 0 && ++ elements.every((el) => (PlaitDrawElement.isDrawElement(el) && !PlaitDrawElement.isArrowLine(el)) || ++ PlaitDrawElement.isCustomGeometryElement(board, el))); ++ }; ++ board.pointerDown = (event) => { ++ if (!canRotate() || PlaitBoard.isReadonly(board) || PlaitBoard.hasBeenTextEditing(board) || !isMainPointer(event)) { ++ pointerDown(event); ++ return; ++ } ++ const activePoint = toActivePoint(board, event.x, event.y); ++ const elements = getSelectedElements(board); ++ const rectangle = getRectangleByElements(board, elements, false); ++ const activeRectangle = toActiveRectangleFromViewBoxRectangle(board, rectangle); ++ const handleRectangle = getRotateHandleRectangle(activeRectangle); ++ const angle = getSelectionAngle(elements); ++ const rotatedPoint = angle ? rotatePoints(activePoint, RectangleClient.getCenterPoint(activeRectangle), -angle) : activePoint; ++ if (handleRectangle && RectangleClient.isHit(RectangleClient.getRectangleByPoints([rotatedPoint, rotatedPoint]), handleRectangle)) { ++ rotateRef = { ++ elements: [...elements], ++ startPoint: activePoint ++ }; ++ } ++ pointerDown(event); ++ }; ++ board.pointerMove = (event) => { ++ if (rotateRef) { ++ event.preventDefault(); ++ const isShift = !!event.shiftKey; ++ addRotating(board, rotateRef); ++ const endPoint = toActivePoint(board, event.x, event.y); ++ const rectangle = getRectangleByElements(board, rotateRef.elements, false); ++ const activeRectangle = toActiveRectangleFromViewBoxRectangle(board, rectangle); ++ const selectionCenterPoint = RectangleClient.getCenterPoint(activeRectangle); ++ if (!getSelectionAngle(rotateRef.elements) && rotateRef.elements.length > 1) { ++ needCustomActiveRectangle = true; ++ } ++ throttleRAF(board, 'with-common-rotate', () => { ++ if (rotateRef && rotateRef.startPoint) { ++ let angle = getAngleBetweenPoints(rotateRef.startPoint, endPoint, selectionCenterPoint); ++ const selectionAngle = getSelectionAngle(rotateRef.elements); ++ angle = normalizeAngle(selectionAngle + angle); ++ if (isShift) { ++ angle += Math.PI / 12 / 2; ++ angle -= angle % (Math.PI / 12); ++ } ++ let remainder = angle % (Math.PI / 2); ++ if (Math.PI / 2 - remainder <= degreesToRadians(5)) { ++ const snapAngle = Math.PI / 2 - remainder; ++ angle += snapAngle; ++ } ++ if (remainder <= degreesToRadians(5)) { ++ const snapAngle = -remainder; ++ angle += snapAngle; ++ } ++ rotateRef.angle = normalizeAngle(angle - selectionAngle) || 0; ++ rotateElements(board, rotateRef.elements, rotateRef.angle); ++ MERGING.set(board, true); ++ PlaitBoard.getBoardContainer(board).classList.add('element-rotating'); ++ } ++ }); ++ return; ++ } ++ pointerMove(event); ++ }; ++ board.globalPointerUp = (event) => { ++ globalPointerUp(event); ++ if (needCustomActiveRectangle) { ++ needCustomActiveRectangle = false; ++ const selectedElements = getSelectedElements(board); ++ Transforms.addSelectionWithTemporaryElements(board, selectedElements); ++ } ++ PlaitBoard.getBoardContainer(board).classList.remove('element-rotating'); ++ removeRotating(board); ++ rotateRef = null; ++ MERGING.set(board, false); ++ }; ++ board.afterChange = () => { ++ afterChange(); ++ if (rotateHandleG) { ++ rotateHandleG.remove(); ++ rotateHandleG = null; ++ } ++ if (canRotate() && !isSelectionMoving(board)) { ++ if (needCustomActiveRectangle && rotateRef) { ++ const boundingRectangle = getRectangleByElements(board, rotateRef.elements, false); ++ const boundingActiveRectangle = toActiveRectangleFromViewBoxRectangle(board, boundingRectangle); ++ rotateHandleG = drawRotateHandle(board, boundingActiveRectangle); ++ rotateHandleG.classList.add(ROTATE_HANDLE_CLASS_NAME); ++ if (rotateRef.angle) { ++ setAngleForG(rotateHandleG, RectangleClient.getCenterPoint(boundingActiveRectangle), rotateRef.angle); ++ } ++ } ++ else { ++ const elements = getSelectedElements(board); ++ const boundingRectangle = getRectangleByElements(board, elements, false); ++ const boundingActiveRectangle = toActiveRectangleFromViewBoxRectangle(board, boundingRectangle); ++ rotateHandleG = drawRotateHandle(board, boundingActiveRectangle); ++ rotateHandleG.classList.add(ROTATE_HANDLE_CLASS_NAME); ++ setAngleForG(rotateHandleG, RectangleClient.getCenterPoint(boundingActiveRectangle), getSelectionAngle(elements)); ++ } ++ PlaitBoard.getActiveHost(board).append(rotateHandleG); ++ } ++ }; ++ board.drawSelectionRectangle = () => { ++ if (needCustomActiveRectangle && rotateRef) { ++ const rectangle = getRectangleByElements(board, rotateRef.elements, false); ++ const activeRectangle = toActiveRectangleFromViewBoxRectangle(board, rectangle); ++ const rectangleG = drawRectangle(board, RectangleClient.inflate(activeRectangle, ACTIVE_STROKE_WIDTH), { ++ stroke: SELECTION_BORDER_COLOR, ++ strokeWidth: ACTIVE_STROKE_WIDTH ++ }); ++ rectangleG.classList.add(SELECTION_RECTANGLE_CLASS_NAME); ++ if (rotateRef.angle) { ++ setAngleForG(rectangleG, RectangleClient.getCenterPoint(activeRectangle), rotateRef.angle); ++ } ++ return rectangleG; ++ } ++ return drawSelectionRectangle(); ++ }; ++ return board; ++}; ++ ++class TableComponent extends CommonElementFlavour { ++ constructor() { ++ super(); ++ } ++ initializeGenerator() { ++ this.activeGenerator = createActiveGenerator(this.board, { ++ getStrokeWidth: () => { ++ return ACTIVE_STROKE_WIDTH; ++ }, ++ getStrokeOpacity: () => { ++ return 1; ++ }, ++ getRectangle: (value) => { ++ const cells = getSelectedCells(value); ++ if (cells?.length) { ++ return getCellsRectangle(this.board, this.element, cells); ++ } ++ return RectangleClient.getRectangleByPoints(value.points); ++ }, ++ hasResizeHandle: () => { ++ const cells = getSelectedCells(this.element); ++ if (cells?.length) { ++ return false; ++ } ++ return hasResizeHandle(this.board, this.element); ++ } ++ }); ++ this.tableGenerator = new TableGenerator(this.board); ++ this.initializeTextManage(); ++ this.lineAutoCompleteGenerator = new ArrowLineAutoCompleteGenerator(this.board); ++ this.getRef().addGenerator(ArrowLineAutoCompleteGenerator.key, this.lineAutoCompleteGenerator); ++ this.getRef().updateActiveSection = () => { ++ this.activeGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected ++ }); ++ this.lineAutoCompleteGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected ++ }); ++ }; ++ } ++ initialize() { ++ super.initialize(); ++ this.initializeGenerator(); ++ this.draw(); ++ } ++ draw() { ++ this.tableGenerator.processDrawing(this.element, this.getElementG()); ++ this.textGenerator.draw(this.getElementG()); ++ this.rotateVerticalText(); ++ this.lineAutoCompleteGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected ++ }); ++ } ++ rotateVerticalText() { ++ const table = this.board.buildTable(this.element); ++ table.cells.forEach((item) => { ++ if (PlaitTableElement.isVerticalText(item)) { ++ const textManage = getTextManageByCell(this.board, item); ++ if (textManage) { ++ const engine = getEngine(TableSymbols.table); ++ const rectangle = engine.getTextRectangle(this.board, this.element, { id: item.id, board: this.board }); ++ textManage.g.classList.add('vertical-cell-text'); ++ setAngleForG(textManage.g, RectangleClient.getCenterPoint(rectangle), degreesToRadians(-90)); ++ } ++ } ++ }); ++ } ++ getDrawShapeTexts(cells) { ++ return cells ++ .filter((item) => isCellIncludeText(item)) ++ .map((item) => { ++ return { ++ id: item.id, ++ text: item.text, ++ board: this.board ++ }; ++ }); ++ } ++ initializeTextManage() { ++ const texts = this.getDrawShapeTexts(this.element.cells); ++ this.textGenerator = new TextGenerator(this.board, this.element, texts, { ++ onChange: (value, data, text) => { ++ const path = PlaitBoard.findPath(this.board, value); ++ if (data.newText) { ++ DrawTransforms.setTableText(this.board, path, text.id, data.newText, data.height); ++ } ++ data.operations && memorizeLatestText(value, data.operations); ++ }, ++ getRenderRectangle: (value, text) => { ++ const cell = getCellWithPoints(this.board, value, text.id); ++ if (PlaitTableElement.isVerticalText(cell)) { ++ const cellRectangle = RectangleClient.getRectangleByPoints(cell.points); ++ const strokeWidth = getStrokeWidthByElement(cell); ++ const width = cellRectangle.height - ShapeDefaultSpace.rectangleAndText * 2 - strokeWidth * 2; ++ const height = getCellTextHeight(this.board, cell, true); ++ return { ++ width: height, ++ height: width > 0 ? width : 0, ++ x: cellRectangle.x + ShapeDefaultSpace.rectangleAndText + strokeWidth, ++ y: cellRectangle.y + (cellRectangle.height - width) / 2 ++ }; ++ } ++ else { ++ return getHorizontalTextRectangle(this.board, cell); ++ } ++ } ++ }); ++ this.textGenerator.initialize(); ++ } ++ onContextChanged(value, previous) { ++ if (value.element !== previous.element || value.hasThemeChanged) { ++ const previousSelectedCells = getSelectedCells(previous.element); ++ if (previousSelectedCells?.length) { ++ clearSelectedCells(previous.element); ++ setSelectedCells(value.element, previousSelectedCells); ++ } ++ this.tableGenerator.processDrawing(value.element, this.getElementG()); ++ this.activeGenerator.processDrawing(value.element, PlaitBoard.getActiveHost(this.board), { selected: this.selected }); ++ const previousTexts = this.getDrawShapeTexts(previous.element.cells); ++ const currentTexts = this.getDrawShapeTexts(value.element.cells); ++ this.textGenerator.update(value.element, previousTexts, currentTexts, this.getElementG()); ++ this.rotateVerticalText(); ++ } ++ else { ++ const hasSameSelected = value.selected === previous.selected; ++ const currentSelectedCells = getSelectedCells(value.element); ++ if (!hasSameSelected || currentSelectedCells?.length || value.selected) { ++ this.activeGenerator.processDrawing(value.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected ++ }); ++ } ++ if (!this.selected) { ++ clearSelectedCells(value.element); ++ } ++ } ++ this.lineAutoCompleteGenerator.processDrawing(this.element, PlaitBoard.getActiveHost(this.board), { ++ selected: this.selected ++ }); ++ } ++ destroy() { ++ super.destroy(); ++ this.activeGenerator.destroy(); ++ this.tableGenerator.destroy(); ++ this.textGenerator.destroy(); ++ this.lineAutoCompleteGenerator.destroy(); ++ } ++} ++ ++const MIN_CELL_SIZE = 20; ++function withTableResize(board) { ++ let snapG; ++ const options = { ++ key: 'draw-table', ++ canResize: () => { ++ const selectedElements = getSelectedElements(board); ++ return isSingleSelectTable(board) && !hasValidAngle(selectedElements[0]); ++ }, ++ hitTest: (point) => { ++ const selectedElements = getSelectedElements(board); ++ const hitElement = selectedElements[0]; ++ // debugGenerator.clear(); ++ if (hitElement && PlaitDrawElement.isElementByTable(hitElement)) { ++ let rectangle = board.getRectangle(hitElement); ++ // debugGenerator.drawRectangle(board, rectangle); ++ // debugGenerator.drawCircles(board, [point], 5); ++ let handleRef = getHitRectangleResizeHandleRef(board, rectangle, point, hitElement.angle); ++ if (handleRef) { ++ const selectElement = isSelectedElement(board, hitElement); ++ if ((selectElement && isSingleSelectTable(board)) || (!selectElement && !isCornerHandle(board, handleRef.handle))) { ++ return { ++ element: hitElement, ++ handle: handleRef.handle, ++ cursorClass: handleRef.cursorClass, ++ rectangle ++ }; ++ } ++ } ++ const cells = getCellsWithPoints(board, hitElement); ++ for (let i = 0; i < cells.length; i++) { ++ rectangle = RectangleClient.getRectangleByPoints(cells[i].points); ++ handleRef = getHitRectangleResizeHandleRef(board, rectangle, point, 0); ++ if (handleRef && !isCornerHandle(board, handleRef.handle)) { ++ return { ++ element: hitElement, ++ handle: handleRef.handle, ++ cursorClass: handleRef.cursorClass, ++ rectangle, ++ options: { ++ cell: cells[i] ++ } ++ }; ++ } ++ } ++ } ++ return null; ++ }, ++ onResize: (resizeRef, resizeState) => { ++ snapG?.remove(); ++ const path = PlaitBoard.findPath(board, resizeRef.element); ++ if (resizeRef.options?.cell && resizeRef.rectangle) { ++ const handleIndex = getIndexByResizeHandle(resizeRef.handle); ++ const { originPoint, handlePoint } = getResizeOriginPointAndHandlePoint(board, handleIndex, resizeRef.rectangle); ++ const resizePoints = [resizeState.startPoint, resizeState.endPoint]; ++ const { xZoom, yZoom } = getResizeZoom(resizePoints, originPoint, handlePoint, false, false); ++ const originPoints = resizeRef.options?.cell.points; ++ const targetPoints = originPoints.map((p) => { ++ return movePointByZoomAndOriginPoint(p, originPoint, xZoom, yZoom); ++ }); ++ const offsetX = targetPoints[1][0] - originPoints[1][0]; ++ const offsetY = targetPoints[1][1] - originPoints[1][1]; ++ const width = targetPoints[1][0] - targetPoints[0][0]; ++ const height = targetPoints[1][1] - targetPoints[0][1]; ++ if (offsetX !== 0 && width >= MIN_CELL_SIZE) { ++ const { columns, points } = updateColumns(resizeRef.element, resizeRef.options?.cell.columnId, width, offsetX); ++ Transforms.setNode(board, { columns, points }, path); ++ } ++ else if (offsetY !== 0 && height >= MIN_CELL_SIZE) { ++ const { rows, points } = updateRows(resizeRef.element, resizeRef.options?.cell.rowId, height, offsetY); ++ Transforms.setNode(board, { rows, points }, path); ++ } ++ } ++ else { ++ const isFromCorner = isCornerHandle(board, resizeRef.handle); ++ const isAspectRatio = resizeState.isShift; ++ const handleIndex = getIndexByResizeHandle(resizeRef.handle); ++ const { originPoint, handlePoint } = getResizeOriginPointAndHandlePoint(board, handleIndex, resizeRef.rectangle); ++ const resizeSnapRefOptions = getSnapResizingRefOptions(board, resizeRef, resizeState, { ++ originPoint, ++ handlePoint ++ }, isAspectRatio, isFromCorner); ++ const resizeSnapRef = getSnapResizingRef(board, [resizeRef.element], resizeSnapRefOptions); ++ snapG = resizeSnapRef.snapG; ++ PlaitBoard.getElementTopHost(board).append(snapG); ++ const points = resizeSnapRef.activePoints; ++ const originPoints = resizeRef.element.points; ++ const originRect = RectangleClient.getRectangleByPoints(originPoints); ++ const targetRect = RectangleClient.getRectangleByPoints(points); ++ const offsetWidth = targetRect.width - originRect.width; ++ const offsetHeight = targetRect.height - originRect.height; ++ let columns = [...resizeRef.element.columns]; ++ let rows = [...resizeRef.element.rows]; ++ if (offsetWidth !== 0) { ++ columns = columns.map((item) => { ++ if (item.width) { ++ return { ++ ...item, ++ width: item.width + offsetWidth * (item.width / originRect.width) ++ }; ++ } ++ return item; ++ }); ++ } ++ if (offsetHeight !== 0) { ++ rows = rows.map((item) => { ++ if (item.height) { ++ return { ++ ...item, ++ height: item.height + offsetHeight * (item.height / originRect.height) ++ }; ++ } ++ return item; ++ }); ++ } ++ Transforms.setNode(board, { points: normalizeShapePoints(points), columns, rows }, path); ++ } ++ }, ++ afterResize: (resizeRef) => { ++ snapG?.remove(); ++ snapG = null; ++ } ++ }; ++ withResize(board, options); ++ return board; ++} ++ ++const withTable = (board) => { ++ const tableBoard = board; ++ const { drawElement, getRectangle, isRectangleHit, isHit, isMovable, dblClick, keyDown, pointerUp } = tableBoard; ++ tableBoard.drawElement = (context) => { ++ if (PlaitDrawElement.isElementByTable(context.element)) { ++ return TableComponent; ++ } ++ return drawElement(context); ++ }; ++ tableBoard.isHit = (element, point, isStrict) => { ++ if (PlaitDrawElement.isElementByTable(element)) { ++ const client = RectangleClient.getRectangleByPoints(element.points); ++ const nearestPoint = TableEngine.getNearestPoint(client, point); ++ const distance = distanceBetweenPointAndPoint(nearestPoint[0], nearestPoint[1], point[0], point[1]); ++ return distance <= HIT_DISTANCE_BUFFER || RectangleClient.isPointInRectangle(client, point); ++ } ++ return isHit(element, point, isStrict); ++ }; ++ tableBoard.getRectangle = (element) => { ++ if (PlaitDrawElement.isElementByTable(element)) { ++ return RectangleClient.getRectangleByPoints(element.points); ++ } ++ return getRectangle(element); ++ }; ++ tableBoard.isMovable = (element) => { ++ if (PlaitDrawElement.isElementByTable(element)) { ++ return true; ++ } ++ return isMovable(element); ++ }; ++ tableBoard.isRectangleHit = (element, selection) => { ++ if (PlaitDrawElement.isElementByTable(element)) { ++ const rangeRectangle = RectangleClient.getRectangleByPoints([selection.anchor, selection.focus]); ++ const client = RectangleClient.getRectangleByPoints(element.points); ++ return isLineHitRectangle(RectangleClient.getCornerPoints(client), rangeRectangle); ++ } ++ return isRectangleHit(element, selection); ++ }; ++ tableBoard.keyDown = (event) => { ++ const selectedElements = getSelectedElements(board); ++ const isSingleSelection = selectedElements.length === 1; ++ const targetElement = selectedElements[0]; ++ if (!PlaitBoard.isReadonly(board) && ++ !PlaitBoard.hasBeenTextEditing(tableBoard) && ++ !isVirtualKey(event) && ++ !isDelete(event) && ++ !isSpaceHotkey(event) && ++ isSingleSelection) { ++ event.preventDefault(); ++ if (PlaitDrawElement.isElementByTable(targetElement)) { ++ const cells = getSelectedCells(targetElement); ++ let cell = targetElement.cells.find((item) => item.text); ++ if (cells?.length) { ++ cell = cells.find((item) => item.text); ++ } ++ if (cell) { ++ editCell(board, cell); ++ return; ++ } ++ } ++ } ++ keyDown(event); ++ }; ++ tableBoard.dblClick = (event) => { ++ event.preventDefault(); ++ if (!PlaitBoard.isReadonly(board)) { ++ const point = toViewBoxPoint(board, toHostPoint(board, event.x, event.y)); ++ const hitElement = getHitElementByPoint(board, point); ++ if (hitElement && PlaitDrawElement.isElementByTable(hitElement)) { ++ const hitCell = getHitCell(tableBoard, hitElement, point); ++ if (hitCell && hitCell.text) { ++ editCell(board, hitCell); ++ return; ++ } ++ } ++ } ++ dblClick(event); ++ }; ++ tableBoard.pointerUp = (event) => { ++ const isSetSelectionPointer = PlaitBoard.isPointer(tableBoard, PlaitPointerType.selection) || PlaitBoard.isPointer(tableBoard, PlaitPointerType.hand); ++ const isSkip = !isMainPointer(event) || isDragging(tableBoard) || !isSetSelectionPointer; ++ if (isSkip) { ++ pointerUp(event); ++ return; ++ } ++ if (isSingleSelectTable(tableBoard)) { ++ const point = toViewBoxPoint(tableBoard, toHostPoint(tableBoard, event.x, event.y)); ++ const element = getSelectedTableElements(tableBoard)[0]; ++ const hitCell = getHitCell(tableBoard, element, point); ++ if (hitCell && hitCell.text) { ++ setSelectedCells(element, [hitCell]); ++ } ++ } ++ pointerUp(event); ++ }; ++ tableBoard.buildTable = (element) => { ++ return element; ++ }; ++ return withTableResize(tableBoard); ++}; ++ ++const isSwimlaneDndMode = (board) => { ++ const isSwimlanePointer = isSwimlanePointers(board); ++ const dndMode = isSwimlanePointer && isDndMode(board); ++ return dndMode; ++}; ++const isSwimlaneDrawingMode = (board) => { ++ const isSwimlanePointer = isSwimlanePointers(board); ++ const drawingMode = isSwimlanePointer && isDrawingMode(board); ++ return drawingMode; ++}; ++const withSwimlaneCreateByDrag = (board) => { ++ const { pointerMove, globalPointerUp, pointerUp } = board; ++ let swimlaneG = null; ++ let temporaryElement = null; ++ board.pointerMove = (event) => { ++ swimlaneG?.remove(); ++ swimlaneG = createG(); ++ const tableGenerator = new TableGenerator(board); ++ const pointer = PlaitBoard.getPointer(board); ++ const dragMode = isSwimlaneDndMode(board); ++ const movingPoint = toViewBoxPoint(board, toHostPoint(board, event.x, event.y)); ++ if (dragMode) { ++ const points = getDefaultSwimlanePoints(pointer, movingPoint); ++ temporaryElement = createDefaultSwimlane(pointer, points); ++ tableGenerator.processDrawing(temporaryElement, swimlaneG); ++ PlaitBoard.getElementTopHost(board).append(swimlaneG); ++ } ++ pointerMove(event); ++ }; ++ board.pointerUp = (event) => { ++ if (isSwimlaneDndMode(board) && temporaryElement) { ++ return; ++ } ++ pointerUp(event); ++ }; ++ board.globalPointerUp = (event) => { ++ if (isSwimlaneDndMode(board) && temporaryElement) { ++ insertElement(board, temporaryElement); ++ } ++ temporaryElement = null; ++ swimlaneG?.remove(); ++ swimlaneG = null; ++ globalPointerUp(event); ++ }; ++ return board; ++}; ++const withSwimlaneCreateByDrawing = (board) => { ++ const { pointerDown, pointerMove, pointerUp, keyDown, keyUp } = board; ++ let start = null; ++ let swimlaneG = null; ++ let temporaryElement = null; ++ let isShift = false; ++ let snapG; ++ board.keyDown = (event) => { ++ isShift = isKeyHotkey('shift', event); ++ keyDown(event); ++ }; ++ board.keyUp = (event) => { ++ isShift = false; ++ keyUp(event); ++ }; ++ board.pointerDown = (event) => { ++ if (!PlaitBoard.isReadonly(board) && isSwimlaneDrawingMode(board)) { ++ const point = toViewBoxPoint(board, toHostPoint(board, event.x, event.y)); ++ start = point; ++ } ++ pointerDown(event); ++ }; ++ board.pointerMove = (event) => { ++ swimlaneG?.remove(); ++ swimlaneG = createG(); ++ const tableGenerator = new TableGenerator(board); ++ const movingPoint = toViewBoxPoint(board, toHostPoint(board, event.x, event.y)); ++ const pointer = PlaitBoard.getPointer(board); ++ snapG?.remove(); ++ if (start && isSwimlaneDrawingMode(board)) { ++ let points = normalizeShapePoints([start, movingPoint], isShift); ++ const activeRectangle = RectangleClient.getRectangleByPoints(points); ++ const [x, y] = getUnitVectorByPointAndPoint(start, movingPoint); ++ const resizeSnapRef = getSnapResizingRef(board, [], { ++ resizePoints: points, ++ activeRectangle, ++ directionFactors: [getDirectionFactorByDirectionComponent(x), getDirectionFactorByDirectionComponent(y)], ++ isAspectRatio: isShift, ++ isFromCorner: true, ++ isCreate: true ++ }); ++ snapG = resizeSnapRef.snapG; ++ PlaitBoard.getElementTopHost(board).append(snapG); ++ points = normalizeShapePoints(resizeSnapRef.activePoints, isShift); ++ temporaryElement = createDefaultSwimlane(pointer, points); ++ tableGenerator.processDrawing(temporaryElement, swimlaneG); ++ PlaitBoard.getElementTopHost(board).append(swimlaneG); ++ } ++ pointerMove(event); ++ }; ++ board.pointerUp = (event) => { ++ if (isSwimlaneDrawingMode(board) && start) { ++ const targetPoint = toViewBoxPoint(board, toHostPoint(board, event.x, event.y)); ++ const { width, height } = RectangleClient.getRectangleByPoints([start, targetPoint]); ++ if (Math.hypot(width, height) < 8) { ++ const pointer = PlaitBoard.getPointer(board); ++ const points = getDefaultSwimlanePoints(pointer, targetPoint); ++ temporaryElement = createDefaultSwimlane(pointer, points); ++ } ++ if (temporaryElement) { ++ insertElement(board, temporaryElement); ++ } ++ snapG?.remove(); ++ swimlaneG?.remove(); ++ swimlaneG = null; ++ start = null; ++ temporaryElement = null; ++ return; ++ } ++ pointerUp(event); ++ }; ++ return board; ++}; ++ ++const withSwimlane = (board) => { ++ const { drawElement, buildTable, pointerUp } = board; ++ board.drawElement = (context) => { ++ if (PlaitDrawElement.isSwimlane(context.element)) { ++ return TableComponent; ++ } ++ return drawElement(context); ++ }; ++ board.buildTable = (element) => { ++ if (PlaitDrawElement.isSwimlane(element)) { ++ return buildSwimlaneTable(element); ++ } ++ return buildTable(element); ++ }; ++ return withSwimlaneCreateByDrawing(withSwimlaneCreateByDrag(board)); ++}; ++ ++const withVectorLineCreateByDraw = (board) => { ++ const { pointerDown, pointerMove, dblClick, globalKeyDown } = board; ++ let lineShapeG = null; ++ let temporaryElement = null; ++ let drawPoints = []; ++ const vectorLineComplete = () => { ++ if (temporaryElement) { ++ Transforms.insertNode(board, temporaryElement, [board.children.length]); ++ } ++ PlaitBoard.getBoardContainer(board).classList.remove(`vector-line-closed`); ++ lineShapeG?.remove(); ++ lineShapeG = null; ++ temporaryElement = null; ++ drawPoints = []; ++ }; ++ board.pointerDown = (event) => { ++ const penPointers = getVectorLinePointers(); ++ const isVectorLinePointer = PlaitBoard.isInPointer(board, penPointers); ++ if (!PlaitBoard.isReadonly(board) && isVectorLinePointer && isDrawingMode(board)) { ++ let point = toViewBoxPoint(board, toHostPoint(board, event.x, event.y)); ++ if (drawPoints.length > 1) { ++ const isClosed = distanceBetweenPointAndPoint(...point, ...drawPoints[0]) <= LINE_HIT_GEOMETRY_BUFFER; ++ if (isClosed) { ++ drawPoints.push(drawPoints[0]); ++ vectorLineComplete(); ++ return; ++ } ++ } ++ drawPoints.push(point); ++ return; ++ } ++ pointerDown(event); ++ }; ++ board.pointerMove = (event) => { ++ lineShapeG?.remove(); ++ lineShapeG = createG(); ++ let movingPoint = toViewBoxPoint(board, toHostPoint(board, event.x, event.y)); ++ const pointer = PlaitBoard.getPointer(board); ++ if (pointer === VectorLinePointerType.vectorLine) { ++ if (drawPoints.length > 0) { ++ const distance = distanceBetweenPointAndPoint(...movingPoint, ...drawPoints[0]); ++ if (distance <= LINE_HIT_GEOMETRY_BUFFER) { ++ movingPoint = drawPoints[0]; ++ PlaitBoard.getBoardContainer(board).classList.add(`vector-line-closed`); ++ } ++ else { ++ PlaitBoard.getBoardContainer(board).classList.remove(`vector-line-closed`); ++ } ++ temporaryElement = vectorLineCreating(board, VectorLineShape.straight, drawPoints, movingPoint, lineShapeG); ++ } ++ } ++ pointerMove(event); ++ }; ++ board.dblClick = (event) => { ++ if (!PlaitBoard.isReadonly(board)) { ++ if (temporaryElement) { ++ vectorLineComplete(); ++ BoardTransforms.updatePointerType(board, PlaitPointerType.selection); ++ return; ++ } ++ } ++ dblClick(event); ++ }; ++ board.globalKeyDown = (event) => { ++ if (!PlaitBoard.isReadonly(board)) { ++ const isEsc = isKeyHotkey('esc', event); ++ const isV = isKeyHotkey('v', event); ++ if ((isEsc || isV) && temporaryElement) { ++ vectorLineComplete(); ++ if (isV) { ++ BoardTransforms.updatePointerType(board, PlaitPointerType.selection); ++ } ++ } ++ } ++ globalKeyDown(event); ++ }; ++ return board; ++}; ++ ++const withVectorLineResize = (board) => { ++ const options = { ++ key: 'draw-vector-line', ++ canResize: () => { ++ return true; ++ }, ++ hitTest: (point) => { ++ const selectedVectorLineElements = getSelectedVectorLineElements(board); ++ if (selectedVectorLineElements.length > 0) { ++ let result = null; ++ selectedVectorLineElements.forEach(value => { ++ const handleRef = getHitLineResizeHandleRef(board, value, point); ++ if (handleRef) { ++ result = { ++ element: value, ++ handle: handleRef.handle, ++ handleIndex: handleRef.handleIndex ++ }; ++ } ++ }); ++ return result; ++ } ++ return null; ++ }, ++ onResize: (resizeRef, resizeState) => { ++ let points = [...resizeRef.element.points]; ++ let handleIndex = resizeRef.handleIndex; ++ if (resizeRef.handle === LineResizeHandle.source || resizeRef.handle === LineResizeHandle.target) { ++ points[handleIndex] = resizeState.endPoint; ++ if (isClosedPoints(resizeRef.element.points)) { ++ points[points.length - 1] = resizeState.endPoint; ++ } ++ else { ++ const targetPoint = resizeRef.handle === LineResizeHandle.source ? points[points.length - 1] : points[0]; ++ const distance = distanceBetweenPointAndPoint(...resizeState.endPoint, ...targetPoint); ++ if (distance <= LINE_HIT_GEOMETRY_BUFFER) { ++ points[handleIndex] = targetPoint; ++ } ++ } ++ } ++ else { ++ if (resizeRef.handle === LineResizeHandle.addHandle) { ++ points.splice(handleIndex + 1, 0, resizeState.endPoint); ++ } ++ else { ++ points[handleIndex] = resizeState.endPoint; ++ } ++ } ++ Transforms.setNode(board, { points }, resizeRef.path); ++ } ++ }; ++ withResize(board, options); ++ return board; ++}; ++ ++const withDraw = (board) => { ++ const { drawElement, getRectangle, isRectangleHit, isHit, isInsidePoint, isMovable, isAlign, getRelatedFragment, getOneHitElement } = board; ++ board.drawElement = (context) => { ++ if (PlaitDrawElement.isGeometry(context.element)) { ++ if (PlaitDrawElement.isUML(context.element)) { ++ return GeometryComponent; ++ } ++ return GeometryComponent; ++ } ++ else if (PlaitDrawElement.isArrowLine(context.element)) { ++ return ArrowLineComponent; ++ } ++ else if (PlaitDrawElement.isVectorLine(context.element)) { ++ return VectorLineComponent; ++ } ++ else if (PlaitDrawElement.isImage(context.element)) { ++ return ImageComponent; ++ } ++ return drawElement(context); ++ }; ++ board.getRectangle = (element) => { ++ if (PlaitDrawElement.isGeometry(element)) { ++ return RectangleClient.getRectangleByPoints(element.points); ++ } ++ if (PlaitDrawElement.isArrowLine(element)) { ++ const points = getArrowLinePoints(board, element); ++ const lineTextRectangles = element.texts.map((text, index) => { ++ const rectangle = getArrowLineTextRectangle(board, element, index); ++ return rectangle; ++ }); ++ const linePointsRectangle = RectangleClient.getRectangleByPoints(points); ++ return RectangleClient.getBoundingRectangle([linePointsRectangle, ...lineTextRectangles]); ++ } ++ if (PlaitDrawElement.isVectorLine(element)) { ++ const points = getVectorLinePoints(board, element); ++ const linePointsRectangle = RectangleClient.getRectangleByPoints(points); ++ return RectangleClient.getBoundingRectangle([linePointsRectangle]); ++ } ++ if (PlaitDrawElement.isImage(element)) { ++ return RectangleClient.getRectangleByPoints(element.points); ++ } ++ return getRectangle(element); ++ }; ++ board.isRectangleHit = (element, selection) => { ++ const result = isRectangleHitDrawElement(board, element, selection); ++ if (result !== null) { ++ return result; ++ } ++ return isRectangleHit(element, selection); ++ }; ++ board.isHit = (element, point, isStrict) => { ++ const result = isHitDrawElement(board, element, point, isStrict); ++ if (result !== null) { ++ return result; ++ } ++ return isHit(element, point, isStrict); ++ }; ++ board.getOneHitElement = (elements) => { ++ const isAllDrawElements = elements.every((item) => PlaitDrawElement.isDrawElement(item)); ++ if (isAllDrawElements) { ++ return getHitDrawElement(board, elements); ++ } ++ return getOneHitElement(elements); ++ }; ++ board.isInsidePoint = (element, point) => { ++ const result = isHitElementInside(board, element, point); ++ if (result !== null) { ++ return result; ++ } ++ return isInsidePoint(element, point); ++ }; ++ board.isMovable = (element) => { ++ if (PlaitDrawElement.isGeometry(element)) { ++ return true; ++ } ++ if (PlaitDrawElement.isImage(element)) { ++ return true; ++ } ++ if (PlaitDrawElement.isVectorLine(element)) { ++ return true; ++ } ++ if (PlaitDrawElement.isArrowLine(element)) { ++ const selectedElements = getSelectedElements(board); ++ const isSelected = (boundId) => { ++ return !!selectedElements.find((value) => value.id === boundId); ++ }; ++ if (!element.source.boundId && !element.target.boundId) { ++ return true; ++ } ++ if (element.source.boundId && isSelected(element.source.boundId) && selectedElements.includes(element)) { ++ return true; ++ } ++ if (element.target.boundId && isSelected(element.target.boundId) && selectedElements.includes(element)) { ++ return true; ++ } ++ return false; ++ } ++ return isMovable(element); ++ }; ++ board.isAlign = (element) => { ++ if (PlaitDrawElement.isGeometry(element) || PlaitDrawElement.isImage(element)) { ++ return true; ++ } ++ return isAlign(element); ++ }; ++ board.getRelatedFragment = (elements, originData) => { ++ const selectedElements = originData?.length ? originData : getSelectedElements(board); ++ const lineElements = board.children.filter((element) => PlaitDrawElement.isArrowLine(element)); ++ const activeLines = lineElements.filter((line) => { ++ const source = selectedElements.find((element) => element.id === line.source.boundId); ++ const target = selectedElements.find((element) => element.id === line.target.boundId); ++ const isSelected = selectedElements.includes(line); ++ return source && target && !isSelected; ++ }); ++ return getRelatedFragment([...elements, ...activeLines], originData); ++ }; ++ return withSwimlane(withTable(withDrawResize(withVectorLineCreateByDraw(withArrowLineAutoCompleteReaction(withArrowLineBoundReaction(withVectorLineResize(withArrowLineResize(withArrowLineTextMove(withArrowLineText(withDrawRotate(withArrowLineCreateByDraw(withArrowLineAutoComplete(withGeometryCreateByDrag(withGeometryCreateByDrawing(withDrawFragment(withDrawHotkey(board))))))))))))))))); ++}; ++ ++/** ++ * Generated bundle index. Do not edit. ++ */ ++ ++export { ArrowLineAutoCompleteGenerator, ArrowLineComponent, ArrowLineHandleKey, ArrowLineMarkerType, ArrowLineShape, BOARD_TO_PRE_COMMIT, BasicShapes, DEFAULT_IMAGE_WIDTH, DefaultActivationProperty, DefaultActorProperty, DefaultArrowProperty, DefaultAssemblyProperty, DefaultBasicShapeProperty, DefaultBasicShapePropertyMap, DefaultClassProperty, DefaultCloudProperty, DefaultCombinedFragmentProperty, DefaultComponentBoxProperty, DefaultConnectorProperty, DefaultContainerProperty, DefaultDataBaseProperty, DefaultDataProperty, DefaultDecisionProperty, DefaultDeletionProperty, DefaultDocumentProperty, DefaultDrawActiveStyle, DefaultDrawStyle, DefaultFlowchartProperty, DefaultFlowchartPropertyMap, DefaultInterfaceProperty, DefaultInternalStorageProperty, DefaultLineStyle, DefaultManualInputProperty, DefaultMergeProperty, DefaultMultiDocumentProperty, DefaultNoteProperty, DefaultObjectProperty, DefaultPackageProperty, DefaultPentagonArrowProperty, DefaultPortProperty, DefaultProvidedInterfaceProperty, DefaultRequiredInterfaceProperty, DefaultSwimlaneHorizontalProperty, DefaultSwimlaneHorizontalWithHeaderProperty, DefaultSwimlanePropertyMap, DefaultSwimlaneVerticalProperty, DefaultSwimlaneVerticalWithHeaderProperty, DefaultTextProperty, DefaultTwoWayArrowProperty, DefaultUMLPropertyMap, DrawI18nKey, DrawThemeColors, DrawTransforms, FlowchartSymbols, GEOMETRY_NOT_CLOSED, GEOMETRY_WITHOUT_TEXT, GEOMETRY_WITH_MULTIPLE_TEXT, GeometryCommonTextKeys, GeometryComponent, GeometryShapeGenerator, GeometryThreshold, KEY_TO_TEXT_MANAGE, LINE_ALIGN_TOLERANCE, LINE_AUTO_COMPLETE_DIAMETER, LINE_AUTO_COMPLETE_HOVERED_DIAMETER, LINE_AUTO_COMPLETE_HOVERED_OPACITY, LINE_AUTO_COMPLETE_OPACITY, LINE_HIT_GEOMETRY_BUFFER, LINE_SNAPPING_BUFFER, LINE_SNAPPING_CONNECTOR_BUFFER, LINE_TEXT, LINE_TEXT_SPACE, LineActiveGenerator, MIN_TEXT_WIDTH, MemorizeKey, MultipleTextGeometryTextKeys, PlaitArrowLine, PlaitDrawElement, PlaitGeometry, PlaitTableElement, Q2C, SELECTED_CELLS, SWIMLANE_HEADER_SIZE, ShapeDefaultSpace, SingleTextGenerator, SwimlaneDrawSymbols, SwimlaneSymbols, TableGenerator, TableSymbols, TextGenerator, UMLSymbols, VectorLineComponent, VectorLinePointerType, VectorLineShape, WithArrowLineAutoCompletePluginKey, WithDrawPluginKey, adjustSwimlaneShape, alignElbowSegment, alignPoint, alignPoints, buildClipboardData, buildDefaultTextsByShape, buildSwimlaneTable, clearSelectedCells, collectArrowLineUpdatedRefsByGeometry, createArrowLineElement, createCell, createDefaultCells, createDefaultGeometry, createDefaultRowsOrColumns, createDefaultSwimlane, createGeometryElement, createGeometryElementWithText, createGeometryElementWithoutText, createMultipleTextGeometryElement, createTextElement, createUMLClassOrInterfaceGeometryElement, createVectorLineElement, debugGenerator$2 as debugGenerator, deleteTextManage, drawArrowLine, drawArrowLineArrow, drawBoundReaction, drawGeometry, drawShape, drawVectorLine, editCell, editText, getArrowLineHandleRefPair, getArrowLinePointers, getArrowLinePoints, getArrowLineTextRectangle, getArrowLines, getAutoCompletePoints, getBasicPointers, getCellWithPoints, getCellsRectangle, getCellsWithPoints, getCenterPointsOnPolygon, getConnectionPoint, getCurvePoints, getCustomTextRectangle, getDefaultBasicShapeProperty, getDefaultFlowchartProperty, getDefaultGeometryPoints, getDefaultGeometryProperty, getDefaultGeometryText, getDefaultSwimlanePoints, getDefaultTextPoints, getDefaultUMLProperty, getDrawDefaultStrokeColor, getElbowLineRouteOptions, getElbowPoints, getFillByElement, getFirstFilledDrawElement, getFlowchartDefaultFill, getFlowchartPointers, getGeometryAlign, getGeometryPointers, getHitCell, getHitConnection, getHitConnectionFromConnectionPoint, getHitConnectorPoint, getHitDrawElement, getHitIndexOfAutoCompletePoint, getHitMultipleGeometryText, getHitShape, getIndexAndDeleteCountByKeyPoint, getLineMemorizedLatest, getMemorizeKey, getMemorizedLatestByPointer, getMemorizedLatestShape, getMidKeyPoints, getMiddlePoints, getMirrorDataPoints, getMultipleTextGeometryTextKeys, getNearestPoint, getNextRenderPoints, getNextSourceAndTargetPoints, getResizedPreviousAndNextPoint, getSelectedArrowLineElements, getSelectedCells, getSelectedCustomGeometryElements, getSelectedDrawElements, getSelectedGeometryElements, getSelectedImageElements, getSelectedSwimlane, getSelectedTableCellsEditor, getSelectedTableElements, getSelectedVectorLineElements, getSnapResizingRef, getSnapResizingRefOptions, getSnappingRef, getSnappingShape, getSolidElements, getSourceAndTargetRectangle, getStrokeColorByElement, getStrokeStyleByElement, getStrokeWidthByElement, getSwimlaneCount, getSwimlanePointers, getSwimlaneShapes, getTextKey, getTextManage, getTextManageByCell, getTextRectangle$1 as getTextRectangle, getTextShapeProperty, getUMLPointers, getVectorByConnection, getVectorLinePointers, getVectorLinePoints, handleArrowLineCreating, hasIllegalElbowPoint, insertClipboardData, insertElement, isCellIncludeText, isClosedCustomGeometry, isClosedDrawElement, isClosedPoints, isDrawElementIncludeText, isDrawElementsIncludeText, isEmptyTextElement, isFilledDrawElement, isGeometryClosed, isGeometryIncludeText, isHitArrowLine, isHitArrowLineText, isHitDrawElement, isHitEdgeOfShape, isHitElementInside, isHitElementText, isHitPolyLine, isHitVectorLine, isInsideOfShape, isMultipleTextGeometry, isMultipleTextShape, isRectangleHitDrawElement, isRectangleHitElementText, isRectangleHitRotatedElement, isRectangleHitRotatedPoints, isSelfLoop, isSingleSelectLine, isSingleSelectSwimlane, isSingleSelectTable, isSingleTextGeometry, isSingleTextShape, isSwimlanePointers, isSwimlaneShape, isSwimlaneWithHeader, isUpdatedHandleIndex, isUseDefaultOrthogonalRoute, memorizeLatestShape, memorizeLatestText, setSelectedCells, setTextManage, traverseDrawShapes, updateCellIds, updateCellIdsByRowOrColumn, updateColumns, updateRowOrColumnIds, updateRows, vectorLineCreating, withArrowLineAutoComplete, withDraw }; ++//# sourceMappingURL=plait-draw.mjs.map diff --git a/patches/@plait+draw+0.92.3.patch b/patches/@plait+draw+0.92.3.patch deleted file mode 100644 index 77780e2..0000000 --- a/patches/@plait+draw+0.92.3.patch +++ /dev/null @@ -1,400 +0,0 @@ ---- plait-draw.mjs.backup 2026-02-21 17:17:17 -+++ plait-draw.mjs 2026-02-21 17:26:05 -@@ -1783,7 +1783,7 @@ - .map((command) => `A ${command.rx} ${command.ry} ${command.xAxisRotation} ${command.largeArcFlag} ${command.sweepFlag} ${command.endX} ${command.endY}`) - .join('\n') + - ' Z'; -- const svgElement = rs.path(pathData, { ...options, fillStyle: 'solid' }); -+ const svgElement = rs.path(pathData, options); - setPathStrokeLinecap(svgElement, 'round'); - return svgElement; - }, -@@ -2185,8 +2185,7 @@ - const maskG = drawShape(board, activeRectangle, shape, { - stroke: SELECTION_BORDER_COLOR, - strokeWidth: 0, -- fill: isClosedDrawElement(element) ? SELECTION_FILL_COLOR : DefaultDrawStyle.fill, -- fillStyle: 'solid' -+ fill: isClosedDrawElement(element) ? SELECTION_FILL_COLOR : DefaultDrawStyle.fill - }, drawOptions); - g.appendChild(maskG); - } -@@ -2196,8 +2195,7 @@ - const circleG = drawCircle(PlaitBoard.getRoughSVG(board), point, 8, { - stroke: SELECTION_BORDER_COLOR, - strokeWidth: ACTIVE_STROKE_WIDTH, -- fill: '#FFF', -- fillStyle: 'solid' -+ fill: '#FFF' - }); - g.appendChild(circleG); - }); -@@ -2270,7 +2268,8 @@ - stroke: strokeColor, - strokeWidth, - fill, -- strokeLineDash -+ strokeLineDash, -+ fillStyle: element.fillStyle - }); - } - } -@@ -2480,8 +2479,7 @@ - middlePoints.forEach((point, index) => { - const circle = drawCircle(PlaitBoard.getRoughSVG(this.board), point, LINE_AUTO_COMPLETE_DIAMETER, { - stroke: 'none', -- fill: rgbaToHEX(PRIMARY_COLOR, LINE_AUTO_COMPLETE_OPACITY), -- fillStyle: 'solid' -+ fill: rgbaToHEX(PRIMARY_COLOR, LINE_AUTO_COMPLETE_OPACITY) - }); - circle.classList.add(`line-auto-complete-${index}`); - this.autoCompleteG.appendChild(circle); -@@ -2527,7 +2525,8 @@ - return getEngine(TableSymbols.table).draw(this.board, rectangle, { - strokeWidth, - stroke: strokeColor, -- strokeLineDash -+ strokeLineDash, -+ fillStyle: element.fillStyle - }, { - element: element - }); -@@ -4069,7 +4068,7 @@ - draw(board, rectangle, options) { - const points = getCommentPoints(rectangle); - const rs = PlaitBoard.getRoughSVG(board); -- const polygon = rs.polygon(points, { ...options, fillStyle: 'solid' }); -+ const polygon = rs.polygon(points, options); - setStrokeLinecap(polygon, 'round'); - return polygon; - }, -@@ -4116,7 +4115,7 @@ - draw(board, rectangle, options) { - const points = getPoints(rectangle); - const rs = PlaitBoard.getRoughSVG(board); -- const polygon = rs.polygon(points, { ...options, fillStyle: 'solid' }); -+ const polygon = rs.polygon(points, options); - setStrokeLinecap(polygon, 'round'); - return polygon; - }, -@@ -4206,7 +4205,7 @@ - draw(board, rectangle, options) { - const centerPoint = [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height / 2]; - const rs = PlaitBoard.getRoughSVG(board); -- const shape = rs.ellipse(centerPoint[0], centerPoint[1], rectangle.width, rectangle.height, { ...options, fillStyle: 'solid' }); -+ const shape = rs.ellipse(centerPoint[0], centerPoint[1], rectangle.width, rectangle.height, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -4431,7 +4430,7 @@ - - const RectangleEngine = { - draw(board, rectangle, options) { -- return drawRectangle(board, rectangle, { ...options, fillStyle: 'solid' }); -+ return drawRectangle(board, rectangle, options); - }, - isInsidePoint(rectangle, point) { - const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); -@@ -4455,7 +4454,7 @@ - - const RoundRectangleEngine = { - draw(board, rectangle, options) { -- return drawRoundRectangle(PlaitBoard.getRoughSVG(board), rectangle.x, rectangle.y, rectangle.x + rectangle.width, rectangle.y + rectangle.height, { ...options, fillStyle: 'solid' }, false, getRoundRectangleRadius(rectangle)); -+ return drawRoundRectangle(PlaitBoard.getRoughSVG(board), rectangle.x, rectangle.y, rectangle.x + rectangle.width, rectangle.y + rectangle.height, options, false, getRoundRectangleRadius(rectangle)); - }, - isInsidePoint(rectangle, point) { - return isPointInRoundRectangle(point, rectangle, getRoundRectangleRadius(rectangle)); -@@ -4528,7 +4527,7 @@ - const point9 = [x1 + rectangle.width / 4, y2]; - const point10 = [x1 + rectangle.width / 4, rectangle.y + rectangle.height]; - const point11 = [x1 + rectangle.width / 2, y2]; -- const shape = rs.path(`M${point2[0]} ${point2[1]} A ${radius} ${radius}, 0, 0, 1, ${point3[0]} ${point3[1]} L ${point4[0]} ${point4[1]} A ${radius} ${radius}, 0, 0, 1, ${point5[0]} ${point5[1]} L ${point11[0]} ${point11[1]} ${point10[0]} ${point10[1]} ${point9[0]} ${point9[1]} ${point6[0]} ${point6[1]} A ${radius} ${radius}, 0, 0, 1, ${point7[0]} ${point7[1]} L ${point8[0]} ${point8[1]} A ${radius} ${radius}, 0, 0, 1, ${point1[0]} ${point1[1]} Z`, { ...options, fillStyle: 'solid' }); -+ const shape = rs.path(`M${point2[0]} ${point2[1]} A ${radius} ${radius}, 0, 0, 1, ${point3[0]} ${point3[1]} L ${point4[0]} ${point4[1]} A ${radius} ${radius}, 0, 0, 1, ${point5[0]} ${point5[1]} L ${point11[0]} ${point11[1]} ${point10[0]} ${point10[1]} ${point9[0]} ${point9[1]} ${point6[0]} ${point6[1]} A ${radius} ${radius}, 0, 0, 1, ${point7[0]} ${point7[1]} L ${point8[0]} ${point8[1]} A ${radius} ${radius}, 0, 0, 1, ${point1[0]} ${point1[1]} Z`, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -4680,7 +4679,7 @@ - - const TerminalEngine = { - draw(board, rectangle, options) { -- return drawRoundRectangle(PlaitBoard.getRoughSVG(board), rectangle.x, rectangle.y, rectangle.x + rectangle.width, rectangle.y + rectangle.height, { ...options, fillStyle: 'solid' }, false, getStartEndRadius(rectangle)); -+ return drawRoundRectangle(PlaitBoard.getRoughSVG(board), rectangle.x, rectangle.y, rectangle.x + rectangle.width, rectangle.y + rectangle.height, options, false, getStartEndRadius(rectangle)); - }, - isInsidePoint(rectangle, point) { - return isPointInRoundRectangle(point, rectangle, getStartEndRadius(rectangle)); -@@ -4849,7 +4848,7 @@ - draw(board, rectangle, options) { - const rs = PlaitBoard.getRoughSVG(board); - const shape = rs.path(`M${rectangle.x} ${rectangle.y} L${rectangle.x + (rectangle.width * 3) / 4} ${rectangle.y} A ${rectangle.width / -- 4} ${rectangle.height / 2}, 0, 0, 1,${rectangle.x + (rectangle.width * 3) / 4} ${rectangle.y + rectangle.height} L${rectangle.x} ${rectangle.y + rectangle.height} Z`, { ...options, fillStyle: 'solid' }); -+ 4} ${rectangle.height / 2}, 0, 0, 1,${rectangle.x + (rectangle.width * 3) / 4} ${rectangle.y + rectangle.height} L${rectangle.x} ${rectangle.y + rectangle.height} Z`, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -4890,7 +4889,7 @@ - const StoredDataEngine = { - draw(board, rectangle, options) { - const rs = PlaitBoard.getRoughSVG(board); -- const shape = rs.path(`M${rectangle.x + rectangle.width / 10} ${rectangle.y} L${rectangle.x + rectangle.width} ${rectangle.y} A ${rectangle.width / 10} ${rectangle.height / 2}, 0, 0, 0,${rectangle.x + rectangle.width} ${rectangle.y + rectangle.height} L${rectangle.x + rectangle.width / 10} ${rectangle.y + rectangle.height}A ${rectangle.width / 10} ${rectangle.height / 2}, 0, 0, 1,${rectangle.x + rectangle.width / 10} ${rectangle.y}`, { ...options, fillStyle: 'solid' }); -+ const shape = rs.path(`M${rectangle.x + rectangle.width / 10} ${rectangle.y} L${rectangle.x + rectangle.width} ${rectangle.y} A ${rectangle.width / 10} ${rectangle.height / 2}, 0, 0, 0,${rectangle.x + rectangle.width} ${rectangle.y + rectangle.height} L${rectangle.x + rectangle.width / 10} ${rectangle.y + rectangle.height}A ${rectangle.width / 10} ${rectangle.height / 2}, 0, 0, 1,${rectangle.x + rectangle.width / 10} ${rectangle.y}`, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -4963,7 +4962,7 @@ - const shape = rs.path(`M${rectangle.x} ${rectangle.y} H${rectangle.x + rectangle.width} V${rectangle.y + rectangle.height} H${rectangle.x} Z M${rectangle.x + rectangle.width * 0.06} ${rectangle.y} L${rectangle.x + rectangle.width * 0.06} ${rectangle.y + - rectangle.height} M${rectangle.x + rectangle.width - rectangle.width * 0.06} ${rectangle.y} L${rectangle.x + - rectangle.width - -- rectangle.width * 0.06} ${rectangle.y + rectangle.height}`, { ...options, fillStyle: 'solid' }); -+ rectangle.width * 0.06} ${rectangle.y + rectangle.height}`, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5041,7 +5040,7 @@ - L${rectangle.x + rectangle.width} ${rectangle.y + rectangle.height / 2} - M${rectangle.x + rectangle.width / 2} ${rectangle.y} - L${rectangle.x + rectangle.width / 2} ${rectangle.y + rectangle.height} -- `, { ...options, fillStyle: 'solid' }); -+ `, options); - setStrokeLinecap(shape, 'round'); - return shape; - } -@@ -5062,7 +5061,7 @@ - L${line1Points[1][0]} ${line1Points[1][1]} - M${line2Points[0][0]} ${line2Points[0][1]} - L${line2Points[1][0]} ${line2Points[1][1]} -- `, { ...options, fillStyle: 'solid' }); -+ `, options); - setStrokeLinecap(shape, 'round'); - return shape; - } -@@ -5078,7 +5077,7 @@ - (rectangle.height / 9) * 3}, ${rectangle.x + rectangle.width / 2} ${rectangle.y + - rectangle.height - - rectangle.height / 9} T${rectangle.x} ${rectangle.y + rectangle.height - rectangle.height / 9} -- `, { ...options, fillStyle: 'solid' }); -+ `, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5169,7 +5168,7 @@ - M${rectangle.x + 5} ${rectangle.y + 10} H${rectangle.x + rectangle.width - 10} V${rectangle.y + rectangle.height - rectangle.height / 9} - - M${rectangle.x + 10} ${rectangle.y + 5} H${rectangle.x + rectangle.width - 5} V${rectangle.y + rectangle.height - rectangle.height / 9 - 10 - 3} -- `, { ...options, fillStyle: 'solid' }); -+ `, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5245,7 +5244,7 @@ - A${rectangle.width / 2} ${rectangle.height * 0.15}, 0, 0, 0,${rectangle.x} ${rectangle.y + rectangle.height * 0.15} - V${rectangle.y + rectangle.height - rectangle.height * 0.15} - A${rectangle.width / 2} ${rectangle.height * 0.15}, 0, 0, 0, ${rectangle.x + rectangle.width} ${rectangle.y + rectangle.height - rectangle.height * 0.15} -- V${rectangle.y + rectangle.height * 0.15}`, { ...options, fillStyle: 'solid' }); -+ V${rectangle.y + rectangle.height * 0.15}`, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5322,7 +5321,7 @@ - H${rectangle.x + rectangle.width * 0.15} - A${rectangle.width * 0.15} ${rectangle.height / 2}, 0, 0, 0, ${rectangle.x + rectangle.width * 0.15} ${rectangle.y + - rectangle.height} -- H${rectangle.x + rectangle.width - rectangle.width * 0.15}`, { ...options, fillStyle: 'solid' }); -+ H${rectangle.x + rectangle.width - rectangle.width * 0.15}`, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5398,7 +5397,7 @@ - const shape = rs.path(`M${rectangle.x} ${rectangle.y} h${rectangle.width} v${rectangle.height} h${-rectangle.width} v${-rectangle.height} - M${rectangle.x} ${rectangle.y + rectangle.height / 10} h${rectangle.width} - M${rectangle.x + rectangle.width / 10} ${rectangle.y} v${rectangle.height} -- `, { ...options, fillStyle: 'solid' }); -+ `, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5466,7 +5465,7 @@ - ${lowerCurve.controlPoint2[0]} ${lowerCurve.controlPoint2[1]}, - ${lowerCurve.endPoint[0]} ${lowerCurve.endPoint[1]}` - ].join(' '); -- const shape = rs.path(pathData, { ...options, fillStyle: 'solid', fill: 'transparent' }); -+ const shape = rs.path(pathData, { ...options, fill: 'transparent' }); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5539,7 +5538,7 @@ - ${lowerCurve.controlPoint2[0]} ${lowerCurve.controlPoint2[1]}, - ${lowerCurve.endPoint[0]} ${lowerCurve.endPoint[1]}` - ].join(' '); -- const shape = rs.path(pathData, { ...options, fillStyle: 'solid', fill: 'transparent' }); -+ const shape = rs.path(pathData, { ...options, fill: 'transparent' }); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5592,7 +5591,7 @@ - const rs = PlaitBoard.getRoughSVG(board); - const shape = rs.path(`M${rectangle.x + rectangle.width * 0.075} ${rectangle.y + rectangle.height} H${rectangle.x} V${rectangle.y} H${rectangle.x + - rectangle.width * 0.075} -- `, { ...options, fillStyle: 'solid', fill: 'transparent' }); -+ `, { ...options, fill: 'transparent' }); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5639,7 +5638,7 @@ - H${rectangle.x + rectangle.width * 0.15} - L${rectangle.x} ${rectangle.y + rectangle.height / 2} - Z -- `, { ...options, fillStyle: 'solid' }); -+ `, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5701,7 +5700,7 @@ - y: y + ACTIVE_STROKE_WIDTH, - width: width - ACTIVE_STROKE_WIDTH * 2, - height: height - ACTIVE_STROKE_WIDTH * 2 -- }, { fill: cell.fill, fillStyle: 'solid', strokeWidth: 0 }); -+ }, { fill: cell.fill, fillStyle: roughOptions?.fillStyle, strokeWidth: 0 }); - const cellRightBorder = drawLine(rs, [x + width, y], [x + width, y + height], roughOptions); - const cellBottomBorder = drawLine(rs, [x, y + height], [x + width, y + height], roughOptions); - g.append(cellRectangle, cellRightBorder, cellBottomBorder); -@@ -5846,7 +5845,7 @@ - `M${leftLegLine[0][0]} ${leftLegLine[0][1]} L${leftLegLine[1][0]} ${leftLegLine[1][1]}`, - `M${rightLegLine[0][0]} ${rightLegLine[0][1]} L${rightLegLine[1][0]} ${rightLegLine[1][1]}` - ].join(' '); -- const shape = rs.path(pathData, { ...options, fillStyle: 'solid' }); -+ const shape = rs.path(pathData, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5918,7 +5917,7 @@ - const ContainerEngine = { - draw(board, rectangle, options) { - const rs = PlaitBoard.getRoughSVG(board); -- const shape = rs.path(`M${rectangle.x} ${rectangle.y} H${rectangle.x + rectangle.width} V${rectangle.y + rectangle.height} H${rectangle.x} Z M${rectangle.x + 40} ${rectangle.y} L${rectangle.x + 40} ${rectangle.y + rectangle.height} `, { ...options, fillStyle: 'solid' }); -+ const shape = rs.path(`M${rectangle.x} ${rectangle.y} H${rectangle.x + rectangle.width} V${rectangle.y + rectangle.height} H${rectangle.x} Z M${rectangle.x + 40} ${rectangle.y} L${rectangle.x + 40} ${rectangle.y + rectangle.height} `, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5989,7 +5988,7 @@ - `V${points.leftMiddle[1]}`, - `H${points.middlePoint[0]}` - ].join(' '); -- const shape = rs.path(pathData, { ...options, fillStyle: 'solid' }); -+ const shape = rs.path(pathData, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -6069,7 +6068,7 @@ - H${rectangle.x + rectangle.width / 3 - 8} - L${rectangle.x + rectangle.width / 3} ${rectangle.y + 16} - V${rectangle.y} -- `, { ...options, fillStyle: 'solid' }); -+ `, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -6135,7 +6134,7 @@ - draw(board, rectangle, options) { - const rs = PlaitBoard.getRoughSVG(board); - const lines = getDeletionLines(rectangle); -- const shape = rs.path(lines.map(([from, to]) => `M${from[0]} ${from[1]} L${to[0]} ${to[1]}`).join(' '), { ...options, fillStyle: 'solid', strokeWidth: 4 }); -+ const shape = rs.path(lines.map(([from, to]) => `M${from[0]} ${from[1]} L${to[0]} ${to[1]}`).join(' '), { ...options, strokeWidth: 4 }); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -6176,7 +6175,7 @@ - const shape = rs.path(`M${rectangle.x} ${rectangle.y} H${rectangle.x + rectangle.width} V${rectangle.y + rectangle.height} H${rectangle.x} Z M${rectangle.x + rectangle.width * 0.125} ${rectangle.y} L${rectangle.x + rectangle.width * 0.125} ${rectangle.y + - rectangle.height} M${rectangle.x + rectangle.width - rectangle.width * 0.125} ${rectangle.y} L${rectangle.x + - rectangle.width - -- rectangle.width * 0.125} ${rectangle.y + rectangle.height}`, { ...options, fillStyle: 'solid' }); -+ rectangle.width * 0.125} ${rectangle.y + rectangle.height}`, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -6225,7 +6224,7 @@ - Z - M${rectangle.x + rectangle.width - 16} ${rectangle.y} - A16 16, 0,0,1, ${rectangle.x + rectangle.width} ${rectangle.y + 16} -- `, { ...options, fillStyle: 'solid' }); -+ `, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -6321,10 +6320,7 @@ - // 最后一条线 - `M${line2[0][0]} ${line2[0][1]} H${line2[1][0]}` - ].join(' '); -- const shape = rs.path(pathData, { -- ...options, -- fillStyle: 'solid' -- }); -+ const shape = rs.path(pathData, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -6404,7 +6400,6 @@ - ].join(' '); - const shape = rs.path(pathData, { - ...options, -- fillStyle: 'solid', - fill: 'transparent' - }); - setStrokeLinecap(shape, 'round'); -@@ -6495,10 +6490,7 @@ - `H${line.endX}`, - ...arcCommands.map((command) => `A${command.rx} ${command.ry} ${command.xAxisRotation} ${command.largeArcFlag} ${command.sweepFlag} ${command.endX} ${command.endY}`) - ].join(' '); -- const shape = rs.path(pathData, { -- ...options, -- fillStyle: 'solid' -- }); -+ const shape = rs.path(pathData, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -6586,7 +6578,7 @@ - `M${points.bottomBoxEnd[0]} ${points.bottomBoxEnd[1]}`, - `V${points.mainEnd[1]}` - ].join(' '); -- const shape = rs.path(pathData, { ...options, fillStyle: 'solid' }); -+ const shape = rs.path(pathData, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -6669,7 +6661,7 @@ - V${rectangle.y + rectangle.height} - H${rectangle.x} Z - -- `, { ...options, fillStyle: 'solid' }); -+ `, options); - const componentShape = ComponentEngine.draw(board, componentRectangle, options); - shape.append(componentShape); - setStrokeLinecap(shape, 'round'); -@@ -6714,7 +6706,6 @@ - const rs = PlaitBoard.getRoughSVG(board); - return drawRoundRectangle(rs, rectangle.x, rectangle.y, rectangle.x + rectangle.width, rectangle.y + rectangle.height, { - ...options, -- fillStyle: 'solid', - dashGap: 10, - strokeLineDash: [10, 10] - }, false, 4); -@@ -8014,8 +8005,7 @@ - const circleG = drawCircle(PlaitBoard.getRoughSVG(board), ref.connectorPoint || ref.edgePoint, 6, { - stroke: SELECTION_BORDER_COLOR, - strokeWidth: SNAPPING_STROKE_WIDTH, -- fill: SELECTION_BORDER_COLOR, -- fillStyle: 'solid' -+ fill: SELECTION_BORDER_COLOR - }); - boundShapeG.appendChild(circleG); - } -@@ -8266,8 +8256,7 @@ - // function 1: dnd - reactionG = drawCircle(PlaitBoard.getRoughSVG(board), hitPoint, LINE_AUTO_COMPLETE_HOVERED_DIAMETER, { - stroke: 'none', -- fill: rgbaToHEX(PRIMARY_COLOR, LINE_AUTO_COMPLETE_HOVERED_OPACITY), -- fillStyle: 'solid' -+ fill: rgbaToHEX(PRIMARY_COLOR, LINE_AUTO_COMPLETE_HOVERED_OPACITY) - }); - PlaitBoard.getActiveHost(board).append(reactionG); - PlaitBoard.getBoardContainer(board).classList.add(CursorClass.crosshair); diff --git a/patches/patches/@plait+draw+0.92.3.patch b/patches/patches/@plait+draw+0.92.3.patch deleted file mode 100644 index dc10e6b..0000000 --- a/patches/patches/@plait+draw+0.92.3.patch +++ /dev/null @@ -1,400 +0,0 @@ ---- plait-draw.mjs.backup 2026-02-21 17:17:17 -+++ plait-draw.mjs 2026-02-21 17:26:05 -@@ -1783,7 +1783,7 @@ - .map((command) => `A ${command.rx} ${command.ry} ${command.xAxisRotation} ${command.largeArcFlag} ${command.sweepFlag} ${command.endX} ${command.endY}`) - .join('\n') + - ' Z'; -- const svgElement = rs.path(pathData, { ...options, fillStyle: 'solid' }); -+ const svgElement = rs.path(pathData, options); - setPathStrokeLinecap(svgElement, 'round'); - return svgElement; - }, -@@ -2185,8 +2185,7 @@ - const maskG = drawShape(board, activeRectangle, shape, { - stroke: SELECTION_BORDER_COLOR, - strokeWidth: 0, -- fill: isClosedDrawElement(element) ? SELECTION_FILL_COLOR : DefaultDrawStyle.fill, -- fillStyle: 'solid' -+ fill: isClosedDrawElement(element) ? SELECTION_FILL_COLOR : DefaultDrawStyle.fill - }, drawOptions); - g.appendChild(maskG); - } -@@ -2196,8 +2195,7 @@ - const circleG = drawCircle(PlaitBoard.getRoughSVG(board), point, 8, { - stroke: SELECTION_BORDER_COLOR, - strokeWidth: ACTIVE_STROKE_WIDTH, -- fill: '#FFF', -- fillStyle: 'solid' -+ fill: '#FFF' - }); - g.appendChild(circleG); - }); -@@ -2270,7 +2268,8 @@ - stroke: strokeColor, - strokeWidth, - fill, -- strokeLineDash -+ strokeLineDash, -+ fillStyle: element.fillStyle - }); - } - } -@@ -2480,8 +2479,7 @@ - middlePoints.forEach((point, index) => { - const circle = drawCircle(PlaitBoard.getRoughSVG(this.board), point, LINE_AUTO_COMPLETE_DIAMETER, { - stroke: 'none', -- fill: rgbaToHEX(PRIMARY_COLOR, LINE_AUTO_COMPLETE_OPACITY), -- fillStyle: 'solid' -+ fill: rgbaToHEX(PRIMARY_COLOR, LINE_AUTO_COMPLETE_OPACITY) - }); - circle.classList.add(`line-auto-complete-${index}`); - this.autoCompleteG.appendChild(circle); -@@ -2527,7 +2525,8 @@ - return getEngine(TableSymbols.table).draw(this.board, rectangle, { - strokeWidth, - stroke: strokeColor, -- strokeLineDash -+ strokeLineDash, -+ fillStyle: element.fillStyle - }, { - element: element - }); -@@ -4069,7 +4068,7 @@ - draw(board, rectangle, options) { - const points = getCommentPoints(rectangle); - const rs = PlaitBoard.getRoughSVG(board); -- const polygon = rs.polygon(points, { ...options, fillStyle: 'solid' }); -+ const polygon = rs.polygon(points, options); - setStrokeLinecap(polygon, 'round'); - return polygon; - }, -@@ -4116,7 +4115,7 @@ - draw(board, rectangle, options) { - const points = getPoints(rectangle); - const rs = PlaitBoard.getRoughSVG(board); -- const polygon = rs.polygon(points, { ...options, fillStyle: 'solid' }); -+ const polygon = rs.polygon(points, options); - setStrokeLinecap(polygon, 'round'); - return polygon; - }, -@@ -4206,7 +4205,7 @@ - draw(board, rectangle, options) { - const centerPoint = [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height / 2]; - const rs = PlaitBoard.getRoughSVG(board); -- const shape = rs.ellipse(centerPoint[0], centerPoint[1], rectangle.width, rectangle.height, { ...options, fillStyle: 'solid' }); -+ const shape = rs.ellipse(centerPoint[0], centerPoint[1], rectangle.width, rectangle.height, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -4431,7 +4430,7 @@ - - const RectangleEngine = { - draw(board, rectangle, options) { -- return drawRectangle(board, rectangle, { ...options, fillStyle: 'solid' }); -+ return drawRectangle(board, rectangle, options); - }, - isInsidePoint(rectangle, point) { - const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]); -@@ -4455,7 +4454,7 @@ - - const RoundRectangleEngine = { - draw(board, rectangle, options) { -- return drawRoundRectangle(PlaitBoard.getRoughSVG(board), rectangle.x, rectangle.y, rectangle.x + rectangle.width, rectangle.y + rectangle.height, { ...options, fillStyle: 'solid' }, false, getRoundRectangleRadius(rectangle)); -+ return drawRoundRectangle(PlaitBoard.getRoughSVG(board), rectangle.x, rectangle.y, rectangle.x + rectangle.width, rectangle.y + rectangle.height, options, false, getRoundRectangleRadius(rectangle)); - }, - isInsidePoint(rectangle, point) { - return isPointInRoundRectangle(point, rectangle, getRoundRectangleRadius(rectangle)); -@@ -4528,7 +4527,7 @@ - const point9 = [x1 + rectangle.width / 4, y2]; - const point10 = [x1 + rectangle.width / 4, rectangle.y + rectangle.height]; - const point11 = [x1 + rectangle.width / 2, y2]; -- const shape = rs.path(`M${point2[0]} ${point2[1]} A ${radius} ${radius}, 0, 0, 1, ${point3[0]} ${point3[1]} L ${point4[0]} ${point4[1]} A ${radius} ${radius}, 0, 0, 1, ${point5[0]} ${point5[1]} L ${point11[0]} ${point11[1]} ${point10[0]} ${point10[1]} ${point9[0]} ${point9[1]} ${point6[0]} ${point6[1]} A ${radius} ${radius}, 0, 0, 1, ${point7[0]} ${point7[1]} L ${point8[0]} ${point8[1]} A ${radius} ${radius}, 0, 0, 1, ${point1[0]} ${point1[1]} Z`, { ...options, fillStyle: 'solid' }); -+ const shape = rs.path(`M${point2[0]} ${point2[1]} A ${radius} ${radius}, 0, 0, 1, ${point3[0]} ${point3[1]} L ${point4[0]} ${point4[1]} A ${radius} ${radius}, 0, 0, 1, ${point5[0]} ${point5[1]} L ${point11[0]} ${point11[1]} ${point10[0]} ${point10[1]} ${point9[0]} ${point9[1]} ${point6[0]} ${point6[1]} A ${radius} ${radius}, 0, 0, 1, ${point7[0]} ${point7[1]} L ${point8[0]} ${point8[1]} A ${radius} ${radius}, 0, 0, 1, ${point1[0]} ${point1[1]} Z`, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -4680,7 +4679,7 @@ - - const TerminalEngine = { - draw(board, rectangle, options) { -- return drawRoundRectangle(PlaitBoard.getRoughSVG(board), rectangle.x, rectangle.y, rectangle.x + rectangle.width, rectangle.y + rectangle.height, { ...options, fillStyle: 'solid' }, false, getStartEndRadius(rectangle)); -+ return drawRoundRectangle(PlaitBoard.getRoughSVG(board), rectangle.x, rectangle.y, rectangle.x + rectangle.width, rectangle.y + rectangle.height, options, false, getStartEndRadius(rectangle)); - }, - isInsidePoint(rectangle, point) { - return isPointInRoundRectangle(point, rectangle, getStartEndRadius(rectangle)); -@@ -4849,7 +4848,7 @@ - draw(board, rectangle, options) { - const rs = PlaitBoard.getRoughSVG(board); - const shape = rs.path(`M${rectangle.x} ${rectangle.y} L${rectangle.x + (rectangle.width * 3) / 4} ${rectangle.y} A ${rectangle.width / -- 4} ${rectangle.height / 2}, 0, 0, 1,${rectangle.x + (rectangle.width * 3) / 4} ${rectangle.y + rectangle.height} L${rectangle.x} ${rectangle.y + rectangle.height} Z`, { ...options, fillStyle: 'solid' }); -+ 4} ${rectangle.height / 2}, 0, 0, 1,${rectangle.x + (rectangle.width * 3) / 4} ${rectangle.y + rectangle.height} L${rectangle.x} ${rectangle.y + rectangle.height} Z`, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -4890,7 +4889,7 @@ - const StoredDataEngine = { - draw(board, rectangle, options) { - const rs = PlaitBoard.getRoughSVG(board); -- const shape = rs.path(`M${rectangle.x + rectangle.width / 10} ${rectangle.y} L${rectangle.x + rectangle.width} ${rectangle.y} A ${rectangle.width / 10} ${rectangle.height / 2}, 0, 0, 0,${rectangle.x + rectangle.width} ${rectangle.y + rectangle.height} L${rectangle.x + rectangle.width / 10} ${rectangle.y + rectangle.height}A ${rectangle.width / 10} ${rectangle.height / 2}, 0, 0, 1,${rectangle.x + rectangle.width / 10} ${rectangle.y}`, { ...options, fillStyle: 'solid' }); -+ const shape = rs.path(`M${rectangle.x + rectangle.width / 10} ${rectangle.y} L${rectangle.x + rectangle.width} ${rectangle.y} A ${rectangle.width / 10} ${rectangle.height / 2}, 0, 0, 0,${rectangle.x + rectangle.width} ${rectangle.y + rectangle.height} L${rectangle.x + rectangle.width / 10} ${rectangle.y + rectangle.height}A ${rectangle.width / 10} ${rectangle.height / 2}, 0, 0, 1,${rectangle.x + rectangle.width / 10} ${rectangle.y}`, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -4963,7 +4962,7 @@ - const shape = rs.path(`M${rectangle.x} ${rectangle.y} H${rectangle.x + rectangle.width} V${rectangle.y + rectangle.height} H${rectangle.x} Z M${rectangle.x + rectangle.width * 0.06} ${rectangle.y} L${rectangle.x + rectangle.width * 0.06} ${rectangle.y + - rectangle.height} M${rectangle.x + rectangle.width - rectangle.width * 0.06} ${rectangle.y} L${rectangle.x + - rectangle.width - -- rectangle.width * 0.06} ${rectangle.y + rectangle.height}`, { ...options, fillStyle: 'solid' }); -+ rectangle.width * 0.06} ${rectangle.y + rectangle.height}`, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5041,7 +5040,7 @@ - L${rectangle.x + rectangle.width} ${rectangle.y + rectangle.height / 2} - M${rectangle.x + rectangle.width / 2} ${rectangle.y} - L${rectangle.x + rectangle.width / 2} ${rectangle.y + rectangle.height} -- `, { ...options, fillStyle: 'solid' }); -+ `, options); - setStrokeLinecap(shape, 'round'); - return shape; - } -@@ -5062,7 +5061,7 @@ - L${line1Points[1][0]} ${line1Points[1][1]} - M${line2Points[0][0]} ${line2Points[0][1]} - L${line2Points[1][0]} ${line2Points[1][1]} -- `, { ...options, fillStyle: 'solid' }); -+ `, options); - setStrokeLinecap(shape, 'round'); - return shape; - } -@@ -5078,7 +5077,7 @@ - (rectangle.height / 9) * 3}, ${rectangle.x + rectangle.width / 2} ${rectangle.y + - rectangle.height - - rectangle.height / 9} T${rectangle.x} ${rectangle.y + rectangle.height - rectangle.height / 9} -- `, { ...options, fillStyle: 'solid' }); -+ `, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5169,7 +5168,7 @@ - M${rectangle.x + 5} ${rectangle.y + 10} H${rectangle.x + rectangle.width - 10} V${rectangle.y + rectangle.height - rectangle.height / 9} - - M${rectangle.x + 10} ${rectangle.y + 5} H${rectangle.x + rectangle.width - 5} V${rectangle.y + rectangle.height - rectangle.height / 9 - 10 - 3} -- `, { ...options, fillStyle: 'solid' }); -+ `, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5245,7 +5244,7 @@ - A${rectangle.width / 2} ${rectangle.height * 0.15}, 0, 0, 0,${rectangle.x} ${rectangle.y + rectangle.height * 0.15} - V${rectangle.y + rectangle.height - rectangle.height * 0.15} - A${rectangle.width / 2} ${rectangle.height * 0.15}, 0, 0, 0, ${rectangle.x + rectangle.width} ${rectangle.y + rectangle.height - rectangle.height * 0.15} -- V${rectangle.y + rectangle.height * 0.15}`, { ...options, fillStyle: 'solid' }); -+ V${rectangle.y + rectangle.height * 0.15}`, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5322,7 +5321,7 @@ - H${rectangle.x + rectangle.width * 0.15} - A${rectangle.width * 0.15} ${rectangle.height / 2}, 0, 0, 0, ${rectangle.x + rectangle.width * 0.15} ${rectangle.y + - rectangle.height} -- H${rectangle.x + rectangle.width - rectangle.width * 0.15}`, { ...options, fillStyle: 'solid' }); -+ H${rectangle.x + rectangle.width - rectangle.width * 0.15}`, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5398,7 +5397,7 @@ - const shape = rs.path(`M${rectangle.x} ${rectangle.y} h${rectangle.width} v${rectangle.height} h${-rectangle.width} v${-rectangle.height} - M${rectangle.x} ${rectangle.y + rectangle.height / 10} h${rectangle.width} - M${rectangle.x + rectangle.width / 10} ${rectangle.y} v${rectangle.height} -- `, { ...options, fillStyle: 'solid' }); -+ `, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5466,7 +5465,7 @@ - ${lowerCurve.controlPoint2[0]} ${lowerCurve.controlPoint2[1]}, - ${lowerCurve.endPoint[0]} ${lowerCurve.endPoint[1]}` - ].join(' '); -- const shape = rs.path(pathData, { ...options, fillStyle: 'solid', fill: 'transparent' }); -+ const shape = rs.path(pathData, { ...options, fill: 'transparent' }); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5539,7 +5538,7 @@ - ${lowerCurve.controlPoint2[0]} ${lowerCurve.controlPoint2[1]}, - ${lowerCurve.endPoint[0]} ${lowerCurve.endPoint[1]}` - ].join(' '); -- const shape = rs.path(pathData, { ...options, fillStyle: 'solid', fill: 'transparent' }); -+ const shape = rs.path(pathData, { ...options, fill: 'transparent' }); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5592,7 +5591,7 @@ - const rs = PlaitBoard.getRoughSVG(board); - const shape = rs.path(`M${rectangle.x + rectangle.width * 0.075} ${rectangle.y + rectangle.height} H${rectangle.x} V${rectangle.y} H${rectangle.x + - rectangle.width * 0.075} -- `, { ...options, fillStyle: 'solid', fill: 'transparent' }); -+ `, { ...options, fill: 'transparent' }); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5639,7 +5638,7 @@ - H${rectangle.x + rectangle.width * 0.15} - L${rectangle.x} ${rectangle.y + rectangle.height / 2} - Z -- `, { ...options, fillStyle: 'solid' }); -+ `, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5701,7 +5700,7 @@ - y: y + ACTIVE_STROKE_WIDTH, - width: width - ACTIVE_STROKE_WIDTH * 2, - height: height - ACTIVE_STROKE_WIDTH * 2 -- }, { fill: cell.fill, fillStyle: 'solid', strokeWidth: 0 }); -+ }, { fill: cell.fill, fillStyle: roughOptions?.fillStyle, strokeWidth: 0 }); - const cellRightBorder = drawLine(rs, [x + width, y], [x + width, y + height], roughOptions); - const cellBottomBorder = drawLine(rs, [x, y + height], [x + width, y + height], roughOptions); - g.append(cellRectangle, cellRightBorder, cellBottomBorder); -@@ -5846,7 +5845,7 @@ - `M${leftLegLine[0][0]} ${leftLegLine[0][1]} L${leftLegLine[1][0]} ${leftLegLine[1][1]}`, - `M${rightLegLine[0][0]} ${rightLegLine[0][1]} L${rightLegLine[1][0]} ${rightLegLine[1][1]}` - ].join(' '); -- const shape = rs.path(pathData, { ...options, fillStyle: 'solid' }); -+ const shape = rs.path(pathData, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5918,7 +5917,7 @@ - const ContainerEngine = { - draw(board, rectangle, options) { - const rs = PlaitBoard.getRoughSVG(board); -- const shape = rs.path(`M${rectangle.x} ${rectangle.y} H${rectangle.x + rectangle.width} V${rectangle.y + rectangle.height} H${rectangle.x} Z M${rectangle.x + 40} ${rectangle.y} L${rectangle.x + 40} ${rectangle.y + rectangle.height} `, { ...options, fillStyle: 'solid' }); -+ const shape = rs.path(`M${rectangle.x} ${rectangle.y} H${rectangle.x + rectangle.width} V${rectangle.y + rectangle.height} H${rectangle.x} Z M${rectangle.x + 40} ${rectangle.y} L${rectangle.x + 40} ${rectangle.y + rectangle.height} `, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -5989,7 +5988,7 @@ - `V${points.leftMiddle[1]}`, - `H${points.middlePoint[0]}` - ].join(' '); -- const shape = rs.path(pathData, { ...options, fillStyle: 'solid' }); -+ const shape = rs.path(pathData, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -6069,7 +6068,7 @@ - H${rectangle.x + rectangle.width / 3 - 8} - L${rectangle.x + rectangle.width / 3} ${rectangle.y + 16} - V${rectangle.y} -- `, { ...options, fillStyle: 'solid' }); -+ `, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -6135,7 +6134,7 @@ - draw(board, rectangle, options) { - const rs = PlaitBoard.getRoughSVG(board); - const lines = getDeletionLines(rectangle); -- const shape = rs.path(lines.map(([from, to]) => `M${from[0]} ${from[1]} L${to[0]} ${to[1]}`).join(' '), { ...options, fillStyle: 'solid', strokeWidth: 4 }); -+ const shape = rs.path(lines.map(([from, to]) => `M${from[0]} ${from[1]} L${to[0]} ${to[1]}`).join(' '), { ...options, strokeWidth: 4 }); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -6176,7 +6175,7 @@ - const shape = rs.path(`M${rectangle.x} ${rectangle.y} H${rectangle.x + rectangle.width} V${rectangle.y + rectangle.height} H${rectangle.x} Z M${rectangle.x + rectangle.width * 0.125} ${rectangle.y} L${rectangle.x + rectangle.width * 0.125} ${rectangle.y + - rectangle.height} M${rectangle.x + rectangle.width - rectangle.width * 0.125} ${rectangle.y} L${rectangle.x + - rectangle.width - -- rectangle.width * 0.125} ${rectangle.y + rectangle.height}`, { ...options, fillStyle: 'solid' }); -+ rectangle.width * 0.125} ${rectangle.y + rectangle.height}`, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -6225,7 +6224,7 @@ - Z - M${rectangle.x + rectangle.width - 16} ${rectangle.y} - A16 16, 0,0,1, ${rectangle.x + rectangle.width} ${rectangle.y + 16} -- `, { ...options, fillStyle: 'solid' }); -+ `, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -6321,10 +6320,7 @@ - // 最后一条线 - `M${line2[0][0]} ${line2[0][1]} H${line2[1][0]}` - ].join(' '); -- const shape = rs.path(pathData, { -- ...options, -- fillStyle: 'solid' -- }); -+ const shape = rs.path(pathData, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -6404,7 +6400,6 @@ - ].join(' '); - const shape = rs.path(pathData, { - ...options, -- fillStyle: 'solid', - fill: 'transparent' - }); - setStrokeLinecap(shape, 'round'); -@@ -6495,10 +6490,7 @@ - `H${line.endX}`, - ...arcCommands.map((command) => `A${command.rx} ${command.ry} ${command.xAxisRotation} ${command.largeArcFlag} ${command.sweepFlag} ${command.endX} ${command.endY}`) - ].join(' '); -- const shape = rs.path(pathData, { -- ...options, -- fillStyle: 'solid' -- }); -+ const shape = rs.path(pathData, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -6586,7 +6578,7 @@ - `M${points.bottomBoxEnd[0]} ${points.bottomBoxEnd[1]}`, - `V${points.mainEnd[1]}` - ].join(' '); -- const shape = rs.path(pathData, { ...options, fillStyle: 'solid' }); -+ const shape = rs.path(pathData, options); - setStrokeLinecap(shape, 'round'); - return shape; - }, -@@ -6669,7 +6661,7 @@ - V${rectangle.y + rectangle.height} - H${rectangle.x} Z - -- `, { ...options, fillStyle: 'solid' }); -+ `, options); - const componentShape = ComponentEngine.draw(board, componentRectangle, options); - shape.append(componentShape); - setStrokeLinecap(shape, 'round'); -@@ -6714,7 +6706,6 @@ - const rs = PlaitBoard.getRoughSVG(board); - return drawRoundRectangle(rs, rectangle.x, rectangle.y, rectangle.x + rectangle.width, rectangle.y + rectangle.height, { - ...options, -- fillStyle: 'solid', - dashGap: 10, - strokeLineDash: [10, 10] - }, false, 4); -@@ -8014,8 +8005,7 @@ - const circleG = drawCircle(PlaitBoard.getRoughSVG(board), ref.connectorPoint || ref.edgePoint, 6, { - stroke: SELECTION_BORDER_COLOR, - strokeWidth: SNAPPING_STROKE_WIDTH, -- fill: SELECTION_BORDER_COLOR, -- fillStyle: 'solid' -+ fill: SELECTION_BORDER_COLOR - }); - boundShapeG.appendChild(circleG); - } -@@ -8266,8 +8256,7 @@ - // function 1: dnd - reactionG = drawCircle(PlaitBoard.getRoughSVG(board), hitPoint, LINE_AUTO_COMPLETE_HOVERED_DIAMETER, { - stroke: 'none', -- fill: rgbaToHEX(PRIMARY_COLOR, LINE_AUTO_COMPLETE_HOVERED_OPACITY), -- fillStyle: 'solid' -+ fill: rgbaToHEX(PRIMARY_COLOR, LINE_AUTO_COMPLETE_HOVERED_OPACITY) - }); - PlaitBoard.getActiveHost(board).append(reactionG); - PlaitBoard.getBoardContainer(board).classList.add(CursorClass.crosshair); \ No newline at end of file diff --git a/scripts/apply-patches.sh b/scripts/apply-patches.sh deleted file mode 100755 index c732932..0000000 --- a/scripts/apply-patches.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - -# Get actual installed version of @plait/draw -DRAW_VERSION=$(node -p "require('$PROJECT_ROOT/node_modules/@plait/draw/package.json').version" 2>/dev/null || echo "unknown") -PATCH_FILE="$PROJECT_ROOT/patches/@plait+draw+$DRAW_VERSION.patch" -TARGET_DIR="$PROJECT_ROOT/node_modules/@plait/draw/fesm2022" -TARGET_FILE="$TARGET_DIR/plait-draw.mjs" - -if [ "$DRAW_VERSION" = "unknown" ]; then - echo "⚠ Could not determine @plait/draw version" - exit 0 -fi - -if [ ! -f "$PATCH_FILE" ]; then - echo "⚠ Patch file not found: $PATCH_FILE (looking for version $DRAW_VERSION)" - exit 0 -fi - -if [ ! -f "$TARGET_FILE" ]; then - echo "⚠ Target file not found: $TARGET_FILE" - exit 0 -fi - -if grep -q "fillStyle: element.fillStyle" "$TARGET_FILE" 2>/dev/null; then - echo "✓ Patch already applied to @plait/draw v$DRAW_VERSION" -else - echo "Applying patch to @plait/draw v$DRAW_VERSION..." - cd "$TARGET_DIR" - patch -p0 < "$PATCH_FILE" --forward 2>/dev/null || { - echo "⚠ Patch may have failed or already been applied" - } - cd - > /dev/null -fi diff --git a/tests/unit/sticky-note.test.ts b/tests/unit/sticky-note.test.ts index 0e321e7..22ab4f1 100644 --- a/tests/unit/sticky-note.test.ts +++ b/tests/unit/sticky-note.test.ts @@ -179,6 +179,43 @@ describe('with-sticky-note', () => { expect(long.width).toBeGreaterThan(short.width); }); + + it('grows beyond 320px tall when content has many lines', async () => { + const { estimateStickySize } = await import('@/features/board/utils/sticky-note'); + + const longContent = Array.from({ length: 20 }, (_, i) => `Line ${i + 1}`).join('\n'); + const result = estimateStickySize(longContent); + + expect(result.height).toBeGreaterThan(320); + }); + + it('keeps width bounded at a sensible post-it max even for very long single lines', async () => { + const { estimateStickySize } = await import('@/features/board/utils/sticky-note'); + + const veryLong = 'a'.repeat(500); + const result = estimateStickySize(veryLong); + + expect(result.width).toBeLessThanOrEqual(320); + expect(result.width).toBeGreaterThanOrEqual(220); + }); + + it('returns a square minimum size for empty text', async () => { + const { estimateStickySize } = await import('@/features/board/utils/sticky-note'); + + const result = estimateStickySize(''); + + expect(result.width).toBe(result.height); + expect(result.width).toBeGreaterThanOrEqual(130); + }); + + it('produces enough vertical space for every wrapped line', async () => { + const { estimateStickySize } = await import('@/features/board/utils/sticky-note'); + + const lines = Array.from({ length: 30 }, (_, i) => `Item ${i + 1}: detail`); + const result = estimateStickySize(lines.join('\n')); + + expect(result.height).toBeGreaterThanOrEqual(lines.length * 18); + }); }); describe('plugin application', () => { diff --git a/tests/unit/theme-elements.test.ts b/tests/unit/theme-elements.test.ts index cb43bbb..05054dd 100644 --- a/tests/unit/theme-elements.test.ts +++ b/tests/unit/theme-elements.test.ts @@ -213,4 +213,103 @@ describe('syncElementsForBoardTheme', () => { expect(nextElements[0].fill).toBe('#17344b'); }); + + it('uses dark text on a custom light fill in dark themes', () => { + const elements = [ + { + id: 'shape-1', + type: 'geometry', + shape: 'rectangle', + fill: '#bbf7d0', + text: { children: [{ text: 'Box' }] }, + }, + ] as unknown as PlaitElement[]; + + const nextElements = syncElementsForBoardTheme(elements, 'dark') as Array<{ + fill?: string; + text?: { children: Array<{ color?: string }> }; + }>; + + expect(nextElements[0].fill).toBe('#bbf7d0'); + expect(nextElements[0].text?.children[0].color).toBe('#000000'); + }); + + it('keeps default-ink text dark on a custom light fill in starry themes', () => { + const elements = [ + { + id: 'shape-1', + type: 'geometry', + shape: 'rectangle', + fill: '#fef3c7', + text: { children: [{ text: 'Box', color: '#000000' }] }, + }, + ] as unknown as PlaitElement[]; + + const nextElements = syncElementsForBoardTheme(elements, 'starry') as Array<{ + fill?: string; + text?: { children: Array<{ color?: string }> }; + }>; + + expect(nextElements[0].fill).toBe('#fef3c7'); + expect(nextElements[0].text?.children[0].color).toBe('#000000'); + }); + + it('uses light text on a custom dark fill in light themes', () => { + const elements = [ + { + id: 'shape-1', + type: 'geometry', + shape: 'rectangle', + fill: '#1e293b', + text: { children: [{ text: 'Box' }] }, + }, + ] as unknown as PlaitElement[]; + + const nextElements = syncElementsForBoardTheme(elements, 'default') as Array<{ + fill?: string; + text?: { children: Array<{ color?: string }> }; + }>; + + expect(nextElements[0].fill).toBe('#1e293b'); + expect(nextElements[0].text?.children[0].color).toBe('#f8fafc'); + }); + + it('preserves a user-customized text color even on custom fills', () => { + const elements = [ + { + id: 'shape-1', + type: 'geometry', + shape: 'rectangle', + fill: '#bbf7d0', + text: { children: [{ text: 'Box', color: '#ff00aa' }] }, + }, + ] as unknown as PlaitElement[]; + + const nextElements = syncElementsForBoardTheme(elements, 'dark') as Array<{ + text?: { children: Array<{ color?: string }> }; + }>; + + expect(nextElements[0].text?.children[0].color).toBe('#ff00aa'); + }); + + it('uses contrast-aware text on a mind topic with a custom fill', () => { + const elements = [ + { + id: 'mind-1', + type: 'mindmap', + fill: '#bbf7d0', + data: { + topic: { children: [{ text: 'Branch' }] }, + }, + }, + ] as unknown as PlaitElement[]; + + const nextElements = syncElementsForBoardTheme(elements, 'dark') as Array<{ + fill?: string; + data?: { topic?: { children: Array<{ color?: string }> } }; + }>; + + expect(nextElements[0].fill).toBe('#bbf7d0'); + expect(nextElements[0].data?.topic?.children[0].color).toBe('#000000'); + }); });