c141960ada
* Fixed point binding for simple arrows Tests added Fix binding Remove unneeded params Unfinished simple arrow avoidance Fix newly created jumping arrow when gets outside Do not apply the jumping logic to elbow arrows for new elements Existing arrows now jump out Type updates to support fixed binding for simple arrows Fix crash for elbow arrws in mutateElement() Refactored simple arrow creation Updating tests No confirm threshold when inside biding range Fix multi-point arrow grid off Make elbow arrows respect grids Unbind arrow if bound and moved at shaft of arrow key Fix binding test Fix drag unbind when the bound element is in the selection Do not move mid point for simple arrows bound on both ends Add test for mobing mid points for simple arrows when bound on the same element on both ends Fix linear editor bug when both midpoint and endpoint is moved Fix all point multipoint arrow highlight and binding Arrow dragging gets a little drag to avoid accidental unbinding Fixed point binding for simple arrows when the arrow doesn't point to the element Fix binding disabled use-case triggering arrow editor Timed binding mode change for simple arrows Apply fixes Remove code to unbind on drag Update simple arrow fixed point when arrow is dragged or moved by arrow keys Binding highlight fixes Change bind mode timeout logic Fix tests Add Alt bindMode switch No dragging of arrows when bound, similar to elbow Fix timeout not taking effect immediately Bumop z-index for arrows when dragged Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Only transparent bindables allow binding fallthrough Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Fix lint Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Fix point click array creation interaction with fixed point binding Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Restrict new behavior to arrows only Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Allow binding inside images Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Fix already existing fixed binding retention Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Refactor and implement fixed point binding for unfilled elements Restore drag Removed point binding Binding code refactor Added centered focus point Binding & focus point debug Add invariants to check binding integrity in elements Binding fixes Small refactors Completely rewritten binding Include point updates after binding update Fix point updates when endpoint dragged and opposite endpoint orbits centered focus point only for new arrows Make z-index arrow reorder on bind Turn off inside binding mode after leaving a shape Remove invariants from debug feat: expose `applyTo` options, don't commit empty text element (#9744) * Expose applyTo options, skip re-draw for empty text * Don't commit empty text elements test: added test file for distribute (#9754) z-index update Bind mode on precise binding Fix binding to inside element Fix initial arrow not following cursor (white dot) Fix elbow arrow Fix z-index so it works on hover Fix fixed angle orbiting Move point click arrow creation over to common strategy Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Add binding strategy for drag arrow creation Fix elbow arrow Fix point handles Snap to center Fix transparent shape binding Internal arrow creation fix Fix point binding Fix selection bug Fix new arrow focus point Images now always bind inside Flashing arrow creation on binding band Add watchState debug method to window.h Fix debug canvas crash Remove non-needed bind mode Fix restore No keyboard movement when bound Add actionFinalize when arrow in edit mode Add drag to the Stats panel when bound arrow is moved Further simplify curve tracking Add typing to action register() Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Fix point at finalize Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Fix type errors Signed-off-by: Mark Tolmacs <mark@lazycat.hu> New arrow binding rules Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Fix cyclical dep Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Fix jiggly arrows Fix jiggly arrow x2 Long inside-other binding Click-click binding Fix arrows Performance [PERF] Replace in-place Jacobian derivation with analytical version Different approach to inside binding Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Fixes Fix inconsistent arrow start jump out Change how images are bound to on new arrow creation Lower timeout Small insurance fix Fix curve test Signed-off-by: Mark Tolmacs <mark@lazycat.hu> No center focus point 90% inside center binding Fixing tests fix: Elbow arrow fixes fix: More arrow fixes Do not trigger arrow binding for linear elements fix: Linear elements fix: Refactor actionFinalize for linear Binding tests updated fix: Jump when cursor not moved fix: history tests Fix history snapshot Fix undo issue fix(eraser): Remove binding from the other element fix(tests): Update tests chore: Attempt filtering new set state Fix excessive history recording Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Fix all tests Signed-off-by: Mark Tolmacs <mark@lazycat.hu> fix(transform): Fix group resize and rotate fix(binding): Harmonize binding param usage fix: Center focus point Signed-off-by: Mark Tolmacs <mark@lazycat.hu> chore: Trigger build Remove binding gap Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Binding highlight refactor fix: Refactored timeout bind mode handling fix: Center when orbiting feat: Color change on highlight Fix orbit binding highlight fix: hiding arrow Fix arrow binding Fix arrow drag selection logic Binding highlight is now hot pink Change inside binding logic for start point Render focus point in debug mode Fix snap to center Fix actionFinalize for new arrow creation fix: snapToCenter() 80% by length fix: attempt at fixing the dancing arrows feat: No center snap when start is not bound Fix centering for existing arrows tweak binding highlight color change `appState.suggestedBindings` -> `suggestedBinding` & remove unused code Refactor delayed bind mode change Binding highlight rotation support + image support fix(highlight): Overdraw fixes feat: Do not allow drag bound arrow closer to the shape than dragging distance feat: Stroke width adaptive fixed binding distance chore: More point dragging centralization New element behavior Refactor dragging Fix incorrect highlight sizing Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Fix delayed bind mode for multiElement arrows Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Fix multi-point Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Fix elbow arrows Simplify state Small positional fixes Fix jiggly arrows Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Fixes for arrow dragging Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Elbow arrow fixes Highlight fixes Fix elbow arrow binding Frame highlight Fix elbow mid-point binding Fix binding suggestion for disabled binding state Implement Alt Remove debug * CHange new arrow creation * fix: allow inside binding via timeout if arrow has no startBinding * fix: Delete invariant violation with arrows * fix: Deleted arrow causes problems * fix: Dragging issues * fix: Dragging fix 2 * fix: Disable drag drag when arrow is bound * fix: Multipoint arrow opposite point movement * fix: Ctrl+Alt precedence * feat: Alt inside start binding mode change * Fix multipoint arrow orbit Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Arrow start inside binding switch Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: New arrow never binds inside Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Small refactor Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Multi-point arrows and linears Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Lint Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Nested shapes handling Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Overlap behavior * Alt unbind fix Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Existing arrow nested bindable Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Binding suggestions Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Circular dep Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: snapshots Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Alt immediate update Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Laxing on invariants Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: New highlight overdraws arrow Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Image binding rule changed * Trigger Rebuild * fix:Highlight flicker Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Fully nested shapes * fix: Tune nested shape binding * fix: Size-based orbit jump-in * fix: Binding highlight stroke on sharp bindables * fix: Nested shape binding * fix: history * fix:More precise element nesting check * feat:Add tolerance to shape nesting detection * fix: Reverse * fix:Change center binding to circular * ignore invisible elements when binding * feat: Center point with more precise highlight outlines * fix:Arrow tool hover stuck highlight * fix:More stroke width for highlight * POC: highlight center on hover * tweak binding highlight width * render highlight on the outside * fix: Locked elbow arrow creation * update hints * reduce timeout * handle overlap when both elements the same size * tweak highlight stroke width some more * fix:Add intersection padding * fix: New arrow start orbit when nested binds on the end * fix: Update history snapshot * feat: Allow inside binding for new arrows in nested cases * chore: Logic for measurement * fix: Locked tool + arrow * feat: Remove center binding * fix: Jump arrow inside it gets visially too short Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore:Basic interactive canvas animation re-render trigger for highlights Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat:Highlight animations Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix:Refactored and fixed highlight animation * fix:Poisoned arrow * fix Arrow edit mode selection * fix:Tool lock binding behavior restored * fix:Overlap inside binding * fix:Animated binding highlight * alt anims + increase timeout to 700 * tweak animation some more + remove countdown * fix: False bind timeout indicator Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: better file normalization (#10024) * feat: better file normalization * fix lint * fix png detection * optimize * fix type * fix: increase rejection delay for opening files with legacy api (#8961) * Increased input change interval to 1000 ms to fix IOS 18 file opening issue * increase more --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> * feat: library search (#9903) * feat(utils): add support for search input type in isWritableElement * feat(i18n): add search text * feat(cmdp+lib): add search functionality for command pallete and lib menu items * chore: fix formats, and whitespaces * fix: opt to optimal code changes * chore: fix for linting * focus input on mount * tweak placeholder * design and UX changes * tweak item hover/active/seletected states * unrelated: move publish button above delete/clear to keep it more stable * esc to clear search input / close sidebar * refactor command pallete library stuff * make library commands bigger --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> * fix: Allow already inside bound arrows to continue inside binding Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: No angle lock over bindable elements Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Center binding on SHIFT key Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * Fix ghost start binding Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * FEAT: No binding to frame cutout * feat: Bind to frame when frame-bound object hidden part is approached * fix: revert preferred selection to box once you switch to `full` UI (#10160) * fix: mobile UI and other fixes (#10177) * remove legacy openMenu=shape state and unused actions * close menus/popups in applicable cases when opening a different one * split ui z-indexes to account prefer different overlap * make top canvas area clickable on mobile * make mobile main menu closable by clicking outside and reduce width * offset picker popups from viewport border on mobile * reduce items gap in mobile main menu * show top picks for canvas bg colors in all ui modes * fix menu separator visibility on mobile * fix command palette items not being filtered * fix: Increase transform handle offset (#10180) * fix: Increase transform handle offset Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Temporarily disable transform handles for linear elements on mobile and tablets Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Linear hidden resize Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * disable mobielOrTablet linear element bbox completely * fix: Test Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Lint Signed-off-by: Mark Tolmacs <mark@lazycat.hu> --------- Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> * fix: context menu getting covered (#10199) * do not show z-index actions on mobile or tablet * fix: context menu getting covered * fix lint * fix style popup getting covered * put contextmenu z-index above sidebar --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> * feat: More prominent keyboard shortcuts in hints (#10057) * Initial * Memoize * Styling * Use double angle brackets for keyboard shortcuts * Use rem in gap * Use an existing function for substituting tags in a string * Revert styling * Avoid unique key warnings * Styling * getTransChildren -> nodesFromTextWithTags * Use height and padding instead of padding only * Initial new idea * WIP shortcut substitutions * Use simple regex for parsing shortcuts * Use single shortcut for combos * Use kbd instead of span * shortcutFromKeyString -> getTaggedShortcutKey * Bug fix * FlowChart -> Flowchart * memo is useless here * Trigger CI * Translate in getShortcutKey * More normalized shortcuts * improve shortcut normalization and replacement & support multi-key tagged shortcuts * fix regex * tweak css --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> * fix: small tweaks to shortcut hints (#10214) * fix: Test Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Bind mode * feat: Support special key shortcut highlight * fix: Lint * fix: Remove non-needed function * fix: Skip frame cutout for hover, but keep shape for binding * fix: Lint * fix: Restore removal of deleted elements on restore * fix: Inside-inside during drag Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Arrow vanishes when orbit binding to the same element * feat: Feature flag support * Simplified binding * fix: Diamond corner binding * feat: Binding highlight band re-added * feat: Settings menu * fix: Same shape binding * fix: set radix PropertiesPopover collision boundary (#10221) * Set collision boundary * Calculate collisionPadding dynamically based on container * Add appState offsetTop and offsetLeft to padding calculation. Refactor collisionPadding calculation to use app state offsets. * Update PropertiesPopover.tsx * popover positioning relative to container * fix: prevent wrap text in a container to only text that are not bound to a container (#10250) * fix: only enable wrap text in a container when at least one text element selected is unbound * Trigger Rebuild --------- Co-authored-by: Mark Tolmacs <mark@lazycat.hu> * chore: Uncap the nodejs version requirement (#10238) Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> * refactor: single source of truths with editor interface (#10178) * refactor device to editor interface and derive styles panel * allow host app to control form factor and ui mode * add editor interface event listener * put new props inside UIOptions * refactor: move related apis into one file * expose getFormFactor * privatize the setting of desktop mode and fix snapshots * refactor and fix test * remove unimplemented code * export getFormFactor() * replace `getFormFactor` with `getEditorInterface` * remove dead & useless * comment * fix ts --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> * chore: Update translations from Crowdin (#7429) * New translations en.json (Punjabi) * New translations en.json (Polish) * New translations en.json (Russian) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Traditional) * New translations en.json (Vietnamese) * New translations en.json (Galician) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Persian) * New translations en.json (Khmer) * New translations en.json (Tamil) * New translations en.json (Bengali) * New translations en.json (Marathi) * New translations en.json (Thai) * New translations en.json (Norwegian Nynorsk) * New translations en.json (Kazakh) * New translations en.json (Latvian) * New translations en.json (Azerbaijani) * New translations en.json (Hindi) * New translations en.json (Burmese) * New translations en.json (Chinese Traditional, Hong Kong) * New translations en.json (Sinhala) * New translations en.json (Uzbek) * New translations en.json (Norwegian Bokmal) * New translations en.json (Occitan) * New translations en.json (German, Switzerland) * New translations en.json (Bengali, India) * New translations en.json (Kabyle) * New translations en.json (Karakalpak) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Marathi) * New translations en.json (Hindi) * New translations en.json (German) * New translations en.json (Chinese Simplified) * New translations en.json (Polish) * New translations en.json (Romanian) * New translations en.json (Korean) * New translations en.json (Chinese Traditional) * New translations en.json (Hebrew) * New translations en.json (Hebrew) * New translations en.json (Slovak) * New translations en.json (Slovak) * New translations en.json (Hungarian) * New translations en.json (Hungarian) * New translations en.json (Slovak) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Korean) * New translations en.json (Chinese Traditional) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Bulgarian) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Basque) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Kurdish) * New translations en.json (Lithuanian) * New translations en.json (Dutch) * New translations en.json (Punjabi) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Slovenian) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Vietnamese) * New translations en.json (Galician) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Indonesian) * New translations en.json (Persian) * New translations en.json (Khmer) * New translations en.json (Tamil) * New translations en.json (Bengali) * New translations en.json (Marathi) * New translations en.json (Thai) * New translations en.json (Norwegian Nynorsk) * New translations en.json (Kazakh) * New translations en.json (Latvian) * New translations en.json (Azerbaijani) * New translations en.json (Hindi) * New translations en.json (Burmese) * New translations en.json (Chinese Traditional, Hong Kong) * New translations en.json (Sinhala) * New translations en.json (Uzbek) * New translations en.json (Norwegian Bokmal) * New translations en.json (Occitan) * New translations en.json (German, Switzerland) * New translations en.json (Bengali, India) * New translations en.json (Kabyle) * New translations en.json (Karakalpak) * New translations en.json (Romanian) * New translations en.json (German) * New translations en.json (Slovenian) * New translations en.json (Chinese Simplified) * New translations en.json (Spanish) * New translations en.json (Russian) * New translations en.json (Chinese Traditional) * New translations en.json (Turkish) * New translations en.json (Slovak) * New translations en.json (Slovak) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Korean) * New translations en.json (Chinese Traditional) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Bulgarian) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Basque) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Kurdish) * New translations en.json (Lithuanian) * New translations en.json (Dutch) * New translations en.json (Punjabi) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Slovenian) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Vietnamese) * New translations en.json (Galician) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Indonesian) * New translations en.json (Persian) * New translations en.json (Khmer) * New translations en.json (Tamil) * New translations en.json (Bengali) * New translations en.json (Marathi) * New translations en.json (Thai) * New translations en.json (Norwegian Nynorsk) * New translations en.json (Kazakh) * New translations en.json (Latvian) * New translations en.json (Azerbaijani) * New translations en.json (Hindi) * New translations en.json (Burmese) * New translations en.json (Chinese Traditional, Hong Kong) * New translations en.json (Sinhala) * New translations en.json (Uzbek) * New translations en.json (Norwegian Bokmal) * New translations en.json (Occitan) * New translations en.json (German, Switzerland) * New translations en.json (Bengali, India) * New translations en.json (Kabyle) * New translations en.json (Karakalpak) * New translations en.json (German) * New translations en.json (Russian) * New translations en.json (Romanian) * New translations en.json (Spanish) * New translations en.json (Chinese Simplified) * New translations en.json (Marathi) * New translations en.json (Hindi) * New translations en.json (Slovak) * New translations en.json (German) * New translations en.json (Portuguese) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Bulgarian) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Basque) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Kurdish) * New translations en.json (Lithuanian) * New translations en.json (Dutch) * New translations en.json (Punjabi) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Slovak) * New translations en.json (Slovenian) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Vietnamese) * New translations en.json (Galician) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Indonesian) * New translations en.json (Persian) * New translations en.json (Khmer) * New translations en.json (Tamil) * New translations en.json (Bengali) * New translations en.json (Marathi) * New translations en.json (Thai) * New translations en.json (Norwegian Nynorsk) * New translations en.json (Kazakh) * New translations en.json (Latvian) * New translations en.json (Azerbaijani) * New translations en.json (Hindi) * New translations en.json (Burmese) * New translations en.json (Chinese Traditional, Hong Kong) * New translations en.json (Sinhala) * New translations en.json (Uzbek) * New translations en.json (Norwegian Bokmal) * New translations en.json (Occitan) * New translations en.json (German, Switzerland) * New translations en.json (Bengali, India) * New translations en.json (Kabyle) * New translations en.json (Karakalpak) * Auto commit: Calculate translation coverage * New translations en.json (Chinese Simplified) * Auto commit: Calculate translation coverage * New translations en.json (Romanian) * Auto commit: Calculate translation coverage * New translations en.json (Romanian) * Auto commit: Calculate translation coverage * New translations en.json (French) * Auto commit: Calculate translation coverage * New translations en.json (Polish) * Auto commit: Calculate translation coverage * New translations en.json (Polish) * Auto commit: Calculate translation coverage * New translations en.json (Turkish) * Auto commit: Calculate translation coverage * New translations en.json (Turkish) --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> * fix: mobile view ui issues (#10284) * hide zen mode when formFactor = phone * tool bar fixes: icon and width * view mode * fix lint * add exit-view-mode button --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> * chore: Update snaps Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Blue highlight Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Diagonal binding point Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Remove settings Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Jump other binding Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Hovered arrow mode highlight Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Alt does not snap Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Check debug Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Alt precise positioning * fix: Jump out to orbit for new arrows when dragged outside * fix: New arrow preserved projection * chore: Remove debug * chore: Introduce different debug color for orbit and other binding modes * fix: Restore arrow start point when self binding * fix: Turn of start jump-out * fix: Tests * fix: Select the first possible altBindPoint * fix: Random projection * fix: Use last point for existing arrows * fix: Preserve alternate orbit focus point during drag * fix: Lint * fix: Update tests * fix: Elbow arrow direction at binding * binding gap and distance and binding highlight tweaks * chore: Naming refactors Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Tests Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Alt-duplication copied elements placement (#10152) * feat: Animation support (#10042) * fix: banner url (#10315) * feat: Animation support (#10042) * fix: Merge discrepancy Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Remove non-needed code Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * Trigger build * chore: Remove hint for V1 Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * shorten focus point diagonal helpers to fix corner binding cases * fix: Tests * fix: Multi-point arrow closeness fallback * fix: Finalize multipoint arrow on binding area click * fix: Finalize arrow now truly finalzes Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Point click arrow creation jumping to orbit Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Alt+drag movement block Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Tests Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * Trigger build * feat: hide point highlight when dragging * feat: hide bbox when dragging points * revert binding gap increase for elbow arrows * reset selectionLinearElement on tool change * chore: Remove debug * feat: Better restore for bindings * use elementsMap instead of array when passing to restoreElement * fix: Arrow angle reset * fix: Arrow angle * Arrow angle support * fix trashing cached canvases in `LinearElementEditor.getElementAbsoluteCoords` --------- Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
1290 lines
33 KiB
TypeScript
1290 lines
33 KiB
TypeScript
import rough from "roughjs/bin/rough";
|
|
|
|
import {
|
|
arrayToMap,
|
|
invariant,
|
|
rescalePoints,
|
|
sizeOf,
|
|
} from "@excalidraw/common";
|
|
|
|
import {
|
|
degreesToRadians,
|
|
lineSegment,
|
|
pointDistance,
|
|
pointFrom,
|
|
pointFromArray,
|
|
pointRotateRads,
|
|
} from "@excalidraw/math";
|
|
|
|
import { getCurvePathOps } from "@excalidraw/utils/shape";
|
|
|
|
import { pointsOnBezierCurves } from "points-on-curve";
|
|
|
|
import type {
|
|
Curve,
|
|
Degrees,
|
|
GlobalPoint,
|
|
LineSegment,
|
|
LocalPoint,
|
|
Radians,
|
|
} from "@excalidraw/math";
|
|
|
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
|
|
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
|
|
|
import { generateRoughOptions } from "./shape";
|
|
import { ShapeCache } from "./shape";
|
|
import { LinearElementEditor } from "./linearElementEditor";
|
|
import { getBoundTextElement, getContainerElement } from "./textElement";
|
|
import {
|
|
isArrowElement,
|
|
isBoundToContainer,
|
|
isFreeDrawElement,
|
|
isLinearElement,
|
|
isLineElement,
|
|
isTextElement,
|
|
} from "./typeChecks";
|
|
|
|
import { getElementShape } from "./shape";
|
|
|
|
import {
|
|
deconstructDiamondElement,
|
|
deconstructRectanguloidElement,
|
|
} from "./utils";
|
|
|
|
import type { Drawable, Op } from "roughjs/bin/core";
|
|
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
|
import type {
|
|
Arrowhead,
|
|
ElementsMap,
|
|
ElementsMapOrArray,
|
|
ExcalidrawElement,
|
|
ExcalidrawEllipseElement,
|
|
ExcalidrawFreeDrawElement,
|
|
ExcalidrawLinearElement,
|
|
ExcalidrawRectanguloidElement,
|
|
ExcalidrawTextElementWithContainer,
|
|
NonDeleted,
|
|
} from "./types";
|
|
|
|
export type RectangleBox = {
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
angle: number;
|
|
};
|
|
|
|
type MaybeQuadraticSolution = [number | null, number | null] | false;
|
|
|
|
/**
|
|
* x and y position of top left corner, x and y position of bottom right corner
|
|
*/
|
|
export type Bounds = readonly [
|
|
minX: number,
|
|
minY: number,
|
|
maxX: number,
|
|
maxY: number,
|
|
];
|
|
|
|
export type SceneBounds = readonly [
|
|
sceneX: number,
|
|
sceneY: number,
|
|
sceneX2: number,
|
|
sceneY2: number,
|
|
];
|
|
|
|
export class ElementBounds {
|
|
private static boundsCache = new WeakMap<
|
|
ExcalidrawElement,
|
|
{
|
|
bounds: Bounds;
|
|
version: ExcalidrawElement["version"];
|
|
}
|
|
>();
|
|
private static nonRotatedBoundsCache = new WeakMap<
|
|
ExcalidrawElement,
|
|
{
|
|
bounds: Bounds;
|
|
version: ExcalidrawElement["version"];
|
|
}
|
|
>();
|
|
|
|
static getBounds(
|
|
element: ExcalidrawElement,
|
|
elementsMap: ElementsMap,
|
|
nonRotated: boolean = false,
|
|
) {
|
|
const cachedBounds =
|
|
nonRotated && element.angle !== 0
|
|
? ElementBounds.nonRotatedBoundsCache.get(element)
|
|
: ElementBounds.boundsCache.get(element);
|
|
|
|
if (
|
|
cachedBounds?.version &&
|
|
cachedBounds.version === element.version &&
|
|
// we don't invalidate cache when we update containers and not labels,
|
|
// which is causing problems down the line. Fix TBA.
|
|
!isBoundToContainer(element)
|
|
) {
|
|
return cachedBounds.bounds;
|
|
}
|
|
|
|
if (nonRotated && element.angle !== 0) {
|
|
const nonRotatedBounds = ElementBounds.calculateBounds(
|
|
{
|
|
...element,
|
|
angle: 0 as Radians,
|
|
},
|
|
elementsMap,
|
|
);
|
|
ElementBounds.nonRotatedBoundsCache.set(element, {
|
|
version: element.version,
|
|
bounds: nonRotatedBounds,
|
|
});
|
|
|
|
return nonRotatedBounds;
|
|
}
|
|
|
|
const bounds = ElementBounds.calculateBounds(element, elementsMap);
|
|
|
|
ElementBounds.boundsCache.set(element, {
|
|
version: element.version,
|
|
bounds,
|
|
});
|
|
|
|
return bounds;
|
|
}
|
|
|
|
private static calculateBounds(
|
|
element: ExcalidrawElement,
|
|
elementsMap: ElementsMap,
|
|
): Bounds {
|
|
let bounds: Bounds;
|
|
|
|
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
|
element,
|
|
elementsMap,
|
|
);
|
|
if (isFreeDrawElement(element)) {
|
|
const [minX, minY, maxX, maxY] = getBoundsFromPoints(
|
|
element.points.map(([x, y]) =>
|
|
pointRotateRads(
|
|
pointFrom(x, y),
|
|
pointFrom(cx - element.x, cy - element.y),
|
|
element.angle,
|
|
),
|
|
),
|
|
);
|
|
|
|
return [
|
|
minX + element.x,
|
|
minY + element.y,
|
|
maxX + element.x,
|
|
maxY + element.y,
|
|
];
|
|
} else if (isLinearElement(element)) {
|
|
bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap);
|
|
} else if (element.type === "diamond") {
|
|
const [x11, y11] = pointRotateRads(
|
|
pointFrom(cx, y1),
|
|
pointFrom(cx, cy),
|
|
element.angle,
|
|
);
|
|
const [x12, y12] = pointRotateRads(
|
|
pointFrom(cx, y2),
|
|
pointFrom(cx, cy),
|
|
element.angle,
|
|
);
|
|
const [x22, y22] = pointRotateRads(
|
|
pointFrom(x1, cy),
|
|
pointFrom(cx, cy),
|
|
element.angle,
|
|
);
|
|
const [x21, y21] = pointRotateRads(
|
|
pointFrom(x2, cy),
|
|
pointFrom(cx, cy),
|
|
element.angle,
|
|
);
|
|
const minX = Math.min(x11, x12, x22, x21);
|
|
const minY = Math.min(y11, y12, y22, y21);
|
|
const maxX = Math.max(x11, x12, x22, x21);
|
|
const maxY = Math.max(y11, y12, y22, y21);
|
|
bounds = [minX, minY, maxX, maxY];
|
|
} else if (element.type === "ellipse") {
|
|
const w = (x2 - x1) / 2;
|
|
const h = (y2 - y1) / 2;
|
|
const cos = Math.cos(element.angle);
|
|
const sin = Math.sin(element.angle);
|
|
const ww = Math.hypot(w * cos, h * sin);
|
|
const hh = Math.hypot(h * cos, w * sin);
|
|
bounds = [cx - ww, cy - hh, cx + ww, cy + hh];
|
|
} else {
|
|
const [x11, y11] = pointRotateRads(
|
|
pointFrom(x1, y1),
|
|
pointFrom(cx, cy),
|
|
element.angle,
|
|
);
|
|
const [x12, y12] = pointRotateRads(
|
|
pointFrom(x1, y2),
|
|
pointFrom(cx, cy),
|
|
element.angle,
|
|
);
|
|
const [x22, y22] = pointRotateRads(
|
|
pointFrom(x2, y2),
|
|
pointFrom(cx, cy),
|
|
element.angle,
|
|
);
|
|
const [x21, y21] = pointRotateRads(
|
|
pointFrom(x2, y1),
|
|
pointFrom(cx, cy),
|
|
element.angle,
|
|
);
|
|
const minX = Math.min(x11, x12, x22, x21);
|
|
const minY = Math.min(y11, y12, y22, y21);
|
|
const maxX = Math.max(x11, x12, x22, x21);
|
|
const maxY = Math.max(y11, y12, y22, y21);
|
|
bounds = [minX, minY, maxX, maxY];
|
|
}
|
|
|
|
return bounds;
|
|
}
|
|
}
|
|
|
|
// Scene -> Scene coords, but in x1,x2,y1,y2 format.
|
|
//
|
|
// If the element is created from right to left, the width is going to be negative
|
|
// This set of functions retrieves the absolute position of the 4 points.
|
|
export const getElementAbsoluteCoords = (
|
|
element: ExcalidrawElement,
|
|
elementsMap: ElementsMap,
|
|
includeBoundText: boolean = false,
|
|
): [number, number, number, number, number, number] => {
|
|
if (isFreeDrawElement(element)) {
|
|
return getFreeDrawElementAbsoluteCoords(element);
|
|
} else if (isLinearElement(element)) {
|
|
return LinearElementEditor.getElementAbsoluteCoords(
|
|
element,
|
|
elementsMap,
|
|
includeBoundText,
|
|
);
|
|
} else if (isTextElement(element)) {
|
|
const container = elementsMap
|
|
? getContainerElement(element, elementsMap)
|
|
: null;
|
|
if (isArrowElement(container)) {
|
|
const { x, y } = LinearElementEditor.getBoundTextElementPosition(
|
|
container,
|
|
element as ExcalidrawTextElementWithContainer,
|
|
elementsMap,
|
|
);
|
|
return [
|
|
x,
|
|
y,
|
|
x + element.width,
|
|
y + element.height,
|
|
x + element.width / 2,
|
|
y + element.height / 2,
|
|
];
|
|
}
|
|
}
|
|
return [
|
|
element.x,
|
|
element.y,
|
|
element.x + element.width,
|
|
element.y + element.height,
|
|
element.x + element.width / 2,
|
|
element.y + element.height / 2,
|
|
];
|
|
};
|
|
|
|
/*
|
|
* for a given element, `getElementLineSegments` returns line segments
|
|
* that can be used for visual collision detection (useful for frames)
|
|
* as opposed to bounding box collision detection
|
|
*/
|
|
/**
|
|
* Given an element, return the line segments that make up the element.
|
|
*
|
|
* Uses helpers from /math
|
|
*/
|
|
export const getElementLineSegments = (
|
|
element: ExcalidrawElement,
|
|
elementsMap: ElementsMap,
|
|
): LineSegment<GlobalPoint>[] => {
|
|
const shape = getElementShape(element, elementsMap);
|
|
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
|
element,
|
|
elementsMap,
|
|
);
|
|
const center = pointFrom<GlobalPoint>(cx, cy);
|
|
|
|
if (shape.type === "polycurve") {
|
|
const curves = shape.data;
|
|
const pointsOnCurves = curves.map((curve) =>
|
|
pointsOnBezierCurves(curve, 10),
|
|
);
|
|
|
|
const segments: LineSegment<GlobalPoint>[] = [];
|
|
|
|
if (
|
|
(isLineElement(element) && !element.polygon) ||
|
|
isArrowElement(element)
|
|
) {
|
|
for (const points of pointsOnCurves) {
|
|
let i = 0;
|
|
|
|
while (i < points.length - 1) {
|
|
segments.push(
|
|
lineSegment(
|
|
pointFrom(points[i][0], points[i][1]),
|
|
pointFrom(points[i + 1][0], points[i + 1][1]),
|
|
),
|
|
);
|
|
i++;
|
|
}
|
|
}
|
|
} else {
|
|
const points = pointsOnCurves.flat();
|
|
let i = 0;
|
|
|
|
while (i < points.length - 1) {
|
|
segments.push(
|
|
lineSegment(
|
|
pointFrom(points[i][0], points[i][1]),
|
|
pointFrom(points[i + 1][0], points[i + 1][1]),
|
|
),
|
|
);
|
|
i++;
|
|
}
|
|
}
|
|
|
|
return segments;
|
|
} else if (shape.type === "polyline") {
|
|
return shape.data as LineSegment<GlobalPoint>[];
|
|
} else if (_isRectanguloidElement(element)) {
|
|
const [sides, corners] = deconstructRectanguloidElement(element);
|
|
const cornerSegments: LineSegment<GlobalPoint>[] = corners
|
|
.map((corner) => getSegmentsOnCurve(corner, center, element.angle))
|
|
.flat();
|
|
const rotatedSides = getRotatedSides(sides, center, element.angle);
|
|
return [...rotatedSides, ...cornerSegments];
|
|
} else if (element.type === "diamond") {
|
|
const [sides, corners] = deconstructDiamondElement(element);
|
|
const cornerSegments = corners
|
|
.map((corner) => getSegmentsOnCurve(corner, center, element.angle))
|
|
.flat();
|
|
const rotatedSides = getRotatedSides(sides, center, element.angle);
|
|
|
|
return [...rotatedSides, ...cornerSegments];
|
|
} else if (shape.type === "polygon") {
|
|
if (isTextElement(element)) {
|
|
const container = getContainerElement(element, elementsMap);
|
|
if (container && isLinearElement(container)) {
|
|
const segments: LineSegment<GlobalPoint>[] = [
|
|
lineSegment(pointFrom(x1, y1), pointFrom(x2, y1)),
|
|
lineSegment(pointFrom(x2, y1), pointFrom(x2, y2)),
|
|
lineSegment(pointFrom(x2, y2), pointFrom(x1, y2)),
|
|
lineSegment(pointFrom(x1, y2), pointFrom(x1, y1)),
|
|
];
|
|
return segments;
|
|
}
|
|
}
|
|
|
|
const points = shape.data as GlobalPoint[];
|
|
const segments: LineSegment<GlobalPoint>[] = [];
|
|
for (let i = 0; i < points.length - 1; i++) {
|
|
segments.push(lineSegment(points[i], points[i + 1]));
|
|
}
|
|
return segments;
|
|
} else if (shape.type === "ellipse") {
|
|
return getSegmentsOnEllipse(element as ExcalidrawEllipseElement);
|
|
}
|
|
|
|
const [nw, ne, sw, se, , , w, e] = (
|
|
[
|
|
[x1, y1],
|
|
[x2, y1],
|
|
[x1, y2],
|
|
[x2, y2],
|
|
[cx, y1],
|
|
[cx, y2],
|
|
[x1, cy],
|
|
[x2, cy],
|
|
] as GlobalPoint[]
|
|
).map((point) => pointRotateRads(point, center, element.angle));
|
|
|
|
return [
|
|
lineSegment(nw, ne),
|
|
lineSegment(sw, se),
|
|
lineSegment(nw, sw),
|
|
lineSegment(ne, se),
|
|
lineSegment(nw, e),
|
|
lineSegment(sw, e),
|
|
lineSegment(ne, w),
|
|
lineSegment(se, w),
|
|
];
|
|
};
|
|
|
|
const _isRectanguloidElement = (
|
|
element: ExcalidrawElement,
|
|
): element is ExcalidrawRectanguloidElement => {
|
|
return (
|
|
element != null &&
|
|
(element.type === "rectangle" ||
|
|
element.type === "image" ||
|
|
element.type === "iframe" ||
|
|
element.type === "embeddable" ||
|
|
element.type === "frame" ||
|
|
element.type === "magicframe" ||
|
|
(element.type === "text" && !element.containerId))
|
|
);
|
|
};
|
|
|
|
const getRotatedSides = (
|
|
sides: LineSegment<GlobalPoint>[],
|
|
center: GlobalPoint,
|
|
angle: Radians,
|
|
) => {
|
|
return sides.map((side) => {
|
|
return lineSegment(
|
|
pointRotateRads<GlobalPoint>(side[0], center, angle),
|
|
pointRotateRads<GlobalPoint>(side[1], center, angle),
|
|
);
|
|
});
|
|
};
|
|
|
|
const getSegmentsOnCurve = (
|
|
curve: Curve<GlobalPoint>,
|
|
center: GlobalPoint,
|
|
angle: Radians,
|
|
): LineSegment<GlobalPoint>[] => {
|
|
const points = pointsOnBezierCurves(curve, 10);
|
|
let i = 0;
|
|
const segments: LineSegment<GlobalPoint>[] = [];
|
|
while (i < points.length - 1) {
|
|
segments.push(
|
|
lineSegment(
|
|
pointRotateRads<GlobalPoint>(
|
|
pointFrom(points[i][0], points[i][1]),
|
|
center,
|
|
angle,
|
|
),
|
|
pointRotateRads<GlobalPoint>(
|
|
pointFrom(points[i + 1][0], points[i + 1][1]),
|
|
center,
|
|
angle,
|
|
),
|
|
),
|
|
);
|
|
i++;
|
|
}
|
|
|
|
return segments;
|
|
};
|
|
|
|
const getSegmentsOnEllipse = (
|
|
ellipse: ExcalidrawEllipseElement,
|
|
): LineSegment<GlobalPoint>[] => {
|
|
const center = pointFrom<GlobalPoint>(
|
|
ellipse.x + ellipse.width / 2,
|
|
ellipse.y + ellipse.height / 2,
|
|
);
|
|
|
|
const a = ellipse.width / 2;
|
|
const b = ellipse.height / 2;
|
|
|
|
const segments: LineSegment<GlobalPoint>[] = [];
|
|
const points: GlobalPoint[] = [];
|
|
const n = 90;
|
|
const deltaT = (Math.PI * 2) / n;
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
const t = i * deltaT;
|
|
const x = center[0] + a * Math.cos(t);
|
|
const y = center[1] + b * Math.sin(t);
|
|
points.push(pointRotateRads(pointFrom(x, y), center, ellipse.angle));
|
|
}
|
|
|
|
for (let i = 0; i < points.length - 1; i++) {
|
|
segments.push(lineSegment(points[i], points[i + 1]));
|
|
}
|
|
|
|
segments.push(lineSegment(points[points.length - 1], points[0]));
|
|
return segments;
|
|
};
|
|
|
|
/**
|
|
* Scene -> Scene coords, but in x1,x2,y1,y2 format.
|
|
*
|
|
* Rectangle here means any rectangular frame, not an excalidraw element.
|
|
*/
|
|
export const getRectangleBoxAbsoluteCoords = (boxSceneCoords: RectangleBox) => {
|
|
return [
|
|
boxSceneCoords.x,
|
|
boxSceneCoords.y,
|
|
boxSceneCoords.x + boxSceneCoords.width,
|
|
boxSceneCoords.y + boxSceneCoords.height,
|
|
boxSceneCoords.x + boxSceneCoords.width / 2,
|
|
boxSceneCoords.y + boxSceneCoords.height / 2,
|
|
];
|
|
};
|
|
|
|
export const getDiamondPoints = (element: ExcalidrawElement) => {
|
|
// Here we add +1 to avoid these numbers to be 0
|
|
// otherwise rough.js will throw an error complaining about it
|
|
const topX = Math.floor(element.width / 2) + 1;
|
|
const topY = 0;
|
|
const rightX = element.width;
|
|
const rightY = Math.floor(element.height / 2) + 1;
|
|
const bottomX = topX;
|
|
const bottomY = element.height;
|
|
const leftX = 0;
|
|
const leftY = rightY;
|
|
|
|
return [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY];
|
|
};
|
|
|
|
// reference: https://eliot-jones.com/2019/12/cubic-bezier-curve-bounding-boxes
|
|
const getBezierValueForT = (
|
|
t: number,
|
|
p0: number,
|
|
p1: number,
|
|
p2: number,
|
|
p3: number,
|
|
) => {
|
|
const oneMinusT = 1 - t;
|
|
return (
|
|
Math.pow(oneMinusT, 3) * p0 +
|
|
3 * Math.pow(oneMinusT, 2) * t * p1 +
|
|
3 * oneMinusT * Math.pow(t, 2) * p2 +
|
|
Math.pow(t, 3) * p3
|
|
);
|
|
};
|
|
|
|
const solveQuadratic = (
|
|
p0: number,
|
|
p1: number,
|
|
p2: number,
|
|
p3: number,
|
|
): MaybeQuadraticSolution => {
|
|
const i = p1 - p0;
|
|
const j = p2 - p1;
|
|
const k = p3 - p2;
|
|
|
|
const a = 3 * i - 6 * j + 3 * k;
|
|
const b = 6 * j - 6 * i;
|
|
const c = 3 * i;
|
|
|
|
const sqrtPart = b * b - 4 * a * c;
|
|
const hasSolution = sqrtPart >= 0;
|
|
|
|
if (!hasSolution) {
|
|
return false;
|
|
}
|
|
|
|
let s1 = null;
|
|
let s2 = null;
|
|
|
|
let t1 = Infinity;
|
|
let t2 = Infinity;
|
|
|
|
if (a === 0) {
|
|
t1 = t2 = -c / b;
|
|
} else {
|
|
t1 = (-b + Math.sqrt(sqrtPart)) / (2 * a);
|
|
t2 = (-b - Math.sqrt(sqrtPart)) / (2 * a);
|
|
}
|
|
|
|
if (t1 >= 0 && t1 <= 1) {
|
|
s1 = getBezierValueForT(t1, p0, p1, p2, p3);
|
|
}
|
|
|
|
if (t2 >= 0 && t2 <= 1) {
|
|
s2 = getBezierValueForT(t2, p0, p1, p2, p3);
|
|
}
|
|
|
|
return [s1, s2];
|
|
};
|
|
|
|
export const getCubicBezierCurveBound = (
|
|
p0: GlobalPoint,
|
|
p1: GlobalPoint,
|
|
p2: GlobalPoint,
|
|
p3: GlobalPoint,
|
|
): Bounds => {
|
|
const solX = solveQuadratic(p0[0], p1[0], p2[0], p3[0]);
|
|
const solY = solveQuadratic(p0[1], p1[1], p2[1], p3[1]);
|
|
|
|
let minX = Math.min(p0[0], p3[0]);
|
|
let maxX = Math.max(p0[0], p3[0]);
|
|
|
|
if (solX) {
|
|
const xs = solX.filter((x) => x !== null) as number[];
|
|
minX = Math.min(minX, ...xs);
|
|
maxX = Math.max(maxX, ...xs);
|
|
}
|
|
|
|
let minY = Math.min(p0[1], p3[1]);
|
|
let maxY = Math.max(p0[1], p3[1]);
|
|
if (solY) {
|
|
const ys = solY.filter((y) => y !== null) as number[];
|
|
minY = Math.min(minY, ...ys);
|
|
maxY = Math.max(maxY, ...ys);
|
|
}
|
|
return [minX, minY, maxX, maxY];
|
|
};
|
|
|
|
export const getMinMaxXYFromCurvePathOps = (
|
|
ops: Op[],
|
|
transformXY?: (p: GlobalPoint) => GlobalPoint,
|
|
): Bounds => {
|
|
let currentP: GlobalPoint = pointFrom(0, 0);
|
|
|
|
const { minX, minY, maxX, maxY } = ops.reduce(
|
|
(limits, { op, data }) => {
|
|
// There are only four operation types:
|
|
// move, bcurveTo, lineTo, and curveTo
|
|
if (op === "move") {
|
|
// change starting point
|
|
const p: GlobalPoint | undefined = pointFromArray(data);
|
|
invariant(p != null, "Op data is not a point");
|
|
currentP = p;
|
|
// move operation does not draw anything; so, it always
|
|
// returns false
|
|
} else if (op === "bcurveTo") {
|
|
const _p1 = pointFrom<GlobalPoint>(data[0], data[1]);
|
|
const _p2 = pointFrom<GlobalPoint>(data[2], data[3]);
|
|
const _p3 = pointFrom<GlobalPoint>(data[4], data[5]);
|
|
|
|
const p1 = transformXY ? transformXY(_p1) : _p1;
|
|
const p2 = transformXY ? transformXY(_p2) : _p2;
|
|
const p3 = transformXY ? transformXY(_p3) : _p3;
|
|
|
|
const p0 = transformXY ? transformXY(currentP) : currentP;
|
|
currentP = _p3;
|
|
|
|
const [minX, minY, maxX, maxY] = getCubicBezierCurveBound(
|
|
p0,
|
|
p1,
|
|
p2,
|
|
p3,
|
|
);
|
|
|
|
limits.minX = Math.min(limits.minX, minX);
|
|
limits.minY = Math.min(limits.minY, minY);
|
|
|
|
limits.maxX = Math.max(limits.maxX, maxX);
|
|
limits.maxY = Math.max(limits.maxY, maxY);
|
|
} else if (op === "lineTo") {
|
|
// TODO: Implement this
|
|
} else if (op === "qcurveTo") {
|
|
// TODO: Implement this
|
|
}
|
|
return limits;
|
|
},
|
|
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
|
|
);
|
|
return [minX, minY, maxX, maxY];
|
|
};
|
|
|
|
export const getBoundsFromPoints = (
|
|
points: ExcalidrawFreeDrawElement["points"],
|
|
): Bounds => {
|
|
let minX = Infinity;
|
|
let minY = Infinity;
|
|
let maxX = -Infinity;
|
|
let maxY = -Infinity;
|
|
|
|
for (const [x, y] of points) {
|
|
minX = Math.min(minX, x);
|
|
minY = Math.min(minY, y);
|
|
maxX = Math.max(maxX, x);
|
|
maxY = Math.max(maxY, y);
|
|
}
|
|
|
|
return [minX, minY, maxX, maxY];
|
|
};
|
|
|
|
const getFreeDrawElementAbsoluteCoords = (
|
|
element: ExcalidrawFreeDrawElement,
|
|
): [number, number, number, number, number, number] => {
|
|
const [minX, minY, maxX, maxY] = getBoundsFromPoints(element.points);
|
|
const x1 = minX + element.x;
|
|
const y1 = minY + element.y;
|
|
const x2 = maxX + element.x;
|
|
const y2 = maxY + element.y;
|
|
return [x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2];
|
|
};
|
|
|
|
/** @returns number in pixels */
|
|
export const getArrowheadSize = (arrowhead: Arrowhead): number => {
|
|
switch (arrowhead) {
|
|
case "arrow":
|
|
return 25;
|
|
case "diamond":
|
|
case "diamond_outline":
|
|
return 12;
|
|
case "crowfoot_many":
|
|
case "crowfoot_one":
|
|
case "crowfoot_one_or_many":
|
|
return 20;
|
|
default:
|
|
return 15;
|
|
}
|
|
};
|
|
|
|
/** @returns number in degrees */
|
|
export const getArrowheadAngle = (arrowhead: Arrowhead): Degrees => {
|
|
switch (arrowhead) {
|
|
case "bar":
|
|
return 90 as Degrees;
|
|
case "arrow":
|
|
return 20 as Degrees;
|
|
default:
|
|
return 25 as Degrees;
|
|
}
|
|
};
|
|
|
|
export const getArrowheadPoints = (
|
|
element: ExcalidrawLinearElement,
|
|
shape: Drawable[],
|
|
position: "start" | "end",
|
|
arrowhead: Arrowhead,
|
|
) => {
|
|
if (shape.length < 1) {
|
|
return null;
|
|
}
|
|
|
|
const ops = getCurvePathOps(shape[0]);
|
|
if (ops.length < 1) {
|
|
return null;
|
|
}
|
|
|
|
// The index of the bCurve operation to examine.
|
|
const index = position === "start" ? 1 : ops.length - 1;
|
|
|
|
const data = ops[index].data;
|
|
|
|
invariant(data.length === 6, "Op data length is not 6");
|
|
|
|
const p3 = pointFrom(data[4], data[5]);
|
|
const p2 = pointFrom(data[2], data[3]);
|
|
const p1 = pointFrom(data[0], data[1]);
|
|
|
|
// We need to find p0 of the bezier curve.
|
|
// It is typically the last point of the previous
|
|
// curve; it can also be the position of moveTo operation.
|
|
const prevOp = ops[index - 1];
|
|
let p0 = pointFrom(0, 0);
|
|
if (prevOp.op === "move") {
|
|
const p = pointFromArray(prevOp.data);
|
|
invariant(p != null, "Op data is not a point");
|
|
p0 = p;
|
|
} else if (prevOp.op === "bcurveTo") {
|
|
p0 = pointFrom(prevOp.data[4], prevOp.data[5]);
|
|
}
|
|
|
|
// B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3
|
|
const equation = (t: number, idx: number) =>
|
|
Math.pow(1 - t, 3) * p3[idx] +
|
|
3 * t * Math.pow(1 - t, 2) * p2[idx] +
|
|
3 * Math.pow(t, 2) * (1 - t) * p1[idx] +
|
|
p0[idx] * Math.pow(t, 3);
|
|
|
|
// Ee know the last point of the arrow (or the first, if start arrowhead).
|
|
const [x2, y2] = position === "start" ? p0 : p3;
|
|
|
|
// By using cubic bezier equation (B(t)) and the given parameters,
|
|
// we calculate a point that is closer to the last point.
|
|
// The value 0.3 is chosen arbitrarily and it works best for all
|
|
// the tested cases.
|
|
const [x1, y1] = [equation(0.3, 0), equation(0.3, 1)];
|
|
|
|
// Find the normalized direction vector based on the
|
|
// previously calculated points.
|
|
const distance = Math.hypot(x2 - x1, y2 - y1);
|
|
const nx = (x2 - x1) / distance;
|
|
const ny = (y2 - y1) / distance;
|
|
|
|
const size = getArrowheadSize(arrowhead);
|
|
|
|
let length = 0;
|
|
|
|
{
|
|
// Length for -> arrows is based on the length of the last section
|
|
const [cx, cy] =
|
|
position === "end"
|
|
? element.points[element.points.length - 1]
|
|
: element.points[0];
|
|
const [px, py] =
|
|
element.points.length > 1
|
|
? position === "end"
|
|
? element.points[element.points.length - 2]
|
|
: element.points[1]
|
|
: [0, 0];
|
|
|
|
length = Math.hypot(cx - px, cy - py);
|
|
}
|
|
|
|
// Scale down the arrowhead until we hit a certain size so that it doesn't look weird.
|
|
// This value is selected by minimizing a minimum size with the last segment of the arrowhead
|
|
const lengthMultiplier =
|
|
arrowhead === "diamond" || arrowhead === "diamond_outline" ? 0.25 : 0.5;
|
|
const minSize = Math.min(size, length * lengthMultiplier);
|
|
const xs = x2 - nx * minSize;
|
|
const ys = y2 - ny * minSize;
|
|
|
|
if (
|
|
arrowhead === "dot" ||
|
|
arrowhead === "circle" ||
|
|
arrowhead === "circle_outline"
|
|
) {
|
|
const diameter = Math.hypot(ys - y2, xs - x2) + element.strokeWidth - 2;
|
|
return [x2, y2, diameter];
|
|
}
|
|
|
|
const angle = getArrowheadAngle(arrowhead);
|
|
|
|
if (arrowhead === "crowfoot_many" || arrowhead === "crowfoot_one_or_many") {
|
|
// swap (xs, ys) with (x2, y2)
|
|
const [x3, y3] = pointRotateRads(
|
|
pointFrom(x2, y2),
|
|
pointFrom(xs, ys),
|
|
degreesToRadians(-angle as Degrees),
|
|
);
|
|
const [x4, y4] = pointRotateRads(
|
|
pointFrom(x2, y2),
|
|
pointFrom(xs, ys),
|
|
degreesToRadians(angle),
|
|
);
|
|
return [xs, ys, x3, y3, x4, y4];
|
|
}
|
|
|
|
// Return points
|
|
const [x3, y3] = pointRotateRads(
|
|
pointFrom(xs, ys),
|
|
pointFrom(x2, y2),
|
|
((-angle * Math.PI) / 180) as Radians,
|
|
);
|
|
const [x4, y4] = pointRotateRads(
|
|
pointFrom(xs, ys),
|
|
pointFrom(x2, y2),
|
|
degreesToRadians(angle),
|
|
);
|
|
|
|
if (arrowhead === "diamond" || arrowhead === "diamond_outline") {
|
|
// point opposite to the arrowhead point
|
|
let ox;
|
|
let oy;
|
|
|
|
if (position === "start") {
|
|
const [px, py] = element.points.length > 1 ? element.points[1] : [0, 0];
|
|
|
|
[ox, oy] = pointRotateRads(
|
|
pointFrom(x2 + minSize * 2, y2),
|
|
pointFrom(x2, y2),
|
|
Math.atan2(py - y2, px - x2) as Radians,
|
|
);
|
|
} else {
|
|
const [px, py] =
|
|
element.points.length > 1
|
|
? element.points[element.points.length - 2]
|
|
: [0, 0];
|
|
|
|
[ox, oy] = pointRotateRads(
|
|
pointFrom(x2 - minSize * 2, y2),
|
|
pointFrom(x2, y2),
|
|
Math.atan2(y2 - py, x2 - px) as Radians,
|
|
);
|
|
}
|
|
|
|
return [x2, y2, x3, y3, ox, oy, x4, y4];
|
|
}
|
|
|
|
return [x2, y2, x3, y3, x4, y4];
|
|
};
|
|
|
|
const generateLinearElementShape = (
|
|
element: ExcalidrawLinearElement,
|
|
): Drawable => {
|
|
const generator = rough.generator();
|
|
const options = generateRoughOptions(element);
|
|
|
|
const method = (() => {
|
|
if (element.roundness) {
|
|
return "curve";
|
|
}
|
|
if (options.fill) {
|
|
return "polygon";
|
|
}
|
|
return "linearPath";
|
|
})();
|
|
|
|
return generator[method](
|
|
element.points as Mutable<LocalPoint>[] as RoughPoint[],
|
|
options,
|
|
);
|
|
};
|
|
|
|
const getLinearElementRotatedBounds = (
|
|
element: ExcalidrawLinearElement,
|
|
cx: number,
|
|
cy: number,
|
|
elementsMap: ElementsMap,
|
|
): Bounds => {
|
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
|
|
|
if (element.points.length < 2) {
|
|
const [pointX, pointY] = element.points[0];
|
|
const [x, y] = pointRotateRads(
|
|
pointFrom(element.x + pointX, element.y + pointY),
|
|
pointFrom(cx, cy),
|
|
element.angle,
|
|
);
|
|
|
|
let coords: Bounds = [x, y, x, y];
|
|
if (boundTextElement) {
|
|
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
|
element,
|
|
elementsMap,
|
|
[x, y, x, y],
|
|
boundTextElement,
|
|
);
|
|
coords = [
|
|
coordsWithBoundText[0],
|
|
coordsWithBoundText[1],
|
|
coordsWithBoundText[2],
|
|
coordsWithBoundText[3],
|
|
];
|
|
}
|
|
return coords;
|
|
}
|
|
|
|
// first element is always the curve
|
|
const cachedShape = ShapeCache.get(element)?.[0];
|
|
const shape = cachedShape ?? generateLinearElementShape(element);
|
|
const ops = getCurvePathOps(shape);
|
|
const transformXY = ([x, y]: GlobalPoint) =>
|
|
pointRotateRads<GlobalPoint>(
|
|
pointFrom(element.x + x, element.y + y),
|
|
pointFrom(cx, cy),
|
|
element.angle,
|
|
);
|
|
const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
|
|
let coords: Bounds = [res[0], res[1], res[2], res[3]];
|
|
if (boundTextElement) {
|
|
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
|
element,
|
|
elementsMap,
|
|
coords,
|
|
boundTextElement,
|
|
);
|
|
coords = [
|
|
coordsWithBoundText[0],
|
|
coordsWithBoundText[1],
|
|
coordsWithBoundText[2],
|
|
coordsWithBoundText[3],
|
|
];
|
|
}
|
|
return coords;
|
|
};
|
|
|
|
export const getElementBounds = (
|
|
element: ExcalidrawElement,
|
|
elementsMap: ElementsMap,
|
|
nonRotated: boolean = false,
|
|
): Bounds => {
|
|
return ElementBounds.getBounds(element, elementsMap, nonRotated);
|
|
};
|
|
|
|
export const getCommonBounds = (
|
|
elements: ElementsMapOrArray,
|
|
elementsMap?: ElementsMap,
|
|
): Bounds => {
|
|
if (!sizeOf(elements)) {
|
|
return [0, 0, 0, 0];
|
|
}
|
|
|
|
let minX = Infinity;
|
|
let maxX = -Infinity;
|
|
let minY = Infinity;
|
|
let maxY = -Infinity;
|
|
|
|
const _elementsMap = elementsMap || arrayToMap(elements);
|
|
|
|
elements.forEach((element) => {
|
|
const [x1, y1, x2, y2] = getElementBounds(element, _elementsMap);
|
|
minX = Math.min(minX, x1);
|
|
minY = Math.min(minY, y1);
|
|
maxX = Math.max(maxX, x2);
|
|
maxY = Math.max(maxY, y2);
|
|
});
|
|
|
|
return [minX, minY, maxX, maxY];
|
|
};
|
|
|
|
export const getDraggedElementsBounds = (
|
|
elements: ExcalidrawElement[],
|
|
dragOffset: { x: number; y: number },
|
|
) => {
|
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
|
return [
|
|
minX + dragOffset.x,
|
|
minY + dragOffset.y,
|
|
maxX + dragOffset.x,
|
|
maxY + dragOffset.y,
|
|
];
|
|
};
|
|
|
|
export const getResizedElementAbsoluteCoords = (
|
|
element: ExcalidrawElement,
|
|
nextWidth: number,
|
|
nextHeight: number,
|
|
normalizePoints: boolean,
|
|
): Bounds => {
|
|
if (!(isLinearElement(element) || isFreeDrawElement(element))) {
|
|
return [
|
|
element.x,
|
|
element.y,
|
|
element.x + nextWidth,
|
|
element.y + nextHeight,
|
|
];
|
|
}
|
|
|
|
const points = rescalePoints(
|
|
0,
|
|
nextWidth,
|
|
rescalePoints(1, nextHeight, element.points, normalizePoints),
|
|
normalizePoints,
|
|
);
|
|
|
|
let bounds: Bounds;
|
|
|
|
if (isFreeDrawElement(element)) {
|
|
// Free Draw
|
|
bounds = getBoundsFromPoints(points);
|
|
} else {
|
|
// Line
|
|
const gen = rough.generator();
|
|
const curve = !element.roundness
|
|
? gen.linearPath(
|
|
points as [number, number][],
|
|
generateRoughOptions(element),
|
|
)
|
|
: gen.curve(points as [number, number][], generateRoughOptions(element));
|
|
|
|
const ops = getCurvePathOps(curve);
|
|
bounds = getMinMaxXYFromCurvePathOps(ops);
|
|
}
|
|
|
|
const [minX, minY, maxX, maxY] = bounds;
|
|
return [
|
|
minX + element.x,
|
|
minY + element.y,
|
|
maxX + element.x,
|
|
maxY + element.y,
|
|
];
|
|
};
|
|
|
|
export const getElementPointsCoords = (
|
|
element: ExcalidrawLinearElement,
|
|
points: readonly (readonly [number, number])[],
|
|
): Bounds => {
|
|
// This might be computationally heavey
|
|
const gen = rough.generator();
|
|
const curve =
|
|
element.roundness == null
|
|
? gen.linearPath(
|
|
points as [number, number][],
|
|
generateRoughOptions(element),
|
|
)
|
|
: gen.curve(points as [number, number][], generateRoughOptions(element));
|
|
const ops = getCurvePathOps(curve);
|
|
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
|
|
return [
|
|
minX + element.x,
|
|
minY + element.y,
|
|
maxX + element.x,
|
|
maxY + element.y,
|
|
];
|
|
};
|
|
|
|
export const getClosestElementBounds = (
|
|
elements: readonly ExcalidrawElement[],
|
|
from: { x: number; y: number },
|
|
): Bounds => {
|
|
if (!elements.length) {
|
|
return [0, 0, 0, 0];
|
|
}
|
|
|
|
let minDistance = Infinity;
|
|
let closestElement = elements[0];
|
|
const elementsMap = arrayToMap(elements);
|
|
elements.forEach((element) => {
|
|
const [x1, y1, x2, y2] = getElementBounds(element, elementsMap);
|
|
const distance = pointDistance(
|
|
pointFrom((x1 + x2) / 2, (y1 + y2) / 2),
|
|
pointFrom(from.x, from.y),
|
|
);
|
|
|
|
if (distance < minDistance) {
|
|
minDistance = distance;
|
|
closestElement = element;
|
|
}
|
|
});
|
|
|
|
return getElementBounds(closestElement, elementsMap);
|
|
};
|
|
|
|
export interface BoundingBox {
|
|
minX: number;
|
|
minY: number;
|
|
maxX: number;
|
|
maxY: number;
|
|
midX: number;
|
|
midY: number;
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
export const getCommonBoundingBox = (
|
|
elements:
|
|
| readonly ExcalidrawElement[]
|
|
| readonly NonDeleted<ExcalidrawElement>[],
|
|
): BoundingBox => {
|
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
|
return {
|
|
minX,
|
|
minY,
|
|
maxX,
|
|
maxY,
|
|
width: maxX - minX,
|
|
height: maxY - minY,
|
|
midX: (minX + maxX) / 2,
|
|
midY: (minY + maxY) / 2,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* returns scene coords of user's editor viewport (visible canvas area) bounds
|
|
*/
|
|
export const getVisibleSceneBounds = ({
|
|
scrollX,
|
|
scrollY,
|
|
width,
|
|
height,
|
|
zoom,
|
|
}: AppState): SceneBounds => {
|
|
return [
|
|
-scrollX,
|
|
-scrollY,
|
|
-scrollX + width / zoom.value,
|
|
-scrollY + height / zoom.value,
|
|
];
|
|
};
|
|
|
|
export const getCenterForBounds = (bounds: Bounds): GlobalPoint =>
|
|
pointFrom(
|
|
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
|
bounds[1] + (bounds[3] - bounds[1]) / 2,
|
|
);
|
|
|
|
/**
|
|
* Get the axis-aligned bounding box for a given element
|
|
*/
|
|
export const aabbForElement = (
|
|
element: Readonly<ExcalidrawElement>,
|
|
elementsMap: ElementsMap,
|
|
offset?: [number, number, number, number],
|
|
) => {
|
|
const bbox = {
|
|
minX: element.x,
|
|
minY: element.y,
|
|
maxX: element.x + element.width,
|
|
maxY: element.y + element.height,
|
|
midX: element.x + element.width / 2,
|
|
midY: element.y + element.height / 2,
|
|
};
|
|
|
|
const center = elementCenterPoint(element, elementsMap);
|
|
const [topLeftX, topLeftY] = pointRotateRads(
|
|
pointFrom(bbox.minX, bbox.minY),
|
|
center,
|
|
element.angle,
|
|
);
|
|
const [topRightX, topRightY] = pointRotateRads(
|
|
pointFrom(bbox.maxX, bbox.minY),
|
|
center,
|
|
element.angle,
|
|
);
|
|
const [bottomRightX, bottomRightY] = pointRotateRads(
|
|
pointFrom(bbox.maxX, bbox.maxY),
|
|
center,
|
|
element.angle,
|
|
);
|
|
const [bottomLeftX, bottomLeftY] = pointRotateRads(
|
|
pointFrom(bbox.minX, bbox.maxY),
|
|
center,
|
|
element.angle,
|
|
);
|
|
|
|
const bounds = [
|
|
Math.min(topLeftX, topRightX, bottomRightX, bottomLeftX),
|
|
Math.min(topLeftY, topRightY, bottomRightY, bottomLeftY),
|
|
Math.max(topLeftX, topRightX, bottomRightX, bottomLeftX),
|
|
Math.max(topLeftY, topRightY, bottomRightY, bottomLeftY),
|
|
] as Bounds;
|
|
|
|
if (offset) {
|
|
const [topOffset, rightOffset, downOffset, leftOffset] = offset;
|
|
return [
|
|
bounds[0] - leftOffset,
|
|
bounds[1] - topOffset,
|
|
bounds[2] + rightOffset,
|
|
bounds[3] + downOffset,
|
|
] as Bounds;
|
|
}
|
|
|
|
return bounds;
|
|
};
|
|
|
|
export const pointInsideBounds = <P extends GlobalPoint | LocalPoint>(
|
|
p: P,
|
|
bounds: Bounds,
|
|
): boolean =>
|
|
p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3];
|
|
|
|
export const doBoundsIntersect = (
|
|
bounds1: Bounds | null,
|
|
bounds2: Bounds | null,
|
|
): boolean => {
|
|
if (bounds1 == null || bounds2 == null) {
|
|
return false;
|
|
}
|
|
|
|
const [minX1, minY1, maxX1, maxY1] = bounds1;
|
|
const [minX2, minY2, maxX2, maxY2] = bounds2;
|
|
|
|
return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2;
|
|
};
|
|
|
|
export const elementCenterPoint = (
|
|
element: ExcalidrawElement,
|
|
elementsMap: ElementsMap,
|
|
xOffset: number = 0,
|
|
yOffset: number = 0,
|
|
) => {
|
|
if (isLinearElement(element)) {
|
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
|
const [x, y] = pointFrom<GlobalPoint>((x1 + x2) / 2, (y1 + y2) / 2);
|
|
|
|
return pointFrom<GlobalPoint>(x + xOffset, y + yOffset);
|
|
}
|
|
|
|
const [x, y] = getCenterForBounds(getElementBounds(element, elementsMap));
|
|
|
|
return pointFrom<GlobalPoint>(x + xOffset, y + yOffset);
|
|
};
|