feat: Non-elbow arrow snapping and behavior changes (#9670)
* 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>
This commit is contained in:
@@ -15,7 +15,6 @@ import {
|
||||
getGlobalFixedPointForBindableElement,
|
||||
isArrowElement,
|
||||
isBindableElement,
|
||||
isFixedPointBinding,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
@@ -35,7 +34,6 @@ import type {
|
||||
ExcalidrawBindableElement,
|
||||
FixedPointBinding,
|
||||
OrderedExcalidrawElement,
|
||||
PointBinding,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import { STORAGE_KEYS } from "../app_constants";
|
||||
@@ -91,48 +89,46 @@ const renderOrigin = (context: CanvasRenderingContext2D, zoom: number) => {
|
||||
|
||||
const _renderBinding = (
|
||||
context: CanvasRenderingContext2D,
|
||||
binding: FixedPointBinding | PointBinding,
|
||||
binding: FixedPointBinding,
|
||||
elementsMap: ElementsMap,
|
||||
zoom: number,
|
||||
width: number,
|
||||
height: number,
|
||||
color: string,
|
||||
) => {
|
||||
if (isFixedPointBinding(binding)) {
|
||||
if (!binding.fixedPoint) {
|
||||
console.warn("Binding must have a fixedPoint");
|
||||
return;
|
||||
}
|
||||
|
||||
const bindable = elementsMap.get(
|
||||
binding.elementId,
|
||||
) as ExcalidrawBindableElement;
|
||||
const [x, y] = getGlobalFixedPointForBindableElement(
|
||||
binding.fixedPoint,
|
||||
bindable,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
context.save();
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = 1;
|
||||
context.beginPath();
|
||||
context.moveTo(x * zoom, y * zoom);
|
||||
context.bezierCurveTo(
|
||||
x * zoom - width,
|
||||
y * zoom - height,
|
||||
x * zoom - width,
|
||||
y * zoom + height,
|
||||
x * zoom,
|
||||
y * zoom,
|
||||
);
|
||||
context.stroke();
|
||||
context.restore();
|
||||
if (!binding.fixedPoint) {
|
||||
console.warn("Binding must have a fixedPoint");
|
||||
return;
|
||||
}
|
||||
|
||||
const bindable = elementsMap.get(
|
||||
binding.elementId,
|
||||
) as ExcalidrawBindableElement;
|
||||
const [x, y] = getGlobalFixedPointForBindableElement(
|
||||
binding.fixedPoint,
|
||||
bindable,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
context.save();
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = 1;
|
||||
context.beginPath();
|
||||
context.moveTo(x * zoom, y * zoom);
|
||||
context.bezierCurveTo(
|
||||
x * zoom - width,
|
||||
y * zoom - height,
|
||||
x * zoom - width,
|
||||
y * zoom + height,
|
||||
x * zoom,
|
||||
y * zoom,
|
||||
);
|
||||
context.stroke();
|
||||
context.restore();
|
||||
};
|
||||
|
||||
const _renderBindableBinding = (
|
||||
binding: FixedPointBinding | PointBinding,
|
||||
binding: FixedPointBinding,
|
||||
context: CanvasRenderingContext2D,
|
||||
elementsMap: ElementsMap,
|
||||
zoom: number,
|
||||
@@ -140,37 +136,35 @@ const _renderBindableBinding = (
|
||||
height: number,
|
||||
color: string,
|
||||
) => {
|
||||
if (isFixedPointBinding(binding)) {
|
||||
const bindable = elementsMap.get(
|
||||
binding.elementId,
|
||||
) as ExcalidrawBindableElement;
|
||||
if (!binding.fixedPoint) {
|
||||
console.warn("Binding must have a fixedPoint");
|
||||
return;
|
||||
}
|
||||
|
||||
const [x, y] = getGlobalFixedPointForBindableElement(
|
||||
binding.fixedPoint,
|
||||
bindable,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
context.save();
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = 1;
|
||||
context.beginPath();
|
||||
context.moveTo(x * zoom, y * zoom);
|
||||
context.bezierCurveTo(
|
||||
x * zoom + width,
|
||||
y * zoom + height,
|
||||
x * zoom + width,
|
||||
y * zoom - height,
|
||||
x * zoom,
|
||||
y * zoom,
|
||||
);
|
||||
context.stroke();
|
||||
context.restore();
|
||||
const bindable = elementsMap.get(
|
||||
binding.elementId,
|
||||
) as ExcalidrawBindableElement;
|
||||
if (!binding.fixedPoint) {
|
||||
console.warn("Binding must have a fixedPoint");
|
||||
return;
|
||||
}
|
||||
|
||||
const [x, y] = getGlobalFixedPointForBindableElement(
|
||||
binding.fixedPoint,
|
||||
bindable,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
context.save();
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = 1;
|
||||
context.beginPath();
|
||||
context.moveTo(x * zoom, y * zoom);
|
||||
context.bezierCurveTo(
|
||||
x * zoom + width,
|
||||
y * zoom + height,
|
||||
x * zoom + width,
|
||||
y * zoom - height,
|
||||
x * zoom,
|
||||
y * zoom,
|
||||
);
|
||||
context.stroke();
|
||||
context.restore();
|
||||
};
|
||||
|
||||
const renderBindings = (
|
||||
@@ -197,12 +191,12 @@ const renderBindings = (
|
||||
|
||||
_renderBinding(
|
||||
context,
|
||||
element.startBinding as FixedPointBinding,
|
||||
element.startBinding,
|
||||
elementsMap,
|
||||
zoom,
|
||||
dim,
|
||||
dim,
|
||||
"red",
|
||||
element.startBinding?.mode === "orbit" ? "red" : "black",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -221,7 +215,7 @@ const renderBindings = (
|
||||
zoom,
|
||||
dim,
|
||||
dim,
|
||||
"red",
|
||||
element.endBinding?.mode === "orbit" ? "red" : "black",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
DEFAULT_SIDEBAR,
|
||||
debounce,
|
||||
} from "@excalidraw/common";
|
||||
import { clearElementsForLocalStorage } from "@excalidraw/element";
|
||||
import {
|
||||
createStore,
|
||||
entries,
|
||||
@@ -28,6 +27,7 @@ import {
|
||||
} from "idb-keyval";
|
||||
|
||||
import { appJotaiStore, atom } from "excalidraw-app/app-jotai";
|
||||
import { getNonDeletedElements } from "@excalidraw/element";
|
||||
|
||||
import type { LibraryPersistedData } from "@excalidraw/excalidraw/data/library";
|
||||
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
||||
@@ -88,7 +88,7 @@ const saveDataStateToLocalStorage = (
|
||||
|
||||
localStorage.setItem(
|
||||
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
|
||||
JSON.stringify(clearElementsForLocalStorage(elements)),
|
||||
JSON.stringify(getNonDeletedElements(elements)),
|
||||
);
|
||||
localStorage.setItem(
|
||||
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
clearAppStateForLocalStorage,
|
||||
getDefaultAppState,
|
||||
} from "@excalidraw/excalidraw/appState";
|
||||
import { clearElementsForLocalStorage } from "@excalidraw/element";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
@@ -50,7 +49,7 @@ export const importFromLocalStorage = () => {
|
||||
let elements: ExcalidrawElement[] = [];
|
||||
if (savedElements) {
|
||||
try {
|
||||
elements = clearElementsForLocalStorage(JSON.parse(savedElements));
|
||||
elements = JSON.parse(savedElements);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
// Do nothing because elements array is already empty
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getFeatureFlag } from "@excalidraw/common";
|
||||
import * as Sentry from "@sentry/browser";
|
||||
import callsites from "callsites";
|
||||
|
||||
@@ -33,6 +34,7 @@ Sentry.init({
|
||||
Sentry.captureConsoleIntegration({
|
||||
levels: ["error"],
|
||||
}),
|
||||
Sentry.featureFlagsIntegration(),
|
||||
],
|
||||
beforeSend(event) {
|
||||
if (event.request?.url) {
|
||||
@@ -79,3 +81,14 @@ Sentry.init({
|
||||
return event;
|
||||
},
|
||||
});
|
||||
|
||||
const flagsIntegration =
|
||||
Sentry.getClient()?.getIntegrationByName<Sentry.FeatureFlagsIntegration>(
|
||||
"FeatureFlags",
|
||||
);
|
||||
if (flagsIntegration) {
|
||||
flagsIntegration.addFeatureFlag(
|
||||
"COMPLEX_BINDINGS",
|
||||
getFeatureFlag("COMPLEX_BINDINGS"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -499,6 +499,8 @@ export const LINE_POLYGON_POINT_MERGE_DISTANCE = 20;
|
||||
|
||||
export const DOUBLE_TAP_POSITION_THRESHOLD = 35;
|
||||
|
||||
export const BIND_MODE_TIMEOUT = 700; // ms
|
||||
|
||||
// glass background for mobile action buttons
|
||||
export const MOBILE_ACTION_BUTTON_BG = {
|
||||
background: "var(--mobile-action-button-bg)",
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { average } from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
ExcalidrawBindableElement,
|
||||
FontFamilyValues,
|
||||
FontString,
|
||||
} from "@excalidraw/element/types";
|
||||
import type { FontFamilyValues, FontString } from "@excalidraw/element/types";
|
||||
|
||||
import type {
|
||||
ActiveTool,
|
||||
@@ -382,6 +378,10 @@ export const removeSelection = () => {
|
||||
|
||||
export const distance = (x: number, y: number) => Math.abs(x - y);
|
||||
|
||||
export const isSelectionLikeTool = (type: ToolType | "custom") => {
|
||||
return type === "selection" || type === "lasso";
|
||||
};
|
||||
|
||||
export const updateActiveTool = (
|
||||
appState: Pick<AppState, "activeTool">,
|
||||
data: ((
|
||||
@@ -558,9 +558,6 @@ export const isTransparent = (color: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const isBindingFallthroughEnabled = (el: ExcalidrawBindableElement) =>
|
||||
el.fillStyle !== "solid" || isTransparent(el.backgroundColor);
|
||||
|
||||
export type ResolvablePromise<T> = Promise<T> & {
|
||||
resolve: [T] extends [undefined]
|
||||
? (value?: MaybePromise<Awaited<T>>) => void
|
||||
@@ -1270,3 +1267,47 @@ export const reduceToCommonValue = <T, R = T>(
|
||||
|
||||
return commonValue;
|
||||
};
|
||||
|
||||
type FEATURE_FLAGS = {
|
||||
COMPLEX_BINDINGS: boolean;
|
||||
};
|
||||
|
||||
const FEATURE_FLAGS_STORAGE_KEY = "excalidraw-feature-flags";
|
||||
const DEFAULT_FEATURE_FLAGS: FEATURE_FLAGS = {
|
||||
COMPLEX_BINDINGS: false,
|
||||
};
|
||||
let featureFlags: FEATURE_FLAGS | null = null;
|
||||
|
||||
export const getFeatureFlag = <F extends keyof FEATURE_FLAGS>(
|
||||
flag: F,
|
||||
): FEATURE_FLAGS[F] => {
|
||||
if (!featureFlags) {
|
||||
try {
|
||||
const serializedFlags = localStorage.getItem(FEATURE_FLAGS_STORAGE_KEY);
|
||||
if (serializedFlags) {
|
||||
const flags = JSON.parse(serializedFlags);
|
||||
featureFlags = flags ?? DEFAULT_FEATURE_FLAGS;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return (featureFlags || DEFAULT_FEATURE_FLAGS)[flag];
|
||||
};
|
||||
|
||||
export const setFeatureFlag = <F extends keyof FEATURE_FLAGS>(
|
||||
flag: F,
|
||||
value: FEATURE_FLAGS[F],
|
||||
) => {
|
||||
try {
|
||||
featureFlags = {
|
||||
...(featureFlags || DEFAULT_FEATURE_FLAGS),
|
||||
[flag]: value,
|
||||
};
|
||||
localStorage.setItem(
|
||||
FEATURE_FLAGS_STORAGE_KEY,
|
||||
JSON.stringify(featureFlags),
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("unable to set feature flag", e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -139,7 +139,7 @@ export const debugDrawPoints = (
|
||||
}: {
|
||||
x: number;
|
||||
y: number;
|
||||
points: LocalPoint[];
|
||||
points: readonly LocalPoint[];
|
||||
},
|
||||
options?: any,
|
||||
) => {
|
||||
|
||||
+1215
-1231
File diff suppressed because it is too large
Load Diff
@@ -1276,6 +1276,13 @@ export const elementCenterPoint = (
|
||||
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);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isTransparent } from "@excalidraw/common";
|
||||
import { invariant, isTransparent } from "@excalidraw/common";
|
||||
import {
|
||||
curveIntersectLineSegment,
|
||||
isPointWithinBounds,
|
||||
@@ -34,10 +34,14 @@ import {
|
||||
elementCenterPoint,
|
||||
getCenterForBounds,
|
||||
getCubicBezierCurveBound,
|
||||
getDiamondPoints,
|
||||
getElementBounds,
|
||||
pointInsideBounds,
|
||||
} from "./bounds";
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isBindableElement,
|
||||
isFrameLikeElement,
|
||||
isFreeDrawElement,
|
||||
isIframeLikeElement,
|
||||
isImageElement,
|
||||
@@ -58,12 +62,17 @@ import { distanceToElement } from "./distance";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawEllipseElement,
|
||||
ExcalidrawFreeDrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawRectanguloidElement,
|
||||
NonDeleted,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
Ordered,
|
||||
} from "./types";
|
||||
|
||||
export const shouldTestInside = (element: ExcalidrawElement) => {
|
||||
@@ -94,6 +103,7 @@ export type HitTestArgs = {
|
||||
threshold: number;
|
||||
elementsMap: ElementsMap;
|
||||
frameNameBound?: FrameNameBounds | null;
|
||||
overrideShouldTestInside?: boolean;
|
||||
};
|
||||
|
||||
export const hitElementItself = ({
|
||||
@@ -102,6 +112,7 @@ export const hitElementItself = ({
|
||||
threshold,
|
||||
elementsMap,
|
||||
frameNameBound = null,
|
||||
overrideShouldTestInside = false,
|
||||
}: HitTestArgs) => {
|
||||
// Hit test against a frame's name
|
||||
const hitFrameName = frameNameBound
|
||||
@@ -134,7 +145,9 @@ export const hitElementItself = ({
|
||||
}
|
||||
|
||||
// Do the precise (and relatively costly) hit test
|
||||
const hitElement = shouldTestInside(element)
|
||||
const hitElement = (
|
||||
overrideShouldTestInside ? true : shouldTestInside(element)
|
||||
)
|
||||
? // Since `inShape` tests STRICTLY againt the insides of a shape
|
||||
// we would need `onShape` as well to include the "borders"
|
||||
isPointInElement(point, element, elementsMap) ||
|
||||
@@ -193,6 +206,116 @@ export const hitElementBoundText = (
|
||||
return isPointInElement(point, boundTextElement, elementsMap);
|
||||
};
|
||||
|
||||
const bindingBorderTest = (
|
||||
element: NonDeleted<ExcalidrawBindableElement>,
|
||||
[x, y]: Readonly<GlobalPoint>,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
tolerance: number = 0,
|
||||
): boolean => {
|
||||
const p = pointFrom<GlobalPoint>(x, y);
|
||||
const shouldTestInside =
|
||||
// disable fullshape snapping for frame elements so we
|
||||
// can bind to frame children
|
||||
!isFrameLikeElement(element);
|
||||
|
||||
// PERF: Run a cheap test to see if the binding element
|
||||
// is even close to the element
|
||||
const t = Math.max(1, tolerance);
|
||||
const bounds = [x - t, y - t, x + t, y + t] as Bounds;
|
||||
const elementBounds = getElementBounds(element, elementsMap);
|
||||
if (!doBoundsIntersect(bounds, elementBounds)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the element is inside a frame, we should clip the element
|
||||
if (element.frameId) {
|
||||
const enclosingFrame = elementsMap.get(element.frameId);
|
||||
if (enclosingFrame && isFrameLikeElement(enclosingFrame)) {
|
||||
const enclosingFrameBounds = getElementBounds(
|
||||
enclosingFrame,
|
||||
elementsMap,
|
||||
);
|
||||
if (!pointInsideBounds(p, enclosingFrameBounds)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do the intersection test against the element since it's close enough
|
||||
const intersections = intersectElementWithLineSegment(
|
||||
element,
|
||||
elementsMap,
|
||||
lineSegment(elementCenterPoint(element, elementsMap), p),
|
||||
);
|
||||
const distance = distanceToElement(element, elementsMap, p);
|
||||
|
||||
return shouldTestInside
|
||||
? intersections.length === 0 || distance <= tolerance
|
||||
: intersections.length > 0 && distance <= t;
|
||||
};
|
||||
|
||||
export const getAllHoveredElementAtPoint = (
|
||||
point: Readonly<GlobalPoint>,
|
||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
toleranceFn?: (element: ExcalidrawBindableElement) => number,
|
||||
): NonDeleted<ExcalidrawBindableElement>[] => {
|
||||
const candidateElements: NonDeleted<ExcalidrawBindableElement>[] = [];
|
||||
// We need to to hit testing from front (end of the array) to back (beginning of the array)
|
||||
// because array is ordered from lower z-index to highest and we want element z-index
|
||||
// with higher z-index
|
||||
for (let index = elements.length - 1; index >= 0; --index) {
|
||||
const element = elements[index];
|
||||
|
||||
invariant(
|
||||
!element.isDeleted,
|
||||
"Elements in the function parameter for getAllElementsAtPositionForBinding() should not contain deleted elements",
|
||||
);
|
||||
|
||||
if (
|
||||
isBindableElement(element, false) &&
|
||||
bindingBorderTest(element, point, elementsMap, toleranceFn?.(element))
|
||||
) {
|
||||
candidateElements.push(element);
|
||||
|
||||
if (!isTransparent(element.backgroundColor)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return candidateElements;
|
||||
};
|
||||
|
||||
export const getHoveredElementForBinding = (
|
||||
point: Readonly<GlobalPoint>,
|
||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
toleranceFn?: (element: ExcalidrawBindableElement) => number,
|
||||
): NonDeleted<ExcalidrawBindableElement> | null => {
|
||||
const candidateElements = getAllHoveredElementAtPoint(
|
||||
point,
|
||||
elements,
|
||||
elementsMap,
|
||||
toleranceFn,
|
||||
);
|
||||
|
||||
if (!candidateElements || candidateElements.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (candidateElements.length === 1) {
|
||||
return candidateElements[0];
|
||||
}
|
||||
|
||||
// Prefer smaller shapes
|
||||
return candidateElements
|
||||
.sort(
|
||||
(a, b) => b.width ** 2 + b.height ** 2 - (a.width ** 2 + a.height ** 2),
|
||||
)
|
||||
.pop() as NonDeleted<ExcalidrawBindableElement>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Intersect a line with an element for binding test
|
||||
*
|
||||
@@ -554,3 +677,61 @@ export const isPointInElement = (
|
||||
|
||||
return intersections.length % 2 === 1;
|
||||
};
|
||||
|
||||
export const isBindableElementInsideOtherBindable = (
|
||||
innerElement: ExcalidrawBindableElement,
|
||||
outerElement: ExcalidrawBindableElement,
|
||||
elementsMap: ElementsMap,
|
||||
): boolean => {
|
||||
// Get corner points of the inner element based on its type
|
||||
const getCornerPoints = (
|
||||
element: ExcalidrawElement,
|
||||
offset: number,
|
||||
): GlobalPoint[] => {
|
||||
const { x, y, width, height, angle } = element;
|
||||
const center = elementCenterPoint(element, elementsMap);
|
||||
|
||||
if (element.type === "diamond") {
|
||||
// Diamond has 4 corner points at the middle of each side
|
||||
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
||||
getDiamondPoints(element);
|
||||
const corners: GlobalPoint[] = [
|
||||
pointFrom(x + topX, y + topY - offset), // top
|
||||
pointFrom(x + rightX + offset, y + rightY), // right
|
||||
pointFrom(x + bottomX, y + bottomY + offset), // bottom
|
||||
pointFrom(x + leftX - offset, y + leftY), // left
|
||||
];
|
||||
return corners.map((corner) => pointRotateRads(corner, center, angle));
|
||||
}
|
||||
if (element.type === "ellipse") {
|
||||
// For ellipse, test points at the extremes (top, right, bottom, left)
|
||||
const cx = x + width / 2;
|
||||
const cy = y + height / 2;
|
||||
const rx = width / 2;
|
||||
const ry = height / 2;
|
||||
const corners: GlobalPoint[] = [
|
||||
pointFrom(cx, cy - ry - offset), // top
|
||||
pointFrom(cx + rx + offset, cy), // right
|
||||
pointFrom(cx, cy + ry + offset), // bottom
|
||||
pointFrom(cx - rx - offset, cy), // left
|
||||
];
|
||||
return corners.map((corner) => pointRotateRads(corner, center, angle));
|
||||
}
|
||||
// Rectangle and other rectangular shapes (image, text, etc.)
|
||||
const corners: GlobalPoint[] = [
|
||||
pointFrom(x - offset, y - offset), // top-left
|
||||
pointFrom(x + width + offset, y - offset), // top-right
|
||||
pointFrom(x + width + offset, y + height + offset), // bottom-right
|
||||
pointFrom(x - offset, y + height + offset), // bottom-left
|
||||
];
|
||||
return corners.map((corner) => pointRotateRads(corner, center, angle));
|
||||
};
|
||||
|
||||
const offset = (-1 * Math.max(innerElement.width, innerElement.height)) / 20; // 5% offset
|
||||
const innerCorners = getCornerPoints(innerElement, offset);
|
||||
|
||||
// Check if all corner points of the inner element are inside the outer element
|
||||
return innerCorners.every((corner) =>
|
||||
isPointInElement(corner, outerElement, elementsMap),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
TEXT_AUTOWRAP_THRESHOLD,
|
||||
getGridPoint,
|
||||
getFontString,
|
||||
DRAGGING_THRESHOLD,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import type {
|
||||
@@ -13,7 +14,7 @@ import type {
|
||||
|
||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import { updateBoundElements } from "./binding";
|
||||
import { unbindBindingElement, updateBoundElements } from "./binding";
|
||||
import { getCommonBounds } from "./bounds";
|
||||
import { getPerfectElementSize } from "./sizeHelpers";
|
||||
import { getBoundTextElement } from "./textElement";
|
||||
@@ -102,9 +103,26 @@ export const dragSelectedElements = (
|
||||
gridSize,
|
||||
);
|
||||
|
||||
const elementsToUpdateIds = new Set(
|
||||
Array.from(elementsToUpdate, (el) => el.id),
|
||||
);
|
||||
|
||||
elementsToUpdate.forEach((element) => {
|
||||
updateElementCoords(pointerDownState, element, scene, adjustedOffset);
|
||||
const isArrow = !isArrowElement(element);
|
||||
const isStartBoundElementSelected =
|
||||
isArrow ||
|
||||
(element.startBinding
|
||||
? elementsToUpdateIds.has(element.startBinding.elementId)
|
||||
: false);
|
||||
const isEndBoundElementSelected =
|
||||
isArrow ||
|
||||
(element.endBinding
|
||||
? elementsToUpdateIds.has(element.endBinding.elementId)
|
||||
: false);
|
||||
|
||||
if (!isArrowElement(element)) {
|
||||
updateElementCoords(pointerDownState, element, scene, adjustedOffset);
|
||||
|
||||
// skip arrow labels since we calculate its position during render
|
||||
const textElement = getBoundTextElement(
|
||||
element,
|
||||
@@ -121,6 +139,33 @@ export const dragSelectedElements = (
|
||||
updateBoundElements(element, scene, {
|
||||
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
||||
});
|
||||
} else if (
|
||||
// NOTE: Add a little initial drag to the arrow dragging when the arrow
|
||||
// is the single element being dragged to avoid accidentally unbinding
|
||||
// the arrow when the user just wants to select it.
|
||||
|
||||
elementsToUpdate.size > 1 ||
|
||||
Math.max(Math.abs(adjustedOffset.x), Math.abs(adjustedOffset.y)) >
|
||||
DRAGGING_THRESHOLD ||
|
||||
(!element.startBinding && !element.endBinding)
|
||||
) {
|
||||
updateElementCoords(pointerDownState, element, scene, adjustedOffset);
|
||||
|
||||
const shouldUnbindStart =
|
||||
element.startBinding && !isStartBoundElementSelected;
|
||||
const shouldUnbindEnd = element.endBinding && !isEndBoundElementSelected;
|
||||
if (shouldUnbindStart || shouldUnbindEnd) {
|
||||
// NOTE: Moving the bound arrow should unbind it, otherwise we would
|
||||
// have weird situations, like 0 lenght arrow when the user moves
|
||||
// the arrow outside a filled shape suddenly forcing the arrow start
|
||||
// and end point to jump "outside" the shape.
|
||||
if (shouldUnbindStart) {
|
||||
unbindBindingElement(element, "start", scene);
|
||||
}
|
||||
if (shouldUnbindEnd) {
|
||||
unbindBindingElement(element, "end", scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
BinaryHeap,
|
||||
invariant,
|
||||
isAnyTrue,
|
||||
tupleToCoors,
|
||||
getSizeFromPoints,
|
||||
isDevEnv,
|
||||
arrayToMap,
|
||||
@@ -27,10 +26,11 @@ import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import {
|
||||
bindPointToSnapToElementOutline,
|
||||
FIXED_BINDING_DISTANCE,
|
||||
getHeadingForElbowArrowSnap,
|
||||
getGlobalFixedPointForBindableElement,
|
||||
getHoveredElementForBinding,
|
||||
getBindingGap,
|
||||
maxBindingDistance_simple,
|
||||
BASE_BINDING_GAP_ELBOW,
|
||||
} from "./binding";
|
||||
import { distanceToElement } from "./distance";
|
||||
import {
|
||||
@@ -51,8 +51,8 @@ import {
|
||||
type ExcalidrawElbowArrowElement,
|
||||
type NonDeletedSceneElementsMap,
|
||||
} from "./types";
|
||||
|
||||
import { aabbForElement, pointInsideBounds } from "./bounds";
|
||||
import { getHoveredElementForBinding } from "./collision";
|
||||
|
||||
import type { Bounds } from "./bounds";
|
||||
import type { Heading } from "./heading";
|
||||
@@ -63,6 +63,7 @@ import type {
|
||||
FixedPointBinding,
|
||||
FixedSegment,
|
||||
NonDeletedExcalidrawElement,
|
||||
Ordered,
|
||||
} from "./types";
|
||||
|
||||
type GridAddress = [number, number] & { _brand: "gridaddress" };
|
||||
@@ -1243,6 +1244,7 @@ const getElbowArrowData = (
|
||||
const startGlobalPoint = getGlobalPoint(
|
||||
{
|
||||
...arrow,
|
||||
angle: 0,
|
||||
type: "arrow",
|
||||
elbowed: true,
|
||||
points: nextPoints,
|
||||
@@ -1257,6 +1259,7 @@ const getElbowArrowData = (
|
||||
const endGlobalPoint = getGlobalPoint(
|
||||
{
|
||||
...arrow,
|
||||
angle: 0,
|
||||
type: "arrow",
|
||||
elbowed: true,
|
||||
points: nextPoints,
|
||||
@@ -1274,6 +1277,7 @@ const getElbowArrowData = (
|
||||
hoveredStartElement,
|
||||
origStartGlobalPoint,
|
||||
elementsMap,
|
||||
options?.zoom,
|
||||
);
|
||||
const endHeading = getBindPointHeading(
|
||||
endGlobalPoint,
|
||||
@@ -1281,6 +1285,7 @@ const getElbowArrowData = (
|
||||
hoveredEndElement,
|
||||
origEndGlobalPoint,
|
||||
elementsMap,
|
||||
options?.zoom,
|
||||
);
|
||||
const startPointBounds = [
|
||||
startGlobalPoint[0] - 2,
|
||||
@@ -1301,8 +1306,8 @@ const getElbowArrowData = (
|
||||
offsetFromHeading(
|
||||
startHeading,
|
||||
arrow.startArrowhead
|
||||
? FIXED_BINDING_DISTANCE * 6
|
||||
: FIXED_BINDING_DISTANCE * 2,
|
||||
? getBindingGap(hoveredStartElement, { elbowed: true }) * 6
|
||||
: getBindingGap(hoveredStartElement, { elbowed: true }) * 2,
|
||||
1,
|
||||
),
|
||||
)
|
||||
@@ -1314,8 +1319,8 @@ const getElbowArrowData = (
|
||||
offsetFromHeading(
|
||||
endHeading,
|
||||
arrow.endArrowhead
|
||||
? FIXED_BINDING_DISTANCE * 6
|
||||
: FIXED_BINDING_DISTANCE * 2,
|
||||
? getBindingGap(hoveredEndElement, { elbowed: true }) * 6
|
||||
: getBindingGap(hoveredEndElement, { elbowed: true }) * 2,
|
||||
1,
|
||||
),
|
||||
)
|
||||
@@ -1362,8 +1367,8 @@ const getElbowArrowData = (
|
||||
? 0
|
||||
: BASE_PADDING -
|
||||
(arrow.startArrowhead
|
||||
? FIXED_BINDING_DISTANCE * 6
|
||||
: FIXED_BINDING_DISTANCE * 2),
|
||||
? BASE_BINDING_GAP_ELBOW * 6
|
||||
: BASE_BINDING_GAP_ELBOW * 2),
|
||||
BASE_PADDING,
|
||||
),
|
||||
boundsOverlap
|
||||
@@ -1378,8 +1383,8 @@ const getElbowArrowData = (
|
||||
? 0
|
||||
: BASE_PADDING -
|
||||
(arrow.endArrowhead
|
||||
? FIXED_BINDING_DISTANCE * 6
|
||||
: FIXED_BINDING_DISTANCE * 2),
|
||||
? BASE_BINDING_GAP_ELBOW * 6
|
||||
: BASE_BINDING_GAP_ELBOW * 2),
|
||||
BASE_PADDING,
|
||||
),
|
||||
boundsOverlap,
|
||||
@@ -2239,6 +2244,7 @@ const getBindPointHeading = (
|
||||
hoveredElement: ExcalidrawBindableElement | null | undefined,
|
||||
origPoint: GlobalPoint,
|
||||
elementsMap: ElementsMap,
|
||||
zoom?: AppState["zoom"],
|
||||
): Heading =>
|
||||
getHeadingForElbowArrowSnap(
|
||||
p,
|
||||
@@ -2257,21 +2263,20 @@ const getBindPointHeading = (
|
||||
),
|
||||
origPoint,
|
||||
elementsMap,
|
||||
zoom,
|
||||
);
|
||||
|
||||
const getHoveredElement = (
|
||||
origPoint: GlobalPoint,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||
zoom?: AppState["zoom"],
|
||||
) => {
|
||||
return getHoveredElementForBinding(
|
||||
tupleToCoors(origPoint),
|
||||
origPoint,
|
||||
elements,
|
||||
elementsMap,
|
||||
zoom,
|
||||
true,
|
||||
true,
|
||||
(element) => maxBindingDistance_simple(zoom),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
PendingExcalidrawElements,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { bindLinearElement } from "./binding";
|
||||
import { bindBindingElement } from "./binding";
|
||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||
import {
|
||||
HEADING_DOWN,
|
||||
@@ -446,8 +446,14 @@ const createBindingArrow = (
|
||||
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
|
||||
bindLinearElement(bindingArrow, startBindingElement, "start", scene);
|
||||
bindLinearElement(bindingArrow, endBindingElement, "end", scene);
|
||||
bindBindingElement(
|
||||
bindingArrow,
|
||||
startBindingElement,
|
||||
"orbit",
|
||||
"start",
|
||||
scene,
|
||||
);
|
||||
bindBindingElement(bindingArrow, endBindingElement, "orbit", "end", scene);
|
||||
|
||||
const changedElements = new Map<string, OrderedExcalidrawElement>();
|
||||
changedElements.set(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { toIterable } from "@excalidraw/common";
|
||||
|
||||
import { isInvisiblySmallElement } from "./sizeHelpers";
|
||||
import { isLinearElementType } from "./typeChecks";
|
||||
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
@@ -55,27 +54,6 @@ export const isNonDeletedElement = <T extends ExcalidrawElement>(
|
||||
element: T,
|
||||
): element is NonDeleted<T> => !element.isDeleted;
|
||||
|
||||
const _clearElements = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
): ExcalidrawElement[] =>
|
||||
getNonDeletedElements(elements).map((element) =>
|
||||
isLinearElementType(element.type)
|
||||
? { ...element, lastCommittedPoint: null }
|
||||
: element,
|
||||
);
|
||||
|
||||
export const clearElementsForDatabase = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
) => _clearElements(elements);
|
||||
|
||||
export const clearElementsForExport = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
) => _clearElements(elements);
|
||||
|
||||
export const clearElementsForLocalStorage = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
) => _clearElements(elements);
|
||||
|
||||
export * from "./align";
|
||||
export * from "./binding";
|
||||
export * from "./bounds";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -46,16 +46,13 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
|
||||
// casting to any because can't use `in` operator
|
||||
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
||||
const { points, fixedSegments, startBinding, endBinding, fileId } =
|
||||
updates as any;
|
||||
const { points, fixedSegments, fileId } = updates as any;
|
||||
|
||||
if (
|
||||
isElbowArrow(element) &&
|
||||
(Object.keys(updates).length === 0 || // normalization case
|
||||
typeof points !== "undefined" || // repositioning
|
||||
typeof fixedSegments !== "undefined" || // segment fixing
|
||||
typeof startBinding !== "undefined" ||
|
||||
typeof endBinding !== "undefined") // manual binding to element
|
||||
typeof fixedSegments !== "undefined") // segment fixing
|
||||
) {
|
||||
updates = {
|
||||
...updates,
|
||||
|
||||
@@ -452,7 +452,6 @@ export const newFreeDrawElement = (
|
||||
points: opts.points || [],
|
||||
pressures: opts.pressures || [],
|
||||
simulatePressure: opts.simulatePressure,
|
||||
lastCommittedPoint: null,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -466,7 +465,7 @@ export const newLinearElement = (
|
||||
const element = {
|
||||
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
|
||||
points: opts.points || [],
|
||||
lastCommittedPoint: null,
|
||||
|
||||
startBinding: null,
|
||||
endBinding: null,
|
||||
startArrowhead: null,
|
||||
@@ -501,7 +500,6 @@ export const newArrowElement = <T extends boolean>(
|
||||
return {
|
||||
..._newElementBase<ExcalidrawElbowArrowElement>(opts.type, opts),
|
||||
points: opts.points || [],
|
||||
lastCommittedPoint: null,
|
||||
startBinding: null,
|
||||
endBinding: null,
|
||||
startArrowhead: opts.startArrowhead || null,
|
||||
@@ -516,7 +514,6 @@ export const newArrowElement = <T extends boolean>(
|
||||
return {
|
||||
..._newElementBase<ExcalidrawArrowElement>(opts.type, opts),
|
||||
points: opts.points || [],
|
||||
lastCommittedPoint: null,
|
||||
startBinding: null,
|
||||
endBinding: null,
|
||||
startArrowhead: opts.startArrowhead || null,
|
||||
|
||||
@@ -98,7 +98,7 @@ const isPendingImageElement = (
|
||||
const shouldResetImageFilter = (
|
||||
element: ExcalidrawElement,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
appState: StaticCanvasAppState,
|
||||
appState: StaticCanvasAppState | InteractiveCanvasAppState,
|
||||
) => {
|
||||
return (
|
||||
appState.theme === THEME.DARK &&
|
||||
@@ -225,7 +225,7 @@ const generateElementCanvas = (
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
zoom: Zoom,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
appState: StaticCanvasAppState,
|
||||
appState: StaticCanvasAppState | InteractiveCanvasAppState,
|
||||
): ExcalidrawElementWithCanvas | null => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const context = canvas.getContext("2d")!;
|
||||
@@ -277,7 +277,7 @@ const generateElementCanvas = (
|
||||
context.filter = IMAGE_INVERT_FILTER;
|
||||
}
|
||||
|
||||
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
||||
drawElementOnCanvas(element, rc, context, renderConfig);
|
||||
|
||||
context.restore();
|
||||
|
||||
@@ -412,7 +412,6 @@ const drawElementOnCanvas = (
|
||||
rc: RoughCanvas,
|
||||
context: CanvasRenderingContext2D,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
appState: StaticCanvasAppState,
|
||||
) => {
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
@@ -558,7 +557,7 @@ const generateElementWithCanvas = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
appState: StaticCanvasAppState,
|
||||
appState: StaticCanvasAppState | InteractiveCanvasAppState,
|
||||
) => {
|
||||
const zoom: Zoom = renderConfig
|
||||
? appState.zoom
|
||||
@@ -615,7 +614,7 @@ const drawElementFromCanvas = (
|
||||
elementWithCanvas: ExcalidrawElementWithCanvas,
|
||||
context: CanvasRenderingContext2D,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
appState: StaticCanvasAppState,
|
||||
appState: StaticCanvasAppState | InteractiveCanvasAppState,
|
||||
allElementsMap: NonDeletedSceneElementsMap,
|
||||
) => {
|
||||
const element = elementWithCanvas.element;
|
||||
@@ -733,7 +732,7 @@ export const renderElement = (
|
||||
rc: RoughCanvas,
|
||||
context: CanvasRenderingContext2D,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
appState: StaticCanvasAppState,
|
||||
appState: StaticCanvasAppState | InteractiveCanvasAppState,
|
||||
) => {
|
||||
const reduceAlphaForSelection =
|
||||
appState.openDialog?.name === "elementLinkSelector" &&
|
||||
@@ -803,7 +802,7 @@ export const renderElement = (
|
||||
context.translate(cx, cy);
|
||||
context.rotate(element.angle);
|
||||
context.translate(-shiftX, -shiftY);
|
||||
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
||||
drawElementOnCanvas(element, rc, context, renderConfig);
|
||||
context.restore();
|
||||
} else {
|
||||
const elementWithCanvas = generateElementWithCanvas(
|
||||
@@ -896,13 +895,7 @@ export const renderElement = (
|
||||
|
||||
tempCanvasContext.translate(-shiftX, -shiftY);
|
||||
|
||||
drawElementOnCanvas(
|
||||
element,
|
||||
tempRc,
|
||||
tempCanvasContext,
|
||||
renderConfig,
|
||||
appState,
|
||||
);
|
||||
drawElementOnCanvas(element, tempRc, tempCanvasContext, renderConfig);
|
||||
|
||||
tempCanvasContext.translate(shiftX, shiftY);
|
||||
|
||||
@@ -941,7 +934,7 @@ export const renderElement = (
|
||||
}
|
||||
|
||||
context.translate(-shiftX, -shiftY);
|
||||
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
||||
drawElementOnCanvas(element, rc, context, renderConfig);
|
||||
}
|
||||
|
||||
context.restore();
|
||||
@@ -1122,7 +1115,7 @@ export function getFreedrawOutlinePoints(element: ExcalidrawFreeDrawElement) {
|
||||
smoothing: 0.5,
|
||||
streamline: 0.5,
|
||||
easing: (t) => Math.sin((t * Math.PI) / 2), // https://easings.net/#easeOutSine
|
||||
last: !!element.lastCommittedPoint, // LastCommittedPoint is added on pointerup
|
||||
last: true,
|
||||
};
|
||||
|
||||
return getStroke(inputPoints as number[][], options) as [number, number][];
|
||||
|
||||
@@ -20,7 +20,11 @@ import type { PointerDownState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||
|
||||
import { getArrowLocalFixedPoints, updateBoundElements } from "./binding";
|
||||
import {
|
||||
getArrowLocalFixedPoints,
|
||||
unbindBindingElement,
|
||||
updateBoundElements,
|
||||
} from "./binding";
|
||||
import {
|
||||
getElementAbsoluteCoords,
|
||||
getCommonBounds,
|
||||
@@ -46,6 +50,7 @@ import {
|
||||
import { wrapText } from "./textWrapping";
|
||||
import {
|
||||
isArrowElement,
|
||||
isBindingElement,
|
||||
isBoundToContainer,
|
||||
isElbowArrow,
|
||||
isFrameLikeElement,
|
||||
@@ -74,7 +79,9 @@ import type {
|
||||
ExcalidrawImageElement,
|
||||
ElementsMap,
|
||||
ExcalidrawElbowArrowElement,
|
||||
ExcalidrawArrowElement,
|
||||
} from "./types";
|
||||
import type { ElementUpdate } from "./mutateElement";
|
||||
|
||||
// Returns true when transform (resizing/rotation) happened
|
||||
export const transformElements = (
|
||||
@@ -220,7 +227,25 @@ const rotateSingleElement = (
|
||||
}
|
||||
const boundTextElementId = getBoundTextElementId(element);
|
||||
|
||||
scene.mutateElement(element, { angle });
|
||||
let update: ElementUpdate<NonDeletedExcalidrawElement> = {
|
||||
angle,
|
||||
};
|
||||
|
||||
if (isBindingElement(element)) {
|
||||
update = {
|
||||
...update,
|
||||
} as ElementUpdate<ExcalidrawArrowElement>;
|
||||
|
||||
if (element.startBinding) {
|
||||
unbindBindingElement(element, "start", scene);
|
||||
}
|
||||
if (element.endBinding) {
|
||||
unbindBindingElement(element, "end", scene);
|
||||
}
|
||||
}
|
||||
|
||||
scene.mutateElement(element, update);
|
||||
|
||||
if (boundTextElementId) {
|
||||
const textElement =
|
||||
scene.getElement<ExcalidrawTextElementWithContainer>(boundTextElementId);
|
||||
@@ -394,6 +419,11 @@ const rotateMultipleElements = (
|
||||
centerAngle -= centerAngle % SHIFT_LOCKING_ANGLE;
|
||||
}
|
||||
|
||||
const rotatedElementsMap = new Map<
|
||||
ExcalidrawElement["id"],
|
||||
NonDeletedExcalidrawElement
|
||||
>(elements.map((element) => [element.id, element]));
|
||||
|
||||
for (const element of elements) {
|
||||
if (!isFrameLikeElement(element)) {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||
@@ -424,6 +454,19 @@ const rotateMultipleElements = (
|
||||
simultaneouslyUpdated: elements,
|
||||
});
|
||||
|
||||
if (isBindingElement(element)) {
|
||||
if (element.startBinding) {
|
||||
if (!rotatedElementsMap.has(element.startBinding.elementId)) {
|
||||
unbindBindingElement(element, "start", scene);
|
||||
}
|
||||
}
|
||||
if (element.endBinding) {
|
||||
if (!rotatedElementsMap.has(element.endBinding.elementId)) {
|
||||
unbindBindingElement(element, "end", scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const boundText = getBoundTextElement(element, elementsMap);
|
||||
if (boundText && !isArrowElement(element)) {
|
||||
const { x, y } = computeBoundTextPosition(
|
||||
@@ -835,13 +878,32 @@ export const resizeSingleElement = (
|
||||
Number.isFinite(newOrigin.x) &&
|
||||
Number.isFinite(newOrigin.y)
|
||||
) {
|
||||
const updates = {
|
||||
let updates: ElementUpdate<ExcalidrawElement> = {
|
||||
...newOrigin,
|
||||
width: Math.abs(nextWidth),
|
||||
height: Math.abs(nextHeight),
|
||||
...rescaledPoints,
|
||||
};
|
||||
|
||||
if (isBindingElement(latestElement)) {
|
||||
if (latestElement.startBinding) {
|
||||
updates = {
|
||||
...updates,
|
||||
} as ElementUpdate<ExcalidrawArrowElement>;
|
||||
|
||||
if (latestElement.startBinding) {
|
||||
unbindBindingElement(latestElement, "start", scene);
|
||||
}
|
||||
}
|
||||
|
||||
if (latestElement.endBinding) {
|
||||
updates = {
|
||||
...updates,
|
||||
endBinding: null,
|
||||
} as ElementUpdate<ExcalidrawArrowElement>;
|
||||
}
|
||||
}
|
||||
|
||||
scene.mutateElement(latestElement, updates, {
|
||||
informMutation: shouldInformMutation,
|
||||
isDragging: false,
|
||||
@@ -859,10 +921,7 @@ export const resizeSingleElement = (
|
||||
shouldMaintainAspectRatio,
|
||||
);
|
||||
|
||||
updateBoundElements(latestElement, scene, {
|
||||
// TODO: confirm with MARK if this actually makes sense
|
||||
newSize: { width: nextWidth, height: nextHeight },
|
||||
});
|
||||
updateBoundElements(latestElement, scene);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1396,20 +1455,36 @@ export const resizeMultipleElements = (
|
||||
}
|
||||
|
||||
const elementsToUpdate = elementsAndUpdates.map(({ element }) => element);
|
||||
const resizedElementsMap = new Map<
|
||||
ExcalidrawElement["id"],
|
||||
NonDeletedExcalidrawElement
|
||||
>(elementsAndUpdates.map(({ element }) => [element.id, element]));
|
||||
|
||||
for (const {
|
||||
element,
|
||||
update: { boundTextFontSize, ...update },
|
||||
} of elementsAndUpdates) {
|
||||
const { width, height, angle } = update;
|
||||
const { angle } = update;
|
||||
|
||||
scene.mutateElement(element, update);
|
||||
|
||||
updateBoundElements(element, scene, {
|
||||
simultaneouslyUpdated: elementsToUpdate,
|
||||
newSize: { width, height },
|
||||
});
|
||||
|
||||
if (isBindingElement(element)) {
|
||||
if (element.startBinding) {
|
||||
if (!resizedElementsMap.has(element.startBinding.elementId)) {
|
||||
unbindBindingElement(element, "start", scene);
|
||||
}
|
||||
}
|
||||
if (element.endBinding) {
|
||||
if (!resizedElementsMap.has(element.endBinding.elementId)) {
|
||||
unbindBindingElement(element, "end", scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement && boundTextFontSize) {
|
||||
scene.mutateElement(boundTextElement, {
|
||||
|
||||
@@ -330,7 +330,10 @@ export const hasBoundingBox = (
|
||||
appState: InteractiveCanvasAppState,
|
||||
editorInterface: EditorInterface,
|
||||
) => {
|
||||
if (appState.selectedLinearElement?.isEditing) {
|
||||
if (
|
||||
appState.selectedLinearElement?.isEditing ||
|
||||
appState.selectedLinearElement?.isDragging
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (elements.length > 1) {
|
||||
|
||||
@@ -28,8 +28,6 @@ import type {
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawElbowArrowElement,
|
||||
ExcalidrawLineElement,
|
||||
PointBinding,
|
||||
FixedPointBinding,
|
||||
ExcalidrawFlowchartNodeElement,
|
||||
ExcalidrawLinearElementSubType,
|
||||
} from "./types";
|
||||
@@ -163,7 +161,7 @@ export const isLinearElementType = (
|
||||
export const isBindingElement = (
|
||||
element?: ExcalidrawElement | null,
|
||||
includeLocked = true,
|
||||
): element is ExcalidrawLinearElement => {
|
||||
): element is ExcalidrawArrowElement => {
|
||||
return (
|
||||
element != null &&
|
||||
(!element.locked || includeLocked === true) &&
|
||||
@@ -358,15 +356,6 @@ export const getDefaultRoundnessTypeForElement = (
|
||||
return null;
|
||||
};
|
||||
|
||||
export const isFixedPointBinding = (
|
||||
binding: PointBinding | FixedPointBinding,
|
||||
): binding is FixedPointBinding => {
|
||||
return (
|
||||
Object.hasOwn(binding, "fixedPoint") &&
|
||||
(binding as FixedPointBinding).fixedPoint != null
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: Move this to @excalidraw/math
|
||||
export const isBounds = (box: unknown): box is Bounds =>
|
||||
Array.isArray(box) &&
|
||||
|
||||
@@ -279,23 +279,22 @@ export type ExcalidrawTextElementWithContainer = {
|
||||
|
||||
export type FixedPoint = [number, number];
|
||||
|
||||
export type PointBinding = {
|
||||
elementId: ExcalidrawBindableElement["id"];
|
||||
focus: number;
|
||||
gap: number;
|
||||
};
|
||||
export type BindMode = "inside" | "orbit" | "skip";
|
||||
|
||||
export type FixedPointBinding = Merge<
|
||||
PointBinding,
|
||||
{
|
||||
// Represents the fixed point binding information in form of a vertical and
|
||||
// horizontal ratio (i.e. a percentage value in the 0.0-1.0 range). This ratio
|
||||
// gives the user selected fixed point by multiplying the bound element width
|
||||
// with fixedPoint[0] and the bound element height with fixedPoint[1] to get the
|
||||
// bound element-local point coordinate.
|
||||
fixedPoint: FixedPoint;
|
||||
}
|
||||
>;
|
||||
export type FixedPointBinding = {
|
||||
elementId: ExcalidrawBindableElement["id"];
|
||||
|
||||
// Represents the fixed point binding information in form of a vertical and
|
||||
// horizontal ratio (i.e. a percentage value in the 0.0-1.0 range). This ratio
|
||||
// gives the user selected fixed point by multiplying the bound element width
|
||||
// with fixedPoint[0] and the bound element height with fixedPoint[1] to get the
|
||||
// bound element-local point coordinate.
|
||||
fixedPoint: FixedPoint;
|
||||
|
||||
// Determines whether the arrow remains outside the shape or is allowed to
|
||||
// go all the way inside the shape up to the exact fixed point.
|
||||
mode: BindMode;
|
||||
};
|
||||
|
||||
type Index = number;
|
||||
|
||||
@@ -322,9 +321,8 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
||||
Readonly<{
|
||||
type: "line" | "arrow";
|
||||
points: readonly LocalPoint[];
|
||||
lastCommittedPoint: LocalPoint | null;
|
||||
startBinding: PointBinding | null;
|
||||
endBinding: PointBinding | null;
|
||||
startBinding: FixedPointBinding | null;
|
||||
endBinding: FixedPointBinding | null;
|
||||
startArrowhead: Arrowhead | null;
|
||||
endArrowhead: Arrowhead | null;
|
||||
}>;
|
||||
@@ -351,9 +349,9 @@ export type ExcalidrawElbowArrowElement = Merge<
|
||||
ExcalidrawArrowElement,
|
||||
{
|
||||
elbowed: true;
|
||||
fixedSegments: readonly FixedSegment[] | null;
|
||||
startBinding: FixedPointBinding | null;
|
||||
endBinding: FixedPointBinding | null;
|
||||
fixedSegments: readonly FixedSegment[] | null;
|
||||
/**
|
||||
* Marks that the 3rd point should be used as the 2nd point of the arrow in
|
||||
* order to temporarily hide the first segment of the arrow without losing
|
||||
@@ -379,7 +377,6 @@ export type ExcalidrawFreeDrawElement = _ExcalidrawElementBase &
|
||||
points: readonly LocalPoint[];
|
||||
pressures: readonly number[];
|
||||
simulatePressure: boolean;
|
||||
lastCommittedPoint: LocalPoint | null;
|
||||
}>;
|
||||
|
||||
export type FileId = string & { _brand: "FileId" };
|
||||
|
||||
+152
-15
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
DEFAULT_ADAPTIVE_RADIUS,
|
||||
DEFAULT_PROPORTIONAL_RADIUS,
|
||||
invariant,
|
||||
LINE_CONFIRM_THRESHOLD,
|
||||
ROUNDNESS,
|
||||
} from "@excalidraw/common";
|
||||
@@ -10,10 +11,17 @@ import {
|
||||
curveCatmullRomCubicApproxPoints,
|
||||
curveOffsetPoints,
|
||||
lineSegment,
|
||||
lineSegmentIntersectionPoints,
|
||||
pointDistance,
|
||||
pointFrom,
|
||||
pointFromArray,
|
||||
pointFromVector,
|
||||
pointRotateRads,
|
||||
pointTranslate,
|
||||
rectangle,
|
||||
vectorFromPoint,
|
||||
vectorNormalize,
|
||||
vectorScale,
|
||||
type GlobalPoint,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
@@ -21,11 +29,17 @@ import type { Curve, LineSegment, LocalPoint } from "@excalidraw/math";
|
||||
|
||||
import type { NormalizedZoomValue, Zoom } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { getDiamondPoints } from "./bounds";
|
||||
import { elementCenterPoint, getDiamondPoints } from "./bounds";
|
||||
|
||||
import { generateLinearCollisionShape } from "./shape";
|
||||
|
||||
import { isPointInElement } from "./collision";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { isRectangularElement } from "./typeChecks";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawFreeDrawElement,
|
||||
@@ -400,20 +414,10 @@ export function deconstructDiamondElement(
|
||||
), // TOP
|
||||
];
|
||||
|
||||
const corners =
|
||||
offset > 0
|
||||
? baseCorners.map(
|
||||
(corner) =>
|
||||
curveCatmullRomCubicApproxPoints(
|
||||
curveOffsetPoints(corner, offset),
|
||||
)!,
|
||||
)
|
||||
: [
|
||||
[baseCorners[0]],
|
||||
[baseCorners[1]],
|
||||
[baseCorners[2]],
|
||||
[baseCorners[3]],
|
||||
];
|
||||
const corners = baseCorners.map(
|
||||
(corner) =>
|
||||
curveCatmullRomCubicApproxPoints(curveOffsetPoints(corner, offset))!,
|
||||
);
|
||||
|
||||
const sides = [
|
||||
lineSegment<GlobalPoint>(
|
||||
@@ -481,3 +485,136 @@ export const getCornerRadius = (x: number, element: ExcalidrawElement) => {
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const getDiagonalsForBindableElement = (
|
||||
element: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
// for rectangles, shrink the diagonals a bit because there's something
|
||||
// going on with the focus points around the corners. Ask Mark for details.
|
||||
const OFFSET_PX = element.type === "rectangle" ? 15 : 0;
|
||||
const shrinkSegment = (seg: LineSegment<GlobalPoint>) => {
|
||||
const v = vectorNormalize(vectorFromPoint(seg[1], seg[0]));
|
||||
const offset = vectorScale(v, OFFSET_PX);
|
||||
return lineSegment<GlobalPoint>(
|
||||
pointTranslate(seg[0], offset),
|
||||
pointTranslate(seg[1], vectorScale(offset, -1)),
|
||||
);
|
||||
};
|
||||
|
||||
const center = elementCenterPoint(element, elementsMap);
|
||||
const diagonalOne = shrinkSegment(
|
||||
isRectangularElement(element)
|
||||
? lineSegment<GlobalPoint>(
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(element.x, element.y),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + element.width,
|
||||
element.y + element.height,
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
)
|
||||
: lineSegment<GlobalPoint>(
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(element.x + element.width / 2, element.y),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height,
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
),
|
||||
);
|
||||
const diagonalTwo = shrinkSegment(
|
||||
isRectangularElement(element)
|
||||
? lineSegment<GlobalPoint>(
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(element.x + element.width, element.y),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(element.x, element.y + element.height),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
)
|
||||
: lineSegment<GlobalPoint>(
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(element.x, element.y + element.height / 2),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + element.width,
|
||||
element.y + element.height / 2,
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return [diagonalOne, diagonalTwo];
|
||||
};
|
||||
|
||||
export const projectFixedPointOntoDiagonal = (
|
||||
arrow: ExcalidrawArrowElement,
|
||||
point: GlobalPoint,
|
||||
element: ExcalidrawElement,
|
||||
startOrEnd: "start" | "end",
|
||||
elementsMap: ElementsMap,
|
||||
): GlobalPoint | null => {
|
||||
invariant(arrow.points.length >= 2, "Arrow must have at least two points");
|
||||
if (arrow.width < 3 && arrow.height < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [diagonalOne, diagonalTwo] = getDiagonalsForBindableElement(
|
||||
element,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
const a = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
arrow,
|
||||
startOrEnd === "start" ? 1 : arrow.points.length - 2,
|
||||
elementsMap,
|
||||
);
|
||||
const b = pointFromVector<GlobalPoint>(
|
||||
vectorScale(
|
||||
vectorFromPoint(point, a),
|
||||
2 * pointDistance(a, point) +
|
||||
Math.max(
|
||||
pointDistance(diagonalOne[0], diagonalOne[1]),
|
||||
pointDistance(diagonalTwo[0], diagonalTwo[1]),
|
||||
),
|
||||
),
|
||||
a,
|
||||
);
|
||||
const intersector = lineSegment<GlobalPoint>(point, b);
|
||||
const p1 = lineSegmentIntersectionPoints(diagonalOne, intersector);
|
||||
const p2 = lineSegmentIntersectionPoints(diagonalTwo, intersector);
|
||||
const d1 = p1 && pointDistance(a, p1);
|
||||
const d2 = p2 && pointDistance(a, p2);
|
||||
|
||||
let p = null;
|
||||
if (d1 != null && d2 != null) {
|
||||
p = d1 < d2 ? p1 : p2;
|
||||
} else {
|
||||
p = p1 || p2 || null;
|
||||
}
|
||||
|
||||
return p && isPointInElement(p, element, elementsMap) ? p : null;
|
||||
};
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import { arrayToMap, findIndex, findLastIndex } from "@excalidraw/common";
|
||||
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
import type { GlobalPoint } from "@excalidraw/math";
|
||||
|
||||
import { isFrameLikeElement } from "./typeChecks";
|
||||
|
||||
import { isFrameLikeElement, isTextElement } from "./typeChecks";
|
||||
import { getElementsInGroup } from "./groups";
|
||||
|
||||
import { syncMovedIndices } from "./fractionalIndex";
|
||||
|
||||
import { getSelectedElements } from "./selection";
|
||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||
import { getHoveredElementForBinding } from "./collision";
|
||||
|
||||
import type { Scene } from "./Scene";
|
||||
|
||||
import type { ExcalidrawElement, ExcalidrawFrameLikeElement } from "./types";
|
||||
import type {
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawFrameLikeElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
Ordered,
|
||||
OrderedExcalidrawElement,
|
||||
} from "./types";
|
||||
|
||||
const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {
|
||||
return element.frameId === frameId || element.id === frameId;
|
||||
@@ -139,6 +146,51 @@ const getContiguousFrameRangeElements = (
|
||||
return allElements.slice(rangeStart, rangeEnd + 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Moves the arrow element above any bindable elements it intersects with or
|
||||
* hovers over.
|
||||
*/
|
||||
export const moveArrowAboveBindable = (
|
||||
point: GlobalPoint,
|
||||
arrow: ExcalidrawArrowElement,
|
||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
scene: Scene,
|
||||
): readonly OrderedExcalidrawElement[] => {
|
||||
const hoveredElement = getHoveredElementForBinding(
|
||||
point,
|
||||
elements,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
if (!hoveredElement) {
|
||||
return elements;
|
||||
}
|
||||
|
||||
const boundTextElement = getBoundTextElement(hoveredElement, elementsMap);
|
||||
const containerElement = isTextElement(hoveredElement)
|
||||
? getContainerElement(hoveredElement, elementsMap)
|
||||
: null;
|
||||
|
||||
const bindableIds = [
|
||||
hoveredElement.id,
|
||||
boundTextElement?.id,
|
||||
containerElement?.id,
|
||||
].filter((id): id is NonDeletedExcalidrawElement["id"] => !!id);
|
||||
const bindableIdx = elements.findIndex((el) => bindableIds.includes(el.id));
|
||||
const arrowIdx = elements.findIndex((el) => el.id === arrow.id);
|
||||
|
||||
if (arrowIdx !== -1 && bindableIdx !== -1 && arrowIdx < bindableIdx) {
|
||||
const updatedElements = Array.from(elements);
|
||||
const arrow = updatedElements.splice(arrowIdx, 1)[0];
|
||||
updatedElements.splice(bindableIdx, 0, arrow);
|
||||
|
||||
scene.replaceAllElements(updatedElements);
|
||||
}
|
||||
|
||||
return elements;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns next candidate index that's available to be moved to. Currently that
|
||||
* is a non-deleted element, and not inside a group (unless we're editing it).
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -135,9 +135,9 @@ describe("getElementBounds", () => {
|
||||
} as ExcalidrawLinearElement;
|
||||
|
||||
const [x1, y1, x2, y2] = getElementBounds(element, arrayToMap([element]));
|
||||
expect(x1).toEqual(360.3176068760539);
|
||||
expect(y1).toEqual(185.90654264413516);
|
||||
expect(x2).toEqual(480.87005902729743);
|
||||
expect(y2).toEqual(320.4751269334226);
|
||||
expect(x1).toEqual(360.9291017525165);
|
||||
expect(y1).toEqual(185.24770129343722);
|
||||
expect(x2).toEqual(481.4815539037601);
|
||||
expect(y2).toEqual(319.8162855827246);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -144,9 +144,8 @@ describe("duplicating multiple elements", () => {
|
||||
id: "arrow1",
|
||||
startBinding: {
|
||||
elementId: "rectangle1",
|
||||
focus: 0.2,
|
||||
gap: 7,
|
||||
fixedPoint: [0.5, 1],
|
||||
mode: "orbit",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -155,9 +154,8 @@ describe("duplicating multiple elements", () => {
|
||||
id: "arrow2",
|
||||
endBinding: {
|
||||
elementId: "rectangle1",
|
||||
focus: 0.2,
|
||||
gap: 7,
|
||||
fixedPoint: [0.5, 1],
|
||||
mode: "orbit",
|
||||
},
|
||||
boundElements: [{ id: "text2", type: "text" }],
|
||||
});
|
||||
@@ -276,9 +274,8 @@ describe("duplicating multiple elements", () => {
|
||||
id: "arrow1",
|
||||
startBinding: {
|
||||
elementId: "rectangle1",
|
||||
focus: 0.2,
|
||||
gap: 7,
|
||||
fixedPoint: [0.5, 1],
|
||||
mode: "orbit",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -293,15 +290,13 @@ describe("duplicating multiple elements", () => {
|
||||
id: "arrow2",
|
||||
startBinding: {
|
||||
elementId: "rectangle1",
|
||||
focus: 0.2,
|
||||
gap: 7,
|
||||
fixedPoint: [0.5, 1],
|
||||
mode: "orbit",
|
||||
},
|
||||
endBinding: {
|
||||
elementId: "rectangle-not-exists",
|
||||
focus: 0.2,
|
||||
gap: 7,
|
||||
fixedPoint: [0.5, 1],
|
||||
mode: "orbit",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -310,15 +305,13 @@ describe("duplicating multiple elements", () => {
|
||||
id: "arrow3",
|
||||
startBinding: {
|
||||
elementId: "rectangle-not-exists",
|
||||
focus: 0.2,
|
||||
gap: 7,
|
||||
fixedPoint: [0.5, 1],
|
||||
mode: "orbit",
|
||||
},
|
||||
endBinding: {
|
||||
elementId: "rectangle1",
|
||||
focus: 0.2,
|
||||
gap: 7,
|
||||
fixedPoint: [0.5, 1],
|
||||
mode: "orbit",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -821,7 +814,7 @@ describe("duplication z-order", () => {
|
||||
const arrow = UI.createElement("arrow", {
|
||||
x: -100,
|
||||
y: 50,
|
||||
width: 95,
|
||||
width: 115,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { ARROW_TYPE } from "@excalidraw/common";
|
||||
import { pointFrom } from "@excalidraw/math";
|
||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||
|
||||
import { actionSelectAll } from "@excalidraw/excalidraw/actions";
|
||||
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions/actionDuplicateSelection";
|
||||
|
||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||
import { Pointer, UI } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
@@ -15,13 +12,11 @@ import {
|
||||
queryByTestId,
|
||||
render,
|
||||
} from "@excalidraw/excalidraw/tests/test-utils";
|
||||
|
||||
import "@excalidraw/utils/test-utils";
|
||||
import { bindBindingElement } from "@excalidraw/element";
|
||||
|
||||
import type { LocalPoint } from "@excalidraw/math";
|
||||
|
||||
import { bindLinearElement } from "../src/binding";
|
||||
|
||||
import { Scene } from "../src/Scene";
|
||||
|
||||
import type {
|
||||
@@ -136,6 +131,11 @@ describe("elbow arrow segment move", () => {
|
||||
});
|
||||
|
||||
describe("elbow arrow routing", () => {
|
||||
beforeEach(async () => {
|
||||
localStorage.clear();
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
});
|
||||
|
||||
it("can properly generate orthogonal arrow points", () => {
|
||||
const scene = new Scene();
|
||||
const arrow = API.createElement({
|
||||
@@ -160,8 +160,8 @@ describe("elbow arrow routing", () => {
|
||||
expect(arrow.width).toEqual(90);
|
||||
expect(arrow.height).toEqual(200);
|
||||
});
|
||||
|
||||
it("can generate proper points for bound elbow arrow", () => {
|
||||
const scene = new Scene();
|
||||
const rectangle1 = API.createElement({
|
||||
type: "rectangle",
|
||||
x: -150,
|
||||
@@ -185,25 +185,23 @@ describe("elbow arrow routing", () => {
|
||||
height: 200,
|
||||
points: [pointFrom(0, 0), pointFrom(90, 200)],
|
||||
}) as ExcalidrawElbowArrowElement;
|
||||
scene.insertElement(rectangle1);
|
||||
scene.insertElement(rectangle2);
|
||||
scene.insertElement(arrow);
|
||||
API.setElements([rectangle1, rectangle2, arrow]);
|
||||
|
||||
bindLinearElement(arrow, rectangle1, "start", scene);
|
||||
bindLinearElement(arrow, rectangle2, "end", scene);
|
||||
bindBindingElement(arrow, rectangle1, "orbit", "start", h.scene);
|
||||
bindBindingElement(arrow, rectangle2, "orbit", "end", h.scene);
|
||||
|
||||
expect(arrow.startBinding).not.toBe(null);
|
||||
expect(arrow.endBinding).not.toBe(null);
|
||||
|
||||
h.app.scene.mutateElement(arrow, {
|
||||
h.scene.mutateElement(arrow, {
|
||||
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
|
||||
});
|
||||
|
||||
expect(arrow.points).toEqual([
|
||||
expect(arrow.points).toCloselyEqualPoints([
|
||||
[0, 0],
|
||||
[45, 0],
|
||||
[45, 200],
|
||||
[90, 200],
|
||||
[39, 0],
|
||||
[39, 200],
|
||||
[78, 200],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -242,9 +240,9 @@ describe("elbow arrow ui", () => {
|
||||
expect(h.state.currentItemArrowType).toBe(ARROW_TYPE.elbow);
|
||||
|
||||
mouse.reset();
|
||||
mouse.moveTo(-43, -99);
|
||||
mouse.moveTo(-53, -99);
|
||||
mouse.click();
|
||||
mouse.moveTo(43, 99);
|
||||
mouse.moveTo(53, 99);
|
||||
mouse.click();
|
||||
|
||||
const arrow = h.scene.getSelectedElements(
|
||||
@@ -253,11 +251,11 @@ describe("elbow arrow ui", () => {
|
||||
|
||||
expect(arrow.type).toBe("arrow");
|
||||
expect(arrow.elbowed).toBe(true);
|
||||
expect(arrow.points).toEqual([
|
||||
expect(arrow.points).toCloselyEqualPoints([
|
||||
[0, 0],
|
||||
[45, 0],
|
||||
[45, 200],
|
||||
[90, 200],
|
||||
[39, 0],
|
||||
[39, 200],
|
||||
[78, 200],
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -279,9 +277,9 @@ describe("elbow arrow ui", () => {
|
||||
UI.clickOnTestId("elbow-arrow");
|
||||
|
||||
mouse.reset();
|
||||
mouse.moveTo(-43, -99);
|
||||
mouse.moveTo(-53, -99);
|
||||
mouse.click();
|
||||
mouse.moveTo(43, 99);
|
||||
mouse.moveTo(53, 99);
|
||||
mouse.click();
|
||||
|
||||
const arrow = h.scene.getSelectedElements(
|
||||
@@ -297,9 +295,11 @@ describe("elbow arrow ui", () => {
|
||||
|
||||
expect(arrow.points.map((point) => point.map(Math.round))).toEqual([
|
||||
[0, 0],
|
||||
[35, 0],
|
||||
[35, 165],
|
||||
[103, 165],
|
||||
[36, 0],
|
||||
[36, 90],
|
||||
[28, 90],
|
||||
[28, 164],
|
||||
[101, 164],
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -321,9 +321,9 @@ describe("elbow arrow ui", () => {
|
||||
UI.clickOnTestId("elbow-arrow");
|
||||
|
||||
mouse.reset();
|
||||
mouse.moveTo(-43, -99);
|
||||
mouse.moveTo(-53, -99);
|
||||
mouse.click();
|
||||
mouse.moveTo(43, 99);
|
||||
mouse.moveTo(53, 99);
|
||||
mouse.click();
|
||||
|
||||
const arrow = h.scene.getSelectedElements(
|
||||
@@ -351,11 +351,11 @@ describe("elbow arrow ui", () => {
|
||||
expect(duplicatedArrow.id).not.toBe(originalArrowId);
|
||||
expect(duplicatedArrow.type).toBe("arrow");
|
||||
expect(duplicatedArrow.elbowed).toBe(true);
|
||||
expect(duplicatedArrow.points).toEqual([
|
||||
expect(duplicatedArrow.points).toCloselyEqualPoints([
|
||||
[0, 0],
|
||||
[45, 0],
|
||||
[45, 200],
|
||||
[90, 200],
|
||||
[39, 0],
|
||||
[39, 200],
|
||||
[78, 200],
|
||||
]);
|
||||
expect(arrow.startBinding).not.toBe(null);
|
||||
expect(arrow.endBinding).not.toBe(null);
|
||||
@@ -379,9 +379,9 @@ describe("elbow arrow ui", () => {
|
||||
UI.clickOnTestId("elbow-arrow");
|
||||
|
||||
mouse.reset();
|
||||
mouse.moveTo(-43, -99);
|
||||
mouse.moveTo(-53, -99);
|
||||
mouse.click();
|
||||
mouse.moveTo(43, 99);
|
||||
mouse.moveTo(53, 99);
|
||||
mouse.click();
|
||||
|
||||
const arrow = h.scene.getSelectedElements(
|
||||
@@ -405,11 +405,11 @@ describe("elbow arrow ui", () => {
|
||||
expect(duplicatedArrow.id).not.toBe(originalArrowId);
|
||||
expect(duplicatedArrow.type).toBe("arrow");
|
||||
expect(duplicatedArrow.elbowed).toBe(true);
|
||||
expect(duplicatedArrow.points).toEqual([
|
||||
expect(duplicatedArrow.points).toCloselyEqualPoints([
|
||||
[0, 0],
|
||||
[0, 100],
|
||||
[90, 100],
|
||||
[90, 200],
|
||||
[78, 100],
|
||||
[78, 200],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -217,7 +217,7 @@ describe("Test Linear Elements", () => {
|
||||
|
||||
// drag line from midpoint
|
||||
drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta));
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
expect(line.points.length).toEqual(3);
|
||||
expect(line.points).toMatchInlineSnapshot(`
|
||||
@@ -329,7 +329,7 @@ describe("Test Linear Elements", () => {
|
||||
expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
||||
|
||||
mouse.doubleClick();
|
||||
expect(h.state.selectedLinearElement).toBe(null);
|
||||
expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
||||
await getTextEditor();
|
||||
});
|
||||
|
||||
@@ -357,6 +357,7 @@ describe("Test Linear Elements", () => {
|
||||
const originalY = line.y;
|
||||
enterLineEditingMode(line);
|
||||
|
||||
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
|
||||
expect(line.points.length).toEqual(2);
|
||||
|
||||
mouse.clickAt(midpoint[0], midpoint[1]);
|
||||
@@ -379,7 +380,7 @@ describe("Test Linear Elements", () => {
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`11`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
|
||||
expect(line.points.length).toEqual(3);
|
||||
expect(line.points).toMatchInlineSnapshot(`
|
||||
@@ -549,7 +550,7 @@ describe("Test Linear Elements", () => {
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`14`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
||||
|
||||
expect(line.points.length).toEqual(5);
|
||||
|
||||
@@ -600,7 +601,7 @@ describe("Test Linear Elements", () => {
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`11`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
|
||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||
line,
|
||||
@@ -641,7 +642,7 @@ describe("Test Linear Elements", () => {
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`11`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
|
||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||
line,
|
||||
@@ -689,7 +690,7 @@ describe("Test Linear Elements", () => {
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`17`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`10`);
|
||||
|
||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
@@ -747,7 +748,7 @@ describe("Test Linear Elements", () => {
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`14`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
||||
expect(line.points.length).toEqual(5);
|
||||
|
||||
expect((h.elements[0] as ExcalidrawLinearElement).points)
|
||||
@@ -845,7 +846,7 @@ describe("Test Linear Elements", () => {
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`11`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
|
||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||
line,
|
||||
@@ -1303,7 +1304,7 @@ describe("Test Linear Elements", () => {
|
||||
const arrow = UI.createElement("arrow", {
|
||||
x: -10,
|
||||
y: 250,
|
||||
width: 400,
|
||||
width: 410,
|
||||
height: 1,
|
||||
});
|
||||
|
||||
@@ -1316,7 +1317,7 @@ describe("Test Linear Elements", () => {
|
||||
const textElement = h.elements[2] as ExcalidrawTextElementWithContainer;
|
||||
|
||||
expect(arrow.endBinding?.elementId).toBe(rect.id);
|
||||
expect(arrow.width).toBe(400);
|
||||
expect(arrow.width).toBeCloseTo(399);
|
||||
expect(rect.x).toBe(400);
|
||||
expect(rect.y).toBe(0);
|
||||
expect(
|
||||
@@ -1335,7 +1336,7 @@ describe("Test Linear Elements", () => {
|
||||
mouse.downAt(rect.x, rect.y);
|
||||
mouse.moveTo(200, 0);
|
||||
mouse.upAt(200, 0);
|
||||
expect(arrow.width).toBeCloseTo(200, 0);
|
||||
expect(arrow.width).toBeCloseTo(199);
|
||||
expect(rect.x).toBe(200);
|
||||
expect(rect.y).toBe(0);
|
||||
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
|
||||
|
||||
@@ -174,29 +174,29 @@ describe("generic element", () => {
|
||||
expect(rectangle.angle).toBeCloseTo(0);
|
||||
});
|
||||
|
||||
it("resizes with bound arrow", async () => {
|
||||
const rectangle = UI.createElement("rectangle", {
|
||||
width: 200,
|
||||
height: 100,
|
||||
});
|
||||
const arrow = UI.createElement("arrow", {
|
||||
x: -30,
|
||||
y: 50,
|
||||
width: 28,
|
||||
height: 5,
|
||||
});
|
||||
// it("resizes with bound arrow", async () => {
|
||||
// const rectangle = UI.createElement("rectangle", {
|
||||
// width: 200,
|
||||
// height: 100,
|
||||
// });
|
||||
// const arrow = UI.createElement("arrow", {
|
||||
// x: -30,
|
||||
// y: 50,
|
||||
// width: 28,
|
||||
// height: 5,
|
||||
// });
|
||||
|
||||
expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
||||
// expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
||||
|
||||
UI.resize(rectangle, "e", [40, 0]);
|
||||
// UI.resize(rectangle, "e", [40, 0]);
|
||||
|
||||
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(30, 0);
|
||||
// expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(30, 0);
|
||||
|
||||
UI.resize(rectangle, "w", [50, 0]);
|
||||
// UI.resize(rectangle, "w", [50, 0]);
|
||||
|
||||
expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
||||
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(80, 0);
|
||||
});
|
||||
// expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
||||
// expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(80, 0);
|
||||
// });
|
||||
|
||||
it("resizes with a label", async () => {
|
||||
const rectangle = UI.createElement("rectangle", {
|
||||
@@ -510,12 +510,12 @@ describe("arrow element", () => {
|
||||
h.state,
|
||||
)[0] as ExcalidrawElbowArrowElement;
|
||||
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.06);
|
||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||
|
||||
UI.resize(rectangle, "se", [-200, -150]);
|
||||
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.06);
|
||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||
});
|
||||
|
||||
@@ -538,11 +538,11 @@ describe("arrow element", () => {
|
||||
h.state,
|
||||
)[0] as ExcalidrawElbowArrowElement;
|
||||
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.06);
|
||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||
|
||||
UI.resize([rectangle, arrow], "nw", [300, 350]);
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(-0.05);
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(-0.06);
|
||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25);
|
||||
});
|
||||
});
|
||||
@@ -595,31 +595,31 @@ describe("text element", () => {
|
||||
expect(text.fontSize).toBeCloseTo(fontSize * scale);
|
||||
});
|
||||
|
||||
it("resizes with bound arrow", async () => {
|
||||
const text = UI.createElement("text");
|
||||
await UI.editText(text, "hello\nworld");
|
||||
const boundArrow = UI.createElement("arrow", {
|
||||
x: -30,
|
||||
y: 25,
|
||||
width: 28,
|
||||
height: 5,
|
||||
});
|
||||
// it("resizes with bound arrow", async () => {
|
||||
// const text = UI.createElement("text");
|
||||
// await UI.editText(text, "hello\nworld");
|
||||
// const boundArrow = UI.createElement("arrow", {
|
||||
// x: -30,
|
||||
// y: 25,
|
||||
// width: 28,
|
||||
// height: 5,
|
||||
// });
|
||||
|
||||
expect(boundArrow.endBinding?.elementId).toEqual(text.id);
|
||||
// expect(boundArrow.endBinding?.elementId).toEqual(text.id);
|
||||
|
||||
UI.resize(text, "ne", [40, 0]);
|
||||
// UI.resize(text, "ne", [40, 0]);
|
||||
|
||||
expect(boundArrow.width + boundArrow.endBinding!.gap).toBeCloseTo(30);
|
||||
// expect(boundArrow.width + boundArrow.endBinding!.gap).toBeCloseTo(30);
|
||||
|
||||
const textWidth = text.width;
|
||||
const scale = 20 / text.height;
|
||||
UI.resize(text, "nw", [50, 20]);
|
||||
// const textWidth = text.width;
|
||||
// const scale = 20 / text.height;
|
||||
// UI.resize(text, "nw", [50, 20]);
|
||||
|
||||
expect(boundArrow.endBinding?.elementId).toEqual(text.id);
|
||||
expect(boundArrow.width + boundArrow.endBinding!.gap).toBeCloseTo(
|
||||
30 + textWidth * scale,
|
||||
);
|
||||
});
|
||||
// expect(boundArrow.endBinding?.elementId).toEqual(text.id);
|
||||
// expect(boundArrow.width + boundArrow.endBinding!.gap).toBeCloseTo(
|
||||
// 30 + textWidth * scale,
|
||||
// );
|
||||
// });
|
||||
|
||||
it("updates font size via keyboard", async () => {
|
||||
const text = UI.createElement("text");
|
||||
@@ -801,36 +801,36 @@ describe("image element", () => {
|
||||
expect(image.scale).toEqual([1, 1]);
|
||||
});
|
||||
|
||||
it("resizes with bound arrow", async () => {
|
||||
const image = API.createElement({
|
||||
type: "image",
|
||||
width: 100,
|
||||
height: 100,
|
||||
});
|
||||
API.setElements([image]);
|
||||
const arrow = UI.createElement("arrow", {
|
||||
x: -30,
|
||||
y: 50,
|
||||
width: 28,
|
||||
height: 5,
|
||||
});
|
||||
// it("resizes with bound arrow", async () => {
|
||||
// const image = API.createElement({
|
||||
// type: "image",
|
||||
// width: 100,
|
||||
// height: 100,
|
||||
// });
|
||||
// API.setElements([image]);
|
||||
// const arrow = UI.createElement("arrow", {
|
||||
// x: -30,
|
||||
// y: 50,
|
||||
// width: 28,
|
||||
// height: 5,
|
||||
// });
|
||||
|
||||
expect(arrow.endBinding?.elementId).toEqual(image.id);
|
||||
// expect(arrow.endBinding?.elementId).toEqual(image.id);
|
||||
|
||||
UI.resize(image, "ne", [40, 0]);
|
||||
// UI.resize(image, "ne", [40, 0]);
|
||||
|
||||
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(30, 0);
|
||||
// expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(30, 0);
|
||||
|
||||
const imageWidth = image.width;
|
||||
const scale = 20 / image.height;
|
||||
UI.resize(image, "nw", [50, 20]);
|
||||
// const imageWidth = image.width;
|
||||
// const scale = 20 / image.height;
|
||||
// UI.resize(image, "nw", [50, 20]);
|
||||
|
||||
expect(arrow.endBinding?.elementId).toEqual(image.id);
|
||||
expect(Math.floor(arrow.width + arrow.endBinding!.gap)).toBeCloseTo(
|
||||
30 + imageWidth * scale,
|
||||
0,
|
||||
);
|
||||
});
|
||||
// expect(arrow.endBinding?.elementId).toEqual(image.id);
|
||||
// expect(Math.floor(arrow.width + arrow.endBinding!.gap)).toBeCloseTo(
|
||||
// 30 + imageWidth * scale,
|
||||
// 0,
|
||||
// );
|
||||
// });
|
||||
});
|
||||
|
||||
describe("multiple selection", () => {
|
||||
@@ -997,68 +997,80 @@ describe("multiple selection", () => {
|
||||
expect(diagLine.angle).toEqual(0);
|
||||
});
|
||||
|
||||
it("resizes with bound arrows", async () => {
|
||||
const rectangle = UI.createElement("rectangle", {
|
||||
position: 0,
|
||||
size: 100,
|
||||
});
|
||||
const leftBoundArrow = UI.createElement("arrow", {
|
||||
x: -110,
|
||||
y: 50,
|
||||
width: 100,
|
||||
height: 0,
|
||||
});
|
||||
// it("resizes with bound arrows", async () => {
|
||||
// const rectangle = UI.createElement("rectangle", {
|
||||
// position: 0,
|
||||
// size: 100,
|
||||
// });
|
||||
// const leftBoundArrow = UI.createElement("arrow", {
|
||||
// x: -110,
|
||||
// y: 50,
|
||||
// width: 100,
|
||||
// height: 0,
|
||||
// });
|
||||
|
||||
const rightBoundArrow = UI.createElement("arrow", {
|
||||
x: 210,
|
||||
y: 50,
|
||||
width: -100,
|
||||
height: 0,
|
||||
});
|
||||
// const rightBoundArrow = UI.createElement("arrow", {
|
||||
// x: 210,
|
||||
// y: 50,
|
||||
// width: -100,
|
||||
// height: 0,
|
||||
// });
|
||||
|
||||
const selectionWidth = 210;
|
||||
const selectionHeight = 100;
|
||||
const move = [40, 40] as [number, number];
|
||||
const scale = Math.max(
|
||||
1 - move[0] / selectionWidth,
|
||||
1 - move[1] / selectionHeight,
|
||||
);
|
||||
const leftArrowBinding = { ...leftBoundArrow.endBinding };
|
||||
const rightArrowBinding = { ...rightBoundArrow.endBinding };
|
||||
delete rightArrowBinding.gap;
|
||||
// const selectionWidth = 210;
|
||||
// const selectionHeight = 100;
|
||||
// const move = [40, 40] as [number, number];
|
||||
// const scale = Math.max(
|
||||
// 1 - move[0] / selectionWidth,
|
||||
// 1 - move[1] / selectionHeight,
|
||||
// );
|
||||
// const leftArrowBinding: {
|
||||
// elementId: string;
|
||||
// gap?: number;
|
||||
// focus?: number;
|
||||
// } = {
|
||||
// ...leftBoundArrow.endBinding,
|
||||
// } as PointBinding;
|
||||
// const rightArrowBinding: {
|
||||
// elementId: string;
|
||||
// gap?: number;
|
||||
// focus?: number;
|
||||
// } = {
|
||||
// ...rightBoundArrow.endBinding,
|
||||
// } as PointBinding;
|
||||
// delete rightArrowBinding.gap;
|
||||
|
||||
UI.resize([rectangle, rightBoundArrow], "nw", move, {
|
||||
shift: true,
|
||||
});
|
||||
// UI.resize([rectangle, rightBoundArrow], "nw", move, {
|
||||
// shift: true,
|
||||
// });
|
||||
|
||||
expect(leftBoundArrow.x).toBeCloseTo(-110);
|
||||
expect(leftBoundArrow.y).toBeCloseTo(50);
|
||||
expect(leftBoundArrow.width).toBeCloseTo(140, 0);
|
||||
expect(leftBoundArrow.height).toBeCloseTo(7, 0);
|
||||
expect(leftBoundArrow.angle).toEqual(0);
|
||||
expect(leftBoundArrow.startBinding).toBeNull();
|
||||
expect(leftBoundArrow.endBinding?.gap).toBeCloseTo(10);
|
||||
expect(leftBoundArrow.endBinding?.elementId).toBe(
|
||||
leftArrowBinding.elementId,
|
||||
);
|
||||
expect(leftBoundArrow.endBinding?.focus).toBe(leftArrowBinding.focus);
|
||||
// expect(leftBoundArrow.x).toBeCloseTo(-110);
|
||||
// expect(leftBoundArrow.y).toBeCloseTo(50);
|
||||
// expect(leftBoundArrow.width).toBeCloseTo(140, 0);
|
||||
// expect(leftBoundArrow.height).toBeCloseTo(7, 0);
|
||||
// expect(leftBoundArrow.angle).toEqual(0);
|
||||
// expect(leftBoundArrow.startBinding).toBeNull();
|
||||
// expect(leftBoundArrow.endBinding?.gap).toBeCloseTo(10);
|
||||
// expect(leftBoundArrow.endBinding?.elementId).toBe(
|
||||
// leftArrowBinding.elementId,
|
||||
// );
|
||||
// expect(leftBoundArrow.endBinding?.focus).toBe(leftArrowBinding.focus);
|
||||
|
||||
expect(rightBoundArrow.x).toBeCloseTo(210);
|
||||
expect(rightBoundArrow.y).toBeCloseTo(
|
||||
(selectionHeight - 50) * (1 - scale) + 50,
|
||||
);
|
||||
expect(rightBoundArrow.width).toBeCloseTo(100 * scale);
|
||||
expect(rightBoundArrow.height).toBeCloseTo(0);
|
||||
expect(rightBoundArrow.angle).toEqual(0);
|
||||
expect(rightBoundArrow.startBinding).toBeNull();
|
||||
expect(rightBoundArrow.endBinding?.gap).toBeCloseTo(8.0952);
|
||||
expect(rightBoundArrow.endBinding?.elementId).toBe(
|
||||
rightArrowBinding.elementId,
|
||||
);
|
||||
expect(rightBoundArrow.endBinding?.focus).toBeCloseTo(
|
||||
rightArrowBinding.focus!,
|
||||
);
|
||||
});
|
||||
// expect(rightBoundArrow.x).toBeCloseTo(210);
|
||||
// expect(rightBoundArrow.y).toBeCloseTo(
|
||||
// (selectionHeight - 50) * (1 - scale) + 50,
|
||||
// );
|
||||
// expect(rightBoundArrow.width).toBeCloseTo(100 * scale);
|
||||
// expect(rightBoundArrow.height).toBeCloseTo(0);
|
||||
// expect(rightBoundArrow.angle).toEqual(0);
|
||||
// expect(rightBoundArrow.startBinding).toBeNull();
|
||||
// expect(rightBoundArrow.endBinding?.gap).toBeCloseTo(8.0952);
|
||||
// expect(rightBoundArrow.endBinding?.elementId).toBe(
|
||||
// rightArrowBinding.elementId,
|
||||
// );
|
||||
// expect(rightBoundArrow.endBinding?.focus).toBeCloseTo(
|
||||
// rightArrowBinding.focus!,
|
||||
// );
|
||||
// });
|
||||
|
||||
it("resizes with labeled arrows", async () => {
|
||||
const topArrow = UI.createElement("arrow", {
|
||||
@@ -1338,8 +1350,8 @@ describe("multiple selection", () => {
|
||||
|
||||
expect(boundArrow.x).toBeCloseTo(380 * scaleX);
|
||||
expect(boundArrow.y).toBeCloseTo(240 * scaleY);
|
||||
expect(boundArrow.points[1][0]).toBeCloseTo(-60 * scaleX);
|
||||
expect(boundArrow.points[1][1]).toBeCloseTo(-80 * scaleY);
|
||||
expect(boundArrow.points[1][0]).toBeCloseTo(59.7979);
|
||||
expect(boundArrow.points[1][1]).toBeCloseTo(-79.7305);
|
||||
|
||||
expect(arrowLabelPos.x + arrowLabel.width / 2).toBeCloseTo(
|
||||
boundArrow.x + boundArrow.points[1][0] / 2,
|
||||
|
||||
@@ -51,7 +51,7 @@ import { register } from "./register";
|
||||
|
||||
import type { AppState, Offsets } from "../types";
|
||||
|
||||
export const actionChangeViewBackgroundColor = register({
|
||||
export const actionChangeViewBackgroundColor = register<Partial<AppState>>({
|
||||
name: "changeViewBackgroundColor",
|
||||
label: "labels.canvasBackground",
|
||||
trackEvent: false,
|
||||
@@ -64,7 +64,7 @@ export const actionChangeViewBackgroundColor = register({
|
||||
perform: (_, appState, value) => {
|
||||
return {
|
||||
appState: { ...appState, ...value },
|
||||
captureUpdate: !!value.viewBackgroundColor
|
||||
captureUpdate: !!value?.viewBackgroundColor
|
||||
? CaptureUpdateAction.IMMEDIATELY
|
||||
: CaptureUpdateAction.EVENTUALLY,
|
||||
};
|
||||
@@ -466,7 +466,7 @@ export const actionZoomToFit = register({
|
||||
!event[KEYS.CTRL_OR_CMD],
|
||||
});
|
||||
|
||||
export const actionToggleTheme = register({
|
||||
export const actionToggleTheme = register<AppState["theme"]>({
|
||||
name: "toggleTheme",
|
||||
label: (_, appState) => {
|
||||
return appState.theme === THEME.DARK
|
||||
@@ -474,7 +474,8 @@ export const actionToggleTheme = register({
|
||||
: "buttons.darkMode";
|
||||
},
|
||||
keywords: ["toggle", "dark", "light", "mode", "theme"],
|
||||
icon: (appState) => (appState.theme === THEME.LIGHT ? MoonIcon : SunIcon),
|
||||
icon: (appState, elements) =>
|
||||
appState.theme === THEME.LIGHT ? MoonIcon : SunIcon,
|
||||
viewMode: true,
|
||||
trackEvent: { category: "canvas" },
|
||||
perform: (_, appState, value) => {
|
||||
|
||||
@@ -20,12 +20,12 @@ import { t } from "../i18n";
|
||||
import { actionDeleteSelected } from "./actionDeleteSelected";
|
||||
import { register } from "./register";
|
||||
|
||||
export const actionCopy = register({
|
||||
export const actionCopy = register<ClipboardEvent | null>({
|
||||
name: "copy",
|
||||
label: "labels.copy",
|
||||
icon: DuplicateIcon,
|
||||
trackEvent: { category: "element" },
|
||||
perform: async (elements, appState, event: ClipboardEvent | null, app) => {
|
||||
perform: async (elements, appState, event, app) => {
|
||||
const elementsToCopy = app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
includeBoundTextElement: true,
|
||||
@@ -109,12 +109,12 @@ export const actionPaste = register({
|
||||
keyTest: undefined,
|
||||
});
|
||||
|
||||
export const actionCut = register({
|
||||
export const actionCut = register<ClipboardEvent | null>({
|
||||
name: "cut",
|
||||
label: "labels.cut",
|
||||
icon: cutIcon,
|
||||
trackEvent: { category: "element" },
|
||||
perform: (elements, appState, event: ClipboardEvent | null, app) => {
|
||||
perform: (elements, appState, event, app) => {
|
||||
actionCopy.perform(elements, appState, event, app);
|
||||
return actionDeleteSelected.perform(elements, appState, null, app);
|
||||
},
|
||||
|
||||
@@ -212,12 +212,8 @@ export const actionDeleteSelected = register({
|
||||
trackEvent: { category: "element", action: "delete" },
|
||||
perform: (elements, appState, formData, app) => {
|
||||
if (appState.selectedLinearElement?.isEditing) {
|
||||
const {
|
||||
elementId,
|
||||
selectedPointsIndices,
|
||||
startBindingElement,
|
||||
endBindingElement,
|
||||
} = appState.selectedLinearElement;
|
||||
const { elementId, selectedPointsIndices } =
|
||||
appState.selectedLinearElement;
|
||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||
const linearElement = LinearElementEditor.getElement(
|
||||
elementId,
|
||||
@@ -254,19 +250,6 @@ export const actionDeleteSelected = register({
|
||||
};
|
||||
}
|
||||
|
||||
// We cannot do this inside `movePoint` because it is also called
|
||||
// when deleting the uncommitted point (which hasn't caused any binding)
|
||||
const binding = {
|
||||
startBindingElement: selectedPointsIndices?.includes(0)
|
||||
? null
|
||||
: startBindingElement,
|
||||
endBindingElement: selectedPointsIndices?.includes(
|
||||
linearElement.points.length - 1,
|
||||
)
|
||||
? null
|
||||
: endBindingElement,
|
||||
};
|
||||
|
||||
LinearElementEditor.deletePoints(
|
||||
linearElement,
|
||||
app,
|
||||
@@ -279,7 +262,6 @@ export const actionDeleteSelected = register({
|
||||
...appState,
|
||||
selectedLinearElement: {
|
||||
...appState.selectedLinearElement,
|
||||
...binding,
|
||||
selectedPointsIndices:
|
||||
selectedPointsIndices?.[0] > 0
|
||||
? [selectedPointsIndices[0] - 1]
|
||||
@@ -308,6 +290,7 @@ export const actionDeleteSelected = register({
|
||||
type: app.state.preferredSelectionTool.type,
|
||||
}),
|
||||
multiElement: null,
|
||||
newElement: null,
|
||||
activeEmbeddable: null,
|
||||
selectedLinearElement: null,
|
||||
},
|
||||
|
||||
@@ -31,7 +31,9 @@ import "../components/ToolIcon.scss";
|
||||
|
||||
import { register } from "./register";
|
||||
|
||||
export const actionChangeProjectName = register({
|
||||
import type { AppState } from "../types";
|
||||
|
||||
export const actionChangeProjectName = register<AppState["name"]>({
|
||||
name: "changeProjectName",
|
||||
label: "labels.fileTitle",
|
||||
trackEvent: false,
|
||||
@@ -51,7 +53,7 @@ export const actionChangeProjectName = register({
|
||||
),
|
||||
});
|
||||
|
||||
export const actionChangeExportScale = register({
|
||||
export const actionChangeExportScale = register<AppState["exportScale"]>({
|
||||
name: "changeExportScale",
|
||||
label: "imageExportDialog.scale",
|
||||
trackEvent: { category: "export", action: "scale" },
|
||||
@@ -101,7 +103,9 @@ export const actionChangeExportScale = register({
|
||||
},
|
||||
});
|
||||
|
||||
export const actionChangeExportBackground = register({
|
||||
export const actionChangeExportBackground = register<
|
||||
AppState["exportBackground"]
|
||||
>({
|
||||
name: "changeExportBackground",
|
||||
label: "imageExportDialog.label.withBackground",
|
||||
trackEvent: { category: "export", action: "toggleBackground" },
|
||||
@@ -121,7 +125,9 @@ export const actionChangeExportBackground = register({
|
||||
),
|
||||
});
|
||||
|
||||
export const actionChangeExportEmbedScene = register({
|
||||
export const actionChangeExportEmbedScene = register<
|
||||
AppState["exportEmbedScene"]
|
||||
>({
|
||||
name: "changeExportEmbedScene",
|
||||
label: "imageExportDialog.tooltip.embedScene",
|
||||
trackEvent: { category: "export", action: "embedScene" },
|
||||
@@ -288,7 +294,9 @@ export const actionLoadScene = register({
|
||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
|
||||
});
|
||||
|
||||
export const actionExportWithDarkMode = register({
|
||||
export const actionExportWithDarkMode = register<
|
||||
AppState["exportWithDarkMode"]
|
||||
>({
|
||||
name: "exportWithDarkMode",
|
||||
label: "imageExportDialog.label.darkMode",
|
||||
trackEvent: { category: "export", action: "toggleTheme" },
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { pointFrom } from "@excalidraw/math";
|
||||
|
||||
import {
|
||||
maybeBindLinearElement,
|
||||
bindOrUnbindLinearElement,
|
||||
isBindingEnabled,
|
||||
} from "@excalidraw/element/binding";
|
||||
import { bindOrUnbindBindingElement } from "@excalidraw/element/binding";
|
||||
import {
|
||||
isValidPolygon,
|
||||
LinearElementEditor,
|
||||
@@ -21,7 +17,7 @@ import {
|
||||
import {
|
||||
KEYS,
|
||||
arrayToMap,
|
||||
tupleToCoors,
|
||||
invariant,
|
||||
updateActiveTool,
|
||||
} from "@excalidraw/common";
|
||||
import { isPathALoop } from "@excalidraw/element";
|
||||
@@ -30,11 +26,12 @@ import { isInvisiblySmallElement } from "@excalidraw/element";
|
||||
|
||||
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||
|
||||
import type { LocalPoint } from "@excalidraw/math";
|
||||
import type { GlobalPoint, LocalPoint } from "@excalidraw/math";
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
NonDeleted,
|
||||
PointsPositionUpdates,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import { t } from "../i18n";
|
||||
@@ -46,20 +43,37 @@ import { register } from "./register";
|
||||
|
||||
import type { AppState } from "../types";
|
||||
|
||||
export const actionFinalize = register({
|
||||
type FormData = {
|
||||
event: PointerEvent;
|
||||
sceneCoords: { x: number; y: number };
|
||||
};
|
||||
|
||||
export const actionFinalize = register<FormData>({
|
||||
name: "finalize",
|
||||
label: "",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, data, app) => {
|
||||
let newElements = elements;
|
||||
const { interactiveCanvas, focusContainer, scene } = app;
|
||||
const { event, sceneCoords } =
|
||||
(data as {
|
||||
event?: PointerEvent;
|
||||
sceneCoords?: { x: number; y: number };
|
||||
}) ?? {};
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
|
||||
if (event && appState.selectedLinearElement) {
|
||||
if (data && appState.selectedLinearElement) {
|
||||
const { event, sceneCoords } = data;
|
||||
const element = LinearElementEditor.getElement(
|
||||
appState.selectedLinearElement.elementId,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
invariant(
|
||||
element,
|
||||
"Arrow element should exist if selectedLinearElement is set",
|
||||
);
|
||||
|
||||
invariant(
|
||||
sceneCoords,
|
||||
"sceneCoords should be defined if actionFinalize is called with event",
|
||||
);
|
||||
|
||||
const linearElementEditor = LinearElementEditor.handlePointerUp(
|
||||
event,
|
||||
appState.selectedLinearElement,
|
||||
@@ -67,19 +81,48 @@ export const actionFinalize = register({
|
||||
app.scene,
|
||||
);
|
||||
|
||||
const { startBindingElement, endBindingElement } = linearElementEditor;
|
||||
const element = app.scene.getElement(linearElementEditor.elementId);
|
||||
if (isBindingElement(element)) {
|
||||
bindOrUnbindLinearElement(
|
||||
element,
|
||||
startBindingElement,
|
||||
endBindingElement,
|
||||
app.scene,
|
||||
);
|
||||
const newArrow = !!appState.newElement;
|
||||
|
||||
const selectedPointsIndices =
|
||||
newArrow || !appState.selectedLinearElement.selectedPointsIndices
|
||||
? [element.points.length - 1] // New arrow creation
|
||||
: appState.selectedLinearElement.selectedPointsIndices;
|
||||
|
||||
const draggedPoints: PointsPositionUpdates =
|
||||
selectedPointsIndices.reduce((map, index) => {
|
||||
map.set(index, {
|
||||
point: LinearElementEditor.pointFromAbsoluteCoords(
|
||||
element,
|
||||
pointFrom<GlobalPoint>(sceneCoords.x, sceneCoords.y),
|
||||
elementsMap,
|
||||
),
|
||||
});
|
||||
|
||||
return map;
|
||||
}, new Map()) ?? new Map();
|
||||
|
||||
bindOrUnbindBindingElement(element, draggedPoints, scene, appState, {
|
||||
newArrow,
|
||||
altKey: event.altKey,
|
||||
});
|
||||
} else if (isLineElement(element)) {
|
||||
if (
|
||||
appState.selectedLinearElement?.isEditing &&
|
||||
!appState.newElement &&
|
||||
!isValidPolygon(element.points)
|
||||
) {
|
||||
scene.mutateElement(element, {
|
||||
polygon: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (linearElementEditor !== appState.selectedLinearElement) {
|
||||
let newElements = elements;
|
||||
// `handlePointerUp()` updated the linear element instance,
|
||||
// so filter out this element if it is too small,
|
||||
// but do an update to all new elements anyway for undo/redo purposes.
|
||||
|
||||
if (element && isInvisiblySmallElement(element)) {
|
||||
// TODO: #7348 in theory this gets recorded by the store, so the invisible elements could be restored by the undo/redo, which might be not what we would want
|
||||
newElements = newElements.map((el) => {
|
||||
@@ -91,39 +134,8 @@ export const actionFinalize = register({
|
||||
return el;
|
||||
});
|
||||
}
|
||||
return {
|
||||
elements: newElements,
|
||||
appState: {
|
||||
selectedLinearElement: {
|
||||
...linearElementEditor,
|
||||
selectedPointsIndices: null,
|
||||
},
|
||||
suggestedBindings: [],
|
||||
},
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (appState.selectedLinearElement?.isEditing) {
|
||||
const { elementId, startBindingElement, endBindingElement } =
|
||||
appState.selectedLinearElement;
|
||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||
|
||||
if (element) {
|
||||
if (isBindingElement(element)) {
|
||||
bindOrUnbindLinearElement(
|
||||
element,
|
||||
startBindingElement,
|
||||
endBindingElement,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
if (isLineElement(element) && !isValidPolygon(element.points)) {
|
||||
scene.mutateElement(element, {
|
||||
polygon: false,
|
||||
});
|
||||
}
|
||||
const activeToolLocked = appState.activeTool?.locked;
|
||||
|
||||
return {
|
||||
elements:
|
||||
@@ -134,23 +146,31 @@ export const actionFinalize = register({
|
||||
}
|
||||
return el;
|
||||
})
|
||||
: undefined,
|
||||
: newElements,
|
||||
appState: {
|
||||
...appState,
|
||||
cursorButton: "up",
|
||||
selectedLinearElement: new LinearElementEditor(
|
||||
element,
|
||||
arrayToMap(elementsMap),
|
||||
false, // exit editing mode
|
||||
),
|
||||
selectedLinearElement: activeToolLocked
|
||||
? null
|
||||
: {
|
||||
...linearElementEditor,
|
||||
selectedPointsIndices: null,
|
||||
isEditing: false,
|
||||
initialState: {
|
||||
...linearElementEditor.initialState,
|
||||
lastClickedPoint: -1,
|
||||
},
|
||||
},
|
||||
selectionElement: null,
|
||||
suggestedBinding: null,
|
||||
newElement: null,
|
||||
multiElement: null,
|
||||
},
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let newElements = elements;
|
||||
|
||||
if (window.document.activeElement instanceof HTMLElement) {
|
||||
focusContainer();
|
||||
}
|
||||
@@ -174,8 +194,14 @@ export const actionFinalize = register({
|
||||
|
||||
if (element) {
|
||||
// pen and mouse have hover
|
||||
if (appState.multiElement && element.type !== "freedraw") {
|
||||
const { points, lastCommittedPoint } = element;
|
||||
if (
|
||||
appState.selectedLinearElement &&
|
||||
appState.multiElement &&
|
||||
element.type !== "freedraw" &&
|
||||
appState.lastPointerDownWith !== "touch"
|
||||
) {
|
||||
const { points } = element;
|
||||
const { lastCommittedPoint } = appState.selectedLinearElement;
|
||||
if (
|
||||
!lastCommittedPoint ||
|
||||
points[points.length - 1] !== lastCommittedPoint
|
||||
@@ -227,25 +253,6 @@ export const actionFinalize = register({
|
||||
polygon: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
isBindingElement(element) &&
|
||||
!isLoop &&
|
||||
element.points.length > 1 &&
|
||||
isBindingEnabled(appState)
|
||||
) {
|
||||
const coords =
|
||||
sceneCoords ??
|
||||
tupleToCoors(
|
||||
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
element,
|
||||
-1,
|
||||
arrayToMap(elements),
|
||||
),
|
||||
);
|
||||
|
||||
maybeBindLinearElement(element, appState, coords, scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,6 +278,25 @@ export const actionFinalize = register({
|
||||
});
|
||||
}
|
||||
|
||||
let selectedLinearElement =
|
||||
element && isLinearElement(element)
|
||||
? new LinearElementEditor(element, arrayToMap(newElements)) // To select the linear element when user has finished mutipoint editing
|
||||
: appState.selectedLinearElement;
|
||||
|
||||
selectedLinearElement = selectedLinearElement
|
||||
? {
|
||||
...selectedLinearElement,
|
||||
isEditing: appState.newElement
|
||||
? false
|
||||
: selectedLinearElement.isEditing,
|
||||
initialState: {
|
||||
...selectedLinearElement.initialState,
|
||||
lastClickedPoint: -1,
|
||||
origin: null,
|
||||
},
|
||||
}
|
||||
: selectedLinearElement;
|
||||
|
||||
return {
|
||||
elements: newElements,
|
||||
appState: {
|
||||
@@ -288,7 +314,7 @@ export const actionFinalize = register({
|
||||
multiElement: null,
|
||||
editingTextElement: null,
|
||||
startBoundElement: null,
|
||||
suggestedBindings: [],
|
||||
suggestedBinding: null,
|
||||
selectedElementIds:
|
||||
element &&
|
||||
!appState.activeTool.locked &&
|
||||
@@ -298,11 +324,8 @@ export const actionFinalize = register({
|
||||
[element.id]: true,
|
||||
}
|
||||
: appState.selectedElementIds,
|
||||
// To select the linear element when user has finished mutipoint editing
|
||||
selectedLinearElement:
|
||||
element && isLinearElement(element)
|
||||
? new LinearElementEditor(element, arrayToMap(newElements))
|
||||
: appState.selectedLinearElement,
|
||||
|
||||
selectedLinearElement,
|
||||
},
|
||||
// TODO: #7348 we should not capture everything, but if we don't, it leads to incosistencies -> revisit
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
|
||||
@@ -38,15 +38,13 @@ describe("flipping re-centers selection", () => {
|
||||
height: 239.9,
|
||||
startBinding: {
|
||||
elementId: "rec1",
|
||||
focus: 0,
|
||||
gap: 5,
|
||||
fixedPoint: [0.49, -0.05],
|
||||
mode: "orbit",
|
||||
},
|
||||
endBinding: {
|
||||
elementId: "rec2",
|
||||
focus: 0,
|
||||
gap: 5,
|
||||
fixedPoint: [-0.05, 0.49],
|
||||
mode: "orbit",
|
||||
},
|
||||
startArrowhead: null,
|
||||
endArrowhead: "arrow",
|
||||
@@ -74,11 +72,11 @@ describe("flipping re-centers selection", () => {
|
||||
|
||||
const rec1 = h.elements.find((el) => el.id === "rec1")!;
|
||||
expect(rec1.x).toBeCloseTo(100, 0);
|
||||
expect(rec1.y).toBeCloseTo(100, 0);
|
||||
expect(rec1.y).toBeCloseTo(101, 0);
|
||||
|
||||
const rec2 = h.elements.find((el) => el.id === "rec2")!;
|
||||
expect(rec2.x).toBeCloseTo(220, 0);
|
||||
expect(rec2.y).toBeCloseTo(250, 0);
|
||||
expect(rec2.y).toBeCloseTo(251, 0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,8 +97,8 @@ describe("flipping arrowheads", () => {
|
||||
endArrowhead: null,
|
||||
endBinding: {
|
||||
elementId: rect.id,
|
||||
focus: 0.5,
|
||||
gap: 5,
|
||||
fixedPoint: [0.5, 0.5],
|
||||
mode: "orbit",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -139,13 +137,13 @@ describe("flipping arrowheads", () => {
|
||||
endArrowhead: "circle",
|
||||
startBinding: {
|
||||
elementId: rect.id,
|
||||
focus: 0.5,
|
||||
gap: 5,
|
||||
fixedPoint: [0.5, 0.5],
|
||||
mode: "orbit",
|
||||
},
|
||||
endBinding: {
|
||||
elementId: rect2.id,
|
||||
focus: 0.5,
|
||||
gap: 5,
|
||||
fixedPoint: [0.5, 0.5],
|
||||
mode: "orbit",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -195,8 +193,8 @@ describe("flipping arrowheads", () => {
|
||||
endArrowhead: null,
|
||||
endBinding: {
|
||||
elementId: rect.id,
|
||||
focus: 0.5,
|
||||
gap: 5,
|
||||
fixedPoint: [0.5, 0.5],
|
||||
mode: "orbit",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import { getNonDeletedElements } from "@excalidraw/element";
|
||||
import {
|
||||
bindOrUnbindLinearElements,
|
||||
isBindingEnabled,
|
||||
} from "@excalidraw/element";
|
||||
import { bindOrUnbindBindingElements } from "@excalidraw/element";
|
||||
import { getCommonBoundingBox } from "@excalidraw/element";
|
||||
import { newElementWith } from "@excalidraw/element";
|
||||
import { deepCopyElement } from "@excalidraw/element";
|
||||
import { resizeMultipleElements } from "@excalidraw/element";
|
||||
import {
|
||||
isArrowElement,
|
||||
isElbowArrow,
|
||||
isLinearElement,
|
||||
} from "@excalidraw/element";
|
||||
import { isArrowElement, isElbowArrow } from "@excalidraw/element";
|
||||
import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element";
|
||||
import { CODES, KEYS, arrayToMap } from "@excalidraw/common";
|
||||
|
||||
@@ -103,7 +96,6 @@ const flipSelectedElements = (
|
||||
const updatedElements = flipElements(
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
appState,
|
||||
flipDirection,
|
||||
app,
|
||||
);
|
||||
@@ -118,7 +110,6 @@ const flipSelectedElements = (
|
||||
const flipElements = (
|
||||
selectedElements: NonDeleted<ExcalidrawElement>[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
appState: AppState,
|
||||
flipDirection: "horizontal" | "vertical",
|
||||
app: AppClassProperties,
|
||||
): ExcalidrawElement[] => {
|
||||
@@ -158,12 +149,10 @@ const flipElements = (
|
||||
},
|
||||
);
|
||||
|
||||
bindOrUnbindLinearElements(
|
||||
selectedElements.filter(isLinearElement),
|
||||
isBindingEnabled(appState),
|
||||
[],
|
||||
bindOrUnbindBindingElements(
|
||||
selectedElements.filter(isArrowElement),
|
||||
app.scene,
|
||||
appState.zoom,
|
||||
app.state,
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -2,6 +2,8 @@ import clsx from "clsx";
|
||||
|
||||
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||
|
||||
import { invariant } from "@excalidraw/common";
|
||||
|
||||
import { getClientColor } from "../clients";
|
||||
import { Avatar } from "../components/Avatar";
|
||||
import {
|
||||
@@ -16,12 +18,17 @@ import { register } from "./register";
|
||||
import type { GoToCollaboratorComponentProps } from "../components/UserList";
|
||||
import type { Collaborator } from "../types";
|
||||
|
||||
export const actionGoToCollaborator = register({
|
||||
export const actionGoToCollaborator = register<Collaborator>({
|
||||
name: "goToCollaborator",
|
||||
label: "Go to a collaborator",
|
||||
viewMode: true,
|
||||
trackEvent: { category: "collab" },
|
||||
perform: (_elements, appState, collaborator: Collaborator) => {
|
||||
perform: (_elements, appState, collaborator) => {
|
||||
invariant(
|
||||
collaborator,
|
||||
"actionGoToCollaborator: collaborator should be defined when actionGoToCollaborator is called",
|
||||
);
|
||||
|
||||
if (
|
||||
!collaborator.socketId ||
|
||||
appState.userToFollow?.socketId === collaborator.socketId ||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { pointFrom } from "@excalidraw/math";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
@@ -20,12 +21,13 @@ import {
|
||||
getLineHeight,
|
||||
isTransparent,
|
||||
reduceToCommonValue,
|
||||
invariant,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { canBecomePolygon, getNonDeletedElements } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
bindLinearElement,
|
||||
bindBindingElement,
|
||||
calculateFixedPointForElbowArrowBinding,
|
||||
updateBoundElements,
|
||||
} from "@excalidraw/element";
|
||||
@@ -59,7 +61,7 @@ import {
|
||||
|
||||
import { deriveStylesPanelMode } from "@excalidraw/common";
|
||||
|
||||
import type { LocalPoint } from "@excalidraw/math";
|
||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
Arrowhead,
|
||||
@@ -306,13 +308,15 @@ const changeFontSize = (
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const actionChangeStrokeColor = register({
|
||||
export const actionChangeStrokeColor = register<
|
||||
Pick<AppState, "currentItemStrokeColor">
|
||||
>({
|
||||
name: "changeStrokeColor",
|
||||
label: "labels.stroke",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, value) => {
|
||||
return {
|
||||
...(value.currentItemStrokeColor && {
|
||||
...(value?.currentItemStrokeColor && {
|
||||
elements: changeProperty(
|
||||
elements,
|
||||
appState,
|
||||
@@ -330,7 +334,7 @@ export const actionChangeStrokeColor = register({
|
||||
...appState,
|
||||
...value,
|
||||
},
|
||||
captureUpdate: !!value.currentItemStrokeColor
|
||||
captureUpdate: !!value?.currentItemStrokeColor
|
||||
? CaptureUpdateAction.IMMEDIATELY
|
||||
: CaptureUpdateAction.EVENTUALLY,
|
||||
};
|
||||
@@ -366,12 +370,14 @@ export const actionChangeStrokeColor = register({
|
||||
},
|
||||
});
|
||||
|
||||
export const actionChangeBackgroundColor = register({
|
||||
export const actionChangeBackgroundColor = register<
|
||||
Pick<AppState, "currentItemBackgroundColor" | "viewBackgroundColor">
|
||||
>({
|
||||
name: "changeBackgroundColor",
|
||||
label: "labels.changeBackground",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, value, app) => {
|
||||
if (!value.currentItemBackgroundColor) {
|
||||
if (!value?.currentItemBackgroundColor) {
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
@@ -451,7 +457,7 @@ export const actionChangeBackgroundColor = register({
|
||||
},
|
||||
});
|
||||
|
||||
export const actionChangeFillStyle = register({
|
||||
export const actionChangeFillStyle = register<ExcalidrawElement["fillStyle"]>({
|
||||
name: "changeFillStyle",
|
||||
label: "labels.fill",
|
||||
trackEvent: false,
|
||||
@@ -533,7 +539,9 @@ export const actionChangeFillStyle = register({
|
||||
},
|
||||
});
|
||||
|
||||
export const actionChangeStrokeWidth = register({
|
||||
export const actionChangeStrokeWidth = register<
|
||||
ExcalidrawElement["strokeWidth"]
|
||||
>({
|
||||
name: "changeStrokeWidth",
|
||||
label: "labels.strokeWidth",
|
||||
trackEvent: false,
|
||||
@@ -589,7 +597,7 @@ export const actionChangeStrokeWidth = register({
|
||||
),
|
||||
});
|
||||
|
||||
export const actionChangeSloppiness = register({
|
||||
export const actionChangeSloppiness = register<ExcalidrawElement["roughness"]>({
|
||||
name: "changeSloppiness",
|
||||
label: "labels.sloppiness",
|
||||
trackEvent: false,
|
||||
@@ -643,7 +651,9 @@ export const actionChangeSloppiness = register({
|
||||
),
|
||||
});
|
||||
|
||||
export const actionChangeStrokeStyle = register({
|
||||
export const actionChangeStrokeStyle = register<
|
||||
ExcalidrawElement["strokeStyle"]
|
||||
>({
|
||||
name: "changeStrokeStyle",
|
||||
label: "labels.strokeStyle",
|
||||
trackEvent: false,
|
||||
@@ -696,7 +706,7 @@ export const actionChangeStrokeStyle = register({
|
||||
),
|
||||
});
|
||||
|
||||
export const actionChangeOpacity = register({
|
||||
export const actionChangeOpacity = register<ExcalidrawElement["opacity"]>({
|
||||
name: "changeOpacity",
|
||||
label: "labels.opacity",
|
||||
trackEvent: false,
|
||||
@@ -720,89 +730,100 @@ export const actionChangeOpacity = register({
|
||||
),
|
||||
});
|
||||
|
||||
export const actionChangeFontSize = register({
|
||||
name: "changeFontSize",
|
||||
label: "labels.fontSize",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, value, app) => {
|
||||
return changeFontSize(elements, appState, app, () => value, value);
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData, app, data }) => {
|
||||
const { isCompact } = getStylesPanelInfo(app);
|
||||
export const actionChangeFontSize = register<ExcalidrawTextElement["fontSize"]>(
|
||||
{
|
||||
name: "changeFontSize",
|
||||
label: "labels.fontSize",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, value, app) => {
|
||||
return changeFontSize(
|
||||
elements,
|
||||
appState,
|
||||
app,
|
||||
() => {
|
||||
invariant(value, "actionChangeFontSize: Expected a font size value");
|
||||
return value;
|
||||
},
|
||||
value,
|
||||
);
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData, app, data }) => {
|
||||
const { isCompact } = getStylesPanelInfo(app);
|
||||
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>{t("labels.fontSize")}</legend>
|
||||
<div className="buttonList">
|
||||
<RadioSelection
|
||||
group="font-size"
|
||||
options={[
|
||||
{
|
||||
value: 16,
|
||||
text: t("labels.small"),
|
||||
icon: FontSizeSmallIcon,
|
||||
testId: "fontSize-small",
|
||||
},
|
||||
{
|
||||
value: 20,
|
||||
text: t("labels.medium"),
|
||||
icon: FontSizeMediumIcon,
|
||||
testId: "fontSize-medium",
|
||||
},
|
||||
{
|
||||
value: 28,
|
||||
text: t("labels.large"),
|
||||
icon: FontSizeLargeIcon,
|
||||
testId: "fontSize-large",
|
||||
},
|
||||
{
|
||||
value: 36,
|
||||
text: t("labels.veryLarge"),
|
||||
icon: FontSizeExtraLargeIcon,
|
||||
testId: "fontSize-veryLarge",
|
||||
},
|
||||
]}
|
||||
value={getFormValue(
|
||||
elements,
|
||||
app,
|
||||
(element) => {
|
||||
if (isTextElement(element)) {
|
||||
return element.fontSize;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>{t("labels.fontSize")}</legend>
|
||||
<div className="buttonList">
|
||||
<RadioSelection
|
||||
group="font-size"
|
||||
options={[
|
||||
{
|
||||
value: 16,
|
||||
text: t("labels.small"),
|
||||
icon: FontSizeSmallIcon,
|
||||
testId: "fontSize-small",
|
||||
},
|
||||
{
|
||||
value: 20,
|
||||
text: t("labels.medium"),
|
||||
icon: FontSizeMediumIcon,
|
||||
testId: "fontSize-medium",
|
||||
},
|
||||
{
|
||||
value: 28,
|
||||
text: t("labels.large"),
|
||||
icon: FontSizeLargeIcon,
|
||||
testId: "fontSize-large",
|
||||
},
|
||||
{
|
||||
value: 36,
|
||||
text: t("labels.veryLarge"),
|
||||
icon: FontSizeExtraLargeIcon,
|
||||
testId: "fontSize-veryLarge",
|
||||
},
|
||||
]}
|
||||
value={getFormValue(
|
||||
elements,
|
||||
app,
|
||||
(element) => {
|
||||
if (isTextElement(element)) {
|
||||
return element.fontSize;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (boundTextElement) {
|
||||
return boundTextElement.fontSize;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
(element) =>
|
||||
isTextElement(element) ||
|
||||
getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
) !== null,
|
||||
(hasSelection) =>
|
||||
hasSelection
|
||||
? null
|
||||
: appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
||||
)}
|
||||
onChange={(value) => {
|
||||
withCaretPositionPreservation(
|
||||
() => updateData(value),
|
||||
isCompact,
|
||||
!!appState.editingTextElement,
|
||||
data?.onPreventClose,
|
||||
);
|
||||
if (boundTextElement) {
|
||||
return boundTextElement.fontSize;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
(element) =>
|
||||
isTextElement(element) ||
|
||||
getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
) !== null,
|
||||
(hasSelection) =>
|
||||
hasSelection
|
||||
? null
|
||||
: appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
||||
)}
|
||||
onChange={(value) => {
|
||||
withCaretPositionPreservation(
|
||||
() => updateData(value),
|
||||
isCompact,
|
||||
!!appState.editingTextElement,
|
||||
data?.onPreventClose,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
export const actionDecreaseFontSize = register({
|
||||
name: "decreaseFontSize",
|
||||
@@ -862,7 +883,10 @@ type ChangeFontFamilyData = Partial<
|
||||
resetContainers?: true;
|
||||
};
|
||||
|
||||
export const actionChangeFontFamily = register({
|
||||
export const actionChangeFontFamily = register<{
|
||||
currentItemFontFamily: any;
|
||||
currentHoveredFontFamily: any;
|
||||
}>({
|
||||
name: "changeFontFamily",
|
||||
label: "labels.fontFamily",
|
||||
trackEvent: false,
|
||||
@@ -899,6 +923,8 @@ export const actionChangeFontFamily = register({
|
||||
};
|
||||
}
|
||||
|
||||
invariant(value, "actionChangeFontFamily: value must be defined");
|
||||
|
||||
const { currentItemFontFamily, currentHoveredFontFamily } = value;
|
||||
|
||||
let nextCaptureUpdateAction: CaptureUpdateActionType =
|
||||
@@ -1241,7 +1267,7 @@ export const actionChangeFontFamily = register({
|
||||
},
|
||||
});
|
||||
|
||||
export const actionChangeTextAlign = register({
|
||||
export const actionChangeTextAlign = register<TextAlign>({
|
||||
name: "changeTextAlign",
|
||||
label: "Change text alignment",
|
||||
trackEvent: false,
|
||||
@@ -1342,7 +1368,7 @@ export const actionChangeTextAlign = register({
|
||||
},
|
||||
});
|
||||
|
||||
export const actionChangeVerticalAlign = register({
|
||||
export const actionChangeVerticalAlign = register<VerticalAlign>({
|
||||
name: "changeVerticalAlign",
|
||||
label: "Change vertical alignment",
|
||||
trackEvent: { category: "element" },
|
||||
@@ -1442,7 +1468,7 @@ export const actionChangeVerticalAlign = register({
|
||||
},
|
||||
});
|
||||
|
||||
export const actionChangeRoundness = register({
|
||||
export const actionChangeRoundness = register<"sharp" | "round">({
|
||||
name: "changeRoundness",
|
||||
label: "Change edge roundness",
|
||||
trackEvent: false,
|
||||
@@ -1599,15 +1625,16 @@ const getArrowheadOptions = (flip: boolean) => {
|
||||
] as const;
|
||||
};
|
||||
|
||||
export const actionChangeArrowhead = register({
|
||||
export const actionChangeArrowhead = register<{
|
||||
position: "start" | "end";
|
||||
type: Arrowhead;
|
||||
}>({
|
||||
name: "changeArrowhead",
|
||||
label: "Change arrowheads",
|
||||
trackEvent: false,
|
||||
perform: (
|
||||
elements,
|
||||
appState,
|
||||
value: { position: "start" | "end"; type: Arrowhead },
|
||||
) => {
|
||||
perform: (elements, appState, value) => {
|
||||
invariant(value, "actionChangeArrowhead: value must be defined");
|
||||
|
||||
return {
|
||||
elements: changeProperty(elements, appState, (el) => {
|
||||
if (isLinearElement(el)) {
|
||||
@@ -1702,7 +1729,7 @@ export const actionChangeArrowProperties = register({
|
||||
},
|
||||
});
|
||||
|
||||
export const actionChangeArrowType = register({
|
||||
export const actionChangeArrowType = register<keyof typeof ARROW_TYPE>({
|
||||
name: "changeArrowType",
|
||||
label: "Change arrow types",
|
||||
trackEvent: false,
|
||||
@@ -1711,7 +1738,20 @@ export const actionChangeArrowType = register({
|
||||
if (!isArrowElement(el)) {
|
||||
return el;
|
||||
}
|
||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||
const startPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
el,
|
||||
0,
|
||||
elementsMap,
|
||||
);
|
||||
const endPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
el,
|
||||
-1,
|
||||
elementsMap,
|
||||
);
|
||||
let newElement = newElementWith(el, {
|
||||
x: value === ARROW_TYPE.elbow ? startPoint[0] : el.x,
|
||||
y: value === ARROW_TYPE.elbow ? startPoint[1] : el.y,
|
||||
roundness:
|
||||
value === ARROW_TYPE.round
|
||||
? {
|
||||
@@ -1719,9 +1759,31 @@ export const actionChangeArrowType = register({
|
||||
}
|
||||
: null,
|
||||
elbowed: value === ARROW_TYPE.elbow,
|
||||
angle: value === ARROW_TYPE.elbow ? (0 as Radians) : el.angle,
|
||||
points:
|
||||
value === ARROW_TYPE.elbow || el.elbowed
|
||||
? [el.points[0], el.points[el.points.length - 1]]
|
||||
? [
|
||||
LinearElementEditor.pointFromAbsoluteCoords(
|
||||
{
|
||||
...el,
|
||||
x: startPoint[0],
|
||||
y: startPoint[1],
|
||||
angle: 0 as Radians,
|
||||
},
|
||||
startPoint,
|
||||
elementsMap,
|
||||
),
|
||||
LinearElementEditor.pointFromAbsoluteCoords(
|
||||
{
|
||||
...el,
|
||||
x: startPoint[0],
|
||||
y: startPoint[1],
|
||||
angle: 0 as Radians,
|
||||
},
|
||||
endPoint,
|
||||
elementsMap,
|
||||
),
|
||||
]
|
||||
: el.points,
|
||||
});
|
||||
|
||||
@@ -1803,7 +1865,13 @@ export const actionChangeArrowType = register({
|
||||
newElement.startBinding.elementId,
|
||||
) as ExcalidrawBindableElement;
|
||||
if (startElement) {
|
||||
bindLinearElement(newElement, startElement, "start", app.scene);
|
||||
bindBindingElement(
|
||||
newElement,
|
||||
startElement,
|
||||
appState.bindMode === "inside" ? "inside" : "orbit",
|
||||
"start",
|
||||
app.scene,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (newElement.endBinding) {
|
||||
@@ -1811,7 +1879,13 @@ export const actionChangeArrowType = register({
|
||||
newElement.endBinding.elementId,
|
||||
) as ExcalidrawBindableElement;
|
||||
if (endElement) {
|
||||
bindLinearElement(newElement, endElement, "end", app.scene);
|
||||
bindBindingElement(
|
||||
newElement,
|
||||
endElement,
|
||||
appState.bindMode === "inside" ? "inside" : "orbit",
|
||||
"end",
|
||||
app.scene,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ import type { Action } from "./types";
|
||||
|
||||
export let actions: readonly Action[] = [];
|
||||
|
||||
export const register = <T extends Action>(action: T) => {
|
||||
export const register = <
|
||||
TData extends any,
|
||||
T extends Action<TData> = Action<TData>,
|
||||
>(
|
||||
action: T,
|
||||
) => {
|
||||
actions = actions.concat(action);
|
||||
return action as T & {
|
||||
keyTest?: unknown extends T["keyTest"] ? never : T["keyTest"];
|
||||
|
||||
@@ -32,10 +32,10 @@ export type ActionResult =
|
||||
}
|
||||
| false;
|
||||
|
||||
type ActionFn = (
|
||||
type ActionFn<TData = any> = (
|
||||
elements: readonly OrderedExcalidrawElement[],
|
||||
appState: Readonly<AppState>,
|
||||
formData: any,
|
||||
formData: TData | undefined,
|
||||
app: AppClassProperties,
|
||||
) => ActionResult | Promise<ActionResult>;
|
||||
|
||||
@@ -157,7 +157,7 @@ export type PanelComponentProps = {
|
||||
) => React.JSX.Element | null;
|
||||
};
|
||||
|
||||
export interface Action {
|
||||
export interface Action<TData = any> {
|
||||
name: ActionName;
|
||||
label:
|
||||
| string
|
||||
@@ -174,7 +174,7 @@ export interface Action {
|
||||
elements: readonly ExcalidrawElement[],
|
||||
) => React.ReactNode);
|
||||
PanelComponent?: React.FC<PanelComponentProps>;
|
||||
perform: ActionFn;
|
||||
perform: ActionFn<TData>;
|
||||
keyPriority?: number;
|
||||
keyTest?: (
|
||||
event: React.KeyboardEvent | KeyboardEvent,
|
||||
|
||||
@@ -100,7 +100,7 @@ export const getDefaultAppState = (): Omit<
|
||||
panels: STATS_PANELS.generalStats | STATS_PANELS.elementProperties,
|
||||
},
|
||||
startBoundElement: null,
|
||||
suggestedBindings: [],
|
||||
suggestedBinding: null,
|
||||
frameRendering: { enabled: true, clip: true, name: true, outline: true },
|
||||
frameToHighlight: null,
|
||||
editingFrame: null,
|
||||
@@ -127,6 +127,7 @@ export const getDefaultAppState = (): Omit<
|
||||
searchMatches: null,
|
||||
lockedMultiSelections: {},
|
||||
activeLockedId: null,
|
||||
bindMode: "orbit",
|
||||
};
|
||||
};
|
||||
|
||||
@@ -229,7 +230,7 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
|
||||
stats: { browser: true, export: false, server: false },
|
||||
startBoundElement: { browser: false, export: false, server: false },
|
||||
suggestedBindings: { browser: false, export: false, server: false },
|
||||
suggestedBinding: { browser: false, export: false, server: false },
|
||||
frameRendering: { browser: false, export: false, server: false },
|
||||
frameToHighlight: { browser: false, export: false, server: false },
|
||||
editingFrame: { browser: false, export: false, server: false },
|
||||
@@ -252,6 +253,7 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
searchMatches: { browser: false, export: false, server: false },
|
||||
lockedMultiSelections: { browser: true, export: true, server: true },
|
||||
activeLockedId: { browser: false, export: false, server: false },
|
||||
bindMode: { browser: true, export: false, server: false },
|
||||
});
|
||||
|
||||
const _clearAppStateForStorage = <
|
||||
|
||||
+1002
-363
File diff suppressed because it is too large
Load Diff
@@ -1034,7 +1034,7 @@ const CommandItem = ({
|
||||
size="var(--icon-size, 1rem)"
|
||||
icon={
|
||||
typeof command.icon === "function"
|
||||
? command.icon(appState)
|
||||
? command.icon(appState, [])
|
||||
: command.icon
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ActionManager } from "../../actions/manager";
|
||||
import type { Action } from "../../actions/types";
|
||||
import type { UIAppState } from "../../types";
|
||||
|
||||
export type CommandPaletteItem = {
|
||||
label: string;
|
||||
@@ -12,7 +11,7 @@ export type CommandPaletteItem = {
|
||||
* (deburred name + keywords)
|
||||
*/
|
||||
haystack?: string;
|
||||
icon?: React.ReactNode | ((appState: UIAppState) => React.ReactNode);
|
||||
icon?: Action["icon"];
|
||||
category: string;
|
||||
order?: number;
|
||||
predicate?: boolean | Action["predicate"];
|
||||
|
||||
@@ -844,7 +844,7 @@ const convertElementType = <
|
||||
}),
|
||||
) as typeof element;
|
||||
|
||||
updateBindings(nextElement, app.scene);
|
||||
updateBindings(nextElement, app.scene, app.state);
|
||||
|
||||
return nextElement;
|
||||
}
|
||||
|
||||
@@ -647,7 +647,7 @@ const LayerUI = ({
|
||||
|
||||
const stripIrrelevantAppStateProps = (appState: AppState): UIAppState => {
|
||||
const {
|
||||
suggestedBindings,
|
||||
suggestedBinding,
|
||||
startBoundElement,
|
||||
cursorButton,
|
||||
scrollX,
|
||||
|
||||
@@ -34,6 +34,7 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
||||
shouldChangeByStepSize,
|
||||
nextValue,
|
||||
scene,
|
||||
app,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const origElement = originalElements[0];
|
||||
@@ -48,7 +49,7 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
||||
scene.mutateElement(latestElement, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
updateBindings(latestElement, scene);
|
||||
updateBindings(latestElement, scene, app.state);
|
||||
|
||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||
@@ -74,7 +75,7 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
||||
scene.mutateElement(latestElement, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
updateBindings(latestElement, scene);
|
||||
updateBindings(latestElement, scene, app.state);
|
||||
|
||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||
|
||||
@@ -94,9 +94,7 @@ const resizeElementInGroup = (
|
||||
);
|
||||
if (boundTextElement) {
|
||||
const newFontSize = boundTextElement.fontSize * scale;
|
||||
updateBoundElements(latestElement, scene, {
|
||||
newSize: { width: updates.width, height: updates.height },
|
||||
});
|
||||
updateBoundElements(latestElement, scene);
|
||||
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
||||
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
|
||||
scene.mutateElement(latestBoundTextElement, {
|
||||
|
||||
@@ -38,6 +38,7 @@ const moveElements = (
|
||||
originalElements: readonly ExcalidrawElement[],
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
appState: AppState,
|
||||
) => {
|
||||
for (let i = 0; i < originalElements.length; i++) {
|
||||
const origElement = originalElements[i];
|
||||
@@ -63,6 +64,7 @@ const moveElements = (
|
||||
newTopLeftY,
|
||||
origElement,
|
||||
scene,
|
||||
appState,
|
||||
originalElementsMap,
|
||||
false,
|
||||
);
|
||||
@@ -75,6 +77,7 @@ const moveGroupTo = (
|
||||
originalElements: ExcalidrawElement[],
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
appState: AppState,
|
||||
) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const [x1, y1, ,] = getCommonBounds(originalElements);
|
||||
@@ -107,6 +110,7 @@ const moveGroupTo = (
|
||||
topLeftY + offsetY,
|
||||
origElement,
|
||||
scene,
|
||||
appState,
|
||||
originalElementsMap,
|
||||
false,
|
||||
);
|
||||
@@ -125,6 +129,7 @@ const handlePositionChange: DragInputCallbackType<
|
||||
property,
|
||||
scene,
|
||||
originalAppState,
|
||||
app,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
|
||||
@@ -152,6 +157,7 @@ const handlePositionChange: DragInputCallbackType<
|
||||
elementsInUnit.map((el) => el.original),
|
||||
originalElementsMap,
|
||||
scene,
|
||||
app.state,
|
||||
);
|
||||
} else {
|
||||
const origElement = elementsInUnit[0]?.original;
|
||||
@@ -178,6 +184,7 @@ const handlePositionChange: DragInputCallbackType<
|
||||
newTopLeftY,
|
||||
origElement,
|
||||
scene,
|
||||
app.state,
|
||||
originalElementsMap,
|
||||
false,
|
||||
);
|
||||
@@ -203,6 +210,7 @@ const handlePositionChange: DragInputCallbackType<
|
||||
originalElements,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
app.state,
|
||||
);
|
||||
|
||||
scene.triggerUpdate();
|
||||
|
||||
@@ -34,6 +34,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||
property,
|
||||
scene,
|
||||
originalAppState,
|
||||
app,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const origElement = originalElements[0];
|
||||
@@ -131,6 +132,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||
newTopLeftY,
|
||||
origElement,
|
||||
scene,
|
||||
app.state,
|
||||
originalElementsMap,
|
||||
);
|
||||
return;
|
||||
@@ -162,6 +164,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||
newTopLeftY,
|
||||
origElement,
|
||||
scene,
|
||||
app.state,
|
||||
originalElementsMap,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useEffect, useMemo, useState, memo } from "react";
|
||||
import { STATS_PANELS } from "@excalidraw/common";
|
||||
import { getCommonBounds } from "@excalidraw/element";
|
||||
import { getUncroppedWidthAndHeight } from "@excalidraw/element";
|
||||
import { isElbowArrow, isImageElement } from "@excalidraw/element";
|
||||
import { isImageElement } from "@excalidraw/element";
|
||||
|
||||
import { frameAndChildrenSelectedTogether } from "@excalidraw/element";
|
||||
|
||||
@@ -333,16 +333,14 @@ export const StatsInner = memo(
|
||||
appState={appState}
|
||||
/>
|
||||
</StatsRow>
|
||||
{!isElbowArrow(singleElement) && (
|
||||
<StatsRow>
|
||||
<Angle
|
||||
property="angle"
|
||||
element={singleElement}
|
||||
scene={scene}
|
||||
appState={appState}
|
||||
/>
|
||||
</StatsRow>
|
||||
)}
|
||||
<StatsRow>
|
||||
<Angle
|
||||
property="angle"
|
||||
element={singleElement}
|
||||
scene={scene}
|
||||
appState={appState}
|
||||
/>
|
||||
</StatsRow>
|
||||
<StatsRow>
|
||||
<FontSize
|
||||
property="fontSize"
|
||||
|
||||
@@ -114,7 +114,7 @@ describe("binding with linear elements", () => {
|
||||
mouse.up(200, 100);
|
||||
|
||||
UI.clickTool("arrow");
|
||||
mouse.down(5, 0);
|
||||
mouse.down(-5, 0);
|
||||
mouse.up(300, 50);
|
||||
|
||||
elementStats = stats?.querySelector("#elementStats");
|
||||
@@ -135,18 +135,7 @@ describe("binding with linear elements", () => {
|
||||
) as HTMLInputElement;
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
expect(inputX).not.toBeNull();
|
||||
UI.updateInput(inputX, String("204"));
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
});
|
||||
|
||||
it("should remain bound to linear element on small angle change", async () => {
|
||||
const linear = h.elements[1] as ExcalidrawLinearElement;
|
||||
const inputAngle = UI.queryStatsProperty("A")?.querySelector(
|
||||
".drag-input",
|
||||
) as HTMLInputElement;
|
||||
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
UI.updateInput(inputAngle, String("1"));
|
||||
UI.updateInput(inputX, String("186"));
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
});
|
||||
|
||||
@@ -161,17 +150,6 @@ describe("binding with linear elements", () => {
|
||||
UI.updateInput(inputX, String("254"));
|
||||
expect(linear.startBinding).toBe(null);
|
||||
});
|
||||
|
||||
it("should remain bound to linear element on small angle change", async () => {
|
||||
const linear = h.elements[1] as ExcalidrawLinearElement;
|
||||
const inputAngle = UI.queryStatsProperty("A")?.querySelector(
|
||||
".drag-input",
|
||||
) as HTMLInputElement;
|
||||
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
UI.updateInput(inputAngle, String("45"));
|
||||
expect(linear.startBinding).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
// single element
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { pointFrom, pointRotateRads } from "@excalidraw/math";
|
||||
|
||||
import { getBoundTextElement } from "@excalidraw/element";
|
||||
import {
|
||||
getBoundTextElement,
|
||||
isBindingElement,
|
||||
unbindBindingElement,
|
||||
} from "@excalidraw/element";
|
||||
import { isFrameLikeElement } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
@@ -12,6 +16,7 @@ import {
|
||||
import { getFrameChildren } from "@excalidraw/element";
|
||||
|
||||
import { updateBindings } from "@excalidraw/element";
|
||||
import { DRAGGING_THRESHOLD } from "@excalidraw/common";
|
||||
|
||||
import type { Radians } from "@excalidraw/math";
|
||||
|
||||
@@ -110,9 +115,25 @@ export const moveElement = (
|
||||
newTopLeftY: number,
|
||||
originalElement: ExcalidrawElement,
|
||||
scene: Scene,
|
||||
appState: AppState,
|
||||
originalElementsMap: ElementsMap,
|
||||
shouldInformMutation = true,
|
||||
) => {
|
||||
if (
|
||||
isBindingElement(originalElement) &&
|
||||
(originalElement.startBinding || originalElement.endBinding)
|
||||
) {
|
||||
if (
|
||||
Math.abs(newTopLeftX - originalElement.x) < DRAGGING_THRESHOLD &&
|
||||
Math.abs(newTopLeftY - originalElement.y) < DRAGGING_THRESHOLD
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
unbindBindingElement(originalElement, "start", scene);
|
||||
unbindBindingElement(originalElement, "end", scene);
|
||||
}
|
||||
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const latestElement = elementsMap.get(originalElement.id);
|
||||
if (!latestElement) {
|
||||
@@ -145,7 +166,7 @@ export const moveElement = (
|
||||
},
|
||||
{ informMutation: shouldInformMutation, isDragging: false },
|
||||
);
|
||||
updateBindings(latestElement, scene);
|
||||
updateBindings(latestElement, scene, appState);
|
||||
|
||||
const boundTextElement = getBoundTextElement(
|
||||
originalElement,
|
||||
@@ -203,7 +224,7 @@ export const moveElement = (
|
||||
},
|
||||
{ informMutation: shouldInformMutation, isDragging: false },
|
||||
);
|
||||
updateBindings(latestChildElement, scene, {
|
||||
updateBindings(latestChildElement, scene, appState, {
|
||||
simultaneouslyUpdated: originalChildren,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -157,6 +157,8 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
||||
remotePointerUserStates,
|
||||
selectionColor,
|
||||
renderScrollbars: props.renderScrollbars,
|
||||
// NOTE not memoized on so we don't rerender on cursor move
|
||||
lastViewportPosition: props.app.lastViewportPosition,
|
||||
},
|
||||
editorInterface: props.editorInterface,
|
||||
callback: props.renderInteractiveSceneCallback,
|
||||
@@ -242,8 +244,9 @@ const getRelevantAppStateProps = (
|
||||
selectedGroupIds: appState.selectedGroupIds,
|
||||
selectedLinearElement: appState.selectedLinearElement,
|
||||
multiElement: appState.multiElement,
|
||||
newElement: appState.newElement,
|
||||
isBindingEnabled: appState.isBindingEnabled,
|
||||
suggestedBindings: appState.suggestedBindings,
|
||||
suggestedBinding: appState.suggestedBinding,
|
||||
isRotating: appState.isRotating,
|
||||
elementsToHighlight: appState.elementsToHighlight,
|
||||
collaborators: appState.collaborators, // Necessary for collab. sessions
|
||||
@@ -255,6 +258,10 @@ const getRelevantAppStateProps = (
|
||||
croppingElementId: appState.croppingElementId,
|
||||
searchMatches: appState.searchMatches,
|
||||
activeLockedId: appState.activeLockedId,
|
||||
hoveredElementIds: appState.hoveredElementIds,
|
||||
frameRendering: appState.frameRendering,
|
||||
shouldCacheIgnoreZoom: appState.shouldCacheIgnoreZoom,
|
||||
exportScale: appState.exportScale,
|
||||
});
|
||||
|
||||
const areEqual = (
|
||||
|
||||
@@ -99,6 +99,7 @@ const getRelevantAppStateProps = (appState: AppState): StaticCanvasAppState => {
|
||||
editingGroupId: appState.editingGroupId,
|
||||
currentHoveredFontFamily: appState.currentHoveredFontFamily,
|
||||
croppingElementId: appState.croppingElementId,
|
||||
suggestedBinding: appState.suggestedBinding,
|
||||
};
|
||||
|
||||
return relevantAppStateProps;
|
||||
|
||||
@@ -88,8 +88,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
"endArrowhead": "arrow",
|
||||
"endBinding": {
|
||||
"elementId": "ellipse-1",
|
||||
"focus": -0.007519379844961235,
|
||||
"gap": 11.562288374879595,
|
||||
"fixedPoint": [
|
||||
0.04,
|
||||
0.4633333333333333,
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
@@ -98,7 +101,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
"id": Any<String>,
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
@@ -118,8 +120,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
"startArrowhead": null,
|
||||
"startBinding": {
|
||||
"elementId": "id49",
|
||||
"focus": -0.0813953488372095,
|
||||
"gap": 1,
|
||||
"fixedPoint": [
|
||||
1,
|
||||
0.5001,
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"strokeColor": "#1864ab",
|
||||
"strokeStyle": "solid",
|
||||
@@ -144,8 +149,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
"endArrowhead": "arrow",
|
||||
"endBinding": {
|
||||
"elementId": "ellipse-1",
|
||||
"focus": 0.10666666666666667,
|
||||
"gap": 3.8343264684446097,
|
||||
"fixedPoint": [
|
||||
-0.01,
|
||||
0.44666666666666666,
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
@@ -154,7 +162,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
"id": Any<String>,
|
||||
"index": "a3",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
@@ -174,8 +181,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
"startArrowhead": null,
|
||||
"startBinding": {
|
||||
"elementId": "diamond-1",
|
||||
"focus": 0,
|
||||
"gap": 4.535423522449215,
|
||||
"fixedPoint": [
|
||||
0.9357142857142857,
|
||||
0.5001,
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"strokeColor": "#e67700",
|
||||
"strokeStyle": "solid",
|
||||
@@ -334,8 +344,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||
"endArrowhead": "arrow",
|
||||
"endBinding": {
|
||||
"elementId": "text-2",
|
||||
"focus": 0,
|
||||
"gap": 16,
|
||||
"fixedPoint": [
|
||||
-2.05,
|
||||
0.5001,
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
@@ -344,7 +357,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||
"id": Any<String>,
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
@@ -364,8 +376,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||
"startArrowhead": null,
|
||||
"startBinding": {
|
||||
"elementId": "text-1",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"fixedPoint": [
|
||||
1,
|
||||
0.5001,
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -436,8 +451,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
||||
"endArrowhead": "arrow",
|
||||
"endBinding": {
|
||||
"elementId": "id42",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"fixedPoint": [
|
||||
0,
|
||||
0.5001,
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
@@ -446,7 +464,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
||||
"id": Any<String>,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
@@ -466,8 +483,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
||||
"startArrowhead": null,
|
||||
"startBinding": {
|
||||
"elementId": "id41",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"fixedPoint": [
|
||||
1,
|
||||
0.5001,
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -612,8 +632,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||
"endArrowhead": "arrow",
|
||||
"endBinding": {
|
||||
"elementId": "id46",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"fixedPoint": [
|
||||
0,
|
||||
0.5001,
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
@@ -622,7 +645,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||
"id": Any<String>,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
@@ -642,8 +664,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||
"startArrowhead": null,
|
||||
"startBinding": {
|
||||
"elementId": "id45",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"fixedPoint": [
|
||||
1,
|
||||
0.5001,
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -839,7 +864,6 @@ exports[`Test Transform > should transform linear elements 1`] = `
|
||||
"id": Any<String>,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
@@ -887,7 +911,6 @@ exports[`Test Transform > should transform linear elements 2`] = `
|
||||
"id": Any<String>,
|
||||
"index": "a1",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
@@ -934,7 +957,6 @@ exports[`Test Transform > should transform linear elements 3`] = `
|
||||
"id": Any<String>,
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
@@ -982,7 +1004,6 @@ exports[`Test Transform > should transform linear elements 4`] = `
|
||||
"id": Any<String>,
|
||||
"index": "a3",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
@@ -1476,8 +1497,11 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||
"endArrowhead": "arrow",
|
||||
"endBinding": {
|
||||
"elementId": "Alice",
|
||||
"focus": -0,
|
||||
"gap": 5.299874999999986,
|
||||
"fixedPoint": [
|
||||
-0.07542628418945944,
|
||||
0.5001,
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
@@ -1486,7 +1510,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||
"id": Any<String>,
|
||||
"index": "a4",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
@@ -1508,8 +1531,11 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||
"startArrowhead": null,
|
||||
"startBinding": {
|
||||
"elementId": "Bob",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"fixedPoint": [
|
||||
1.000004978564514,
|
||||
0.5001,
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -1539,8 +1565,11 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||
"endArrowhead": "arrow",
|
||||
"endBinding": {
|
||||
"elementId": "B",
|
||||
"focus": 0,
|
||||
"gap": 32,
|
||||
"fixedPoint": [
|
||||
0.46387050630528887,
|
||||
0.48466257668711654,
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
@@ -1549,7 +1578,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||
"id": Any<String>,
|
||||
"index": "a5",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
@@ -1567,8 +1595,11 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||
"startArrowhead": null,
|
||||
"startBinding": {
|
||||
"elementId": "Bob",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"fixedPoint": [
|
||||
0.39381496335223337,
|
||||
1,
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -1858,7 +1889,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||
"id": Any<String>,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
@@ -1911,7 +1941,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||
"id": Any<String>,
|
||||
"index": "a1",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
@@ -1964,7 +1993,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||
"id": Any<String>,
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
@@ -2017,7 +2045,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||
"id": Any<String>,
|
||||
"index": "a3",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
|
||||
@@ -7,8 +7,6 @@ import {
|
||||
isPromiseLike,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { clearElementsForExport } from "@excalidraw/element";
|
||||
|
||||
import type { ValueOf } from "@excalidraw/common/utility-types";
|
||||
import type { ExcalidrawElement, FileId } from "@excalidraw/element/types";
|
||||
|
||||
@@ -159,7 +157,7 @@ export const loadSceneOrLibraryFromBlob = async (
|
||||
type: MIME_TYPES.excalidraw,
|
||||
data: restore(
|
||||
{
|
||||
elements: clearElementsForExport(data.elements || []),
|
||||
elements: data.elements || [],
|
||||
appState: {
|
||||
theme: localAppState?.theme,
|
||||
fileHandle: fileHandle || blob.handle || null,
|
||||
|
||||
@@ -6,11 +6,6 @@ import {
|
||||
VERSIONS,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
clearElementsForDatabase,
|
||||
clearElementsForExport,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import { cleanAppStateForExport, clearAppStateForDatabase } from "../appState";
|
||||
@@ -57,10 +52,7 @@ export const serializeAsJSON = (
|
||||
type: EXPORT_DATA_TYPES.excalidraw,
|
||||
version: VERSIONS.excalidraw,
|
||||
source: getExportSource(),
|
||||
elements:
|
||||
type === "local"
|
||||
? clearElementsForExport(elements)
|
||||
: clearElementsForDatabase(elements),
|
||||
elements,
|
||||
appState:
|
||||
type === "local"
|
||||
? cleanAppStateForExport(appState)
|
||||
|
||||
@@ -18,7 +18,13 @@ import {
|
||||
normalizeLink,
|
||||
getLineHeight,
|
||||
} from "@excalidraw/common";
|
||||
import { getNonDeletedElements, isValidPolygon } from "@excalidraw/element";
|
||||
import {
|
||||
calculateFixedPointForNonElbowArrowBinding,
|
||||
getNonDeletedElements,
|
||||
isPointInElement,
|
||||
isValidPolygon,
|
||||
projectFixedPointOntoDiagonal,
|
||||
} from "@excalidraw/element";
|
||||
import { normalizeFixedPoint } from "@excalidraw/element";
|
||||
import {
|
||||
updateElbowArrowPoints,
|
||||
@@ -32,7 +38,6 @@ import {
|
||||
isArrowBoundToElement,
|
||||
isArrowElement,
|
||||
isElbowArrow,
|
||||
isFixedPointBinding,
|
||||
isLinearElement,
|
||||
isLineElement,
|
||||
isTextElement,
|
||||
@@ -50,10 +55,11 @@ import { isInvisiblySmallElement } from "@excalidraw/element";
|
||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawElbowArrowElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawElementType,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawSelectionElement,
|
||||
ExcalidrawTextElement,
|
||||
@@ -61,7 +67,6 @@ import type {
|
||||
FontFamilyValues,
|
||||
NonDeletedSceneElementsMap,
|
||||
OrderedExcalidrawElement,
|
||||
PointBinding,
|
||||
StrokeRoundness,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
@@ -121,38 +126,74 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
|
||||
return DEFAULT_FONT_FAMILY;
|
||||
};
|
||||
|
||||
const repairBinding = <T extends ExcalidrawLinearElement>(
|
||||
const repairBinding = <T extends ExcalidrawArrowElement>(
|
||||
element: T,
|
||||
binding: PointBinding | FixedPointBinding | null,
|
||||
): T extends ExcalidrawElbowArrowElement
|
||||
? FixedPointBinding | null
|
||||
: PointBinding | FixedPointBinding | null => {
|
||||
binding: FixedPointBinding | null,
|
||||
elementsMap: Readonly<ElementsMap>,
|
||||
startOrEnd: "start" | "end",
|
||||
): FixedPointBinding | null => {
|
||||
if (!binding) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const focus = binding.focus || 0;
|
||||
|
||||
if (isElbowArrow(element)) {
|
||||
const fixedPointBinding:
|
||||
| ExcalidrawElbowArrowElement["startBinding"]
|
||||
| ExcalidrawElbowArrowElement["endBinding"] = isFixedPointBinding(binding)
|
||||
? {
|
||||
...binding,
|
||||
focus,
|
||||
fixedPoint: normalizeFixedPoint(binding.fixedPoint ?? [0, 0]),
|
||||
}
|
||||
: null;
|
||||
| ExcalidrawElbowArrowElement["endBinding"] = {
|
||||
...binding,
|
||||
fixedPoint: normalizeFixedPoint(binding.fixedPoint ?? [0, 0]),
|
||||
mode: binding.mode || "orbit",
|
||||
};
|
||||
|
||||
return fixedPointBinding;
|
||||
}
|
||||
|
||||
return {
|
||||
...binding,
|
||||
focus,
|
||||
} as T extends ExcalidrawElbowArrowElement
|
||||
? FixedPointBinding | null
|
||||
: PointBinding | FixedPointBinding | null;
|
||||
const boundElement =
|
||||
(elementsMap.get(binding.elementId) as ExcalidrawBindableElement) ||
|
||||
undefined;
|
||||
if (boundElement) {
|
||||
if (binding.mode) {
|
||||
return {
|
||||
elementId: binding.elementId,
|
||||
mode: binding.mode || "orbit",
|
||||
fixedPoint: normalizeFixedPoint(binding.fixedPoint || [0.5, 0.5]),
|
||||
} as FixedPointBinding | null;
|
||||
}
|
||||
|
||||
const p = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
element,
|
||||
startOrEnd === "start" ? 0 : element.points.length - 1,
|
||||
elementsMap,
|
||||
);
|
||||
const mode = isPointInElement(p, boundElement, elementsMap)
|
||||
? "inside"
|
||||
: "orbit";
|
||||
const focusPoint =
|
||||
mode === "inside"
|
||||
? p
|
||||
: projectFixedPointOntoDiagonal(
|
||||
element,
|
||||
p,
|
||||
boundElement,
|
||||
startOrEnd,
|
||||
elementsMap,
|
||||
) || p;
|
||||
const { fixedPoint } = calculateFixedPointForNonElbowArrowBinding(
|
||||
element,
|
||||
boundElement,
|
||||
startOrEnd,
|
||||
elementsMap,
|
||||
focusPoint,
|
||||
);
|
||||
|
||||
return {
|
||||
mode,
|
||||
elementId: binding.elementId,
|
||||
fixedPoint,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const restoreElementWithProperties = <
|
||||
@@ -243,7 +284,10 @@ const restoreElementWithProperties = <
|
||||
|
||||
export const restoreElement = (
|
||||
element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
||||
opts?: { deleteInvisibleElements?: boolean },
|
||||
elementsMap: Readonly<ElementsMap>,
|
||||
opts?: {
|
||||
deleteInvisibleElements?: boolean;
|
||||
},
|
||||
): typeof element | null => {
|
||||
element = { ...element };
|
||||
|
||||
@@ -301,7 +345,6 @@ export const restoreElement = (
|
||||
case "freedraw": {
|
||||
return restoreElementWithProperties(element, {
|
||||
points: element.points,
|
||||
lastCommittedPoint: null,
|
||||
simulatePressure: element.simulatePressure,
|
||||
pressures: element.pressures,
|
||||
});
|
||||
@@ -331,13 +374,9 @@ export const restoreElement = (
|
||||
}
|
||||
|
||||
return restoreElementWithProperties(element, {
|
||||
type:
|
||||
(element.type as ExcalidrawElementType | "draw") === "draw"
|
||||
? "line"
|
||||
: element.type,
|
||||
startBinding: repairBinding(element, element.startBinding),
|
||||
endBinding: repairBinding(element, element.endBinding),
|
||||
lastCommittedPoint: null,
|
||||
type: "line",
|
||||
startBinding: null,
|
||||
endBinding: null,
|
||||
startArrowhead,
|
||||
endArrowhead,
|
||||
points,
|
||||
@@ -354,23 +393,27 @@ export const restoreElement = (
|
||||
});
|
||||
case "arrow": {
|
||||
const { startArrowhead = null, endArrowhead = "arrow" } = element;
|
||||
let x: number | undefined = element.x;
|
||||
let y: number | undefined = element.y;
|
||||
let points: readonly LocalPoint[] | undefined = // migrate old arrow model to new one
|
||||
const x: number | undefined = element.x;
|
||||
const y: number | undefined = element.y;
|
||||
const points: readonly LocalPoint[] | undefined = // migrate old arrow model to new one
|
||||
!Array.isArray(element.points) || element.points.length < 2
|
||||
? [pointFrom(0, 0), pointFrom(element.width, element.height)]
|
||||
: element.points;
|
||||
|
||||
if (points[0][0] !== 0 || points[0][1] !== 0) {
|
||||
({ points, x, y } =
|
||||
LinearElementEditor.getNormalizeElementPointsAndCoords(element));
|
||||
}
|
||||
|
||||
const base = {
|
||||
type: element.type,
|
||||
startBinding: repairBinding(element, element.startBinding),
|
||||
endBinding: repairBinding(element, element.endBinding),
|
||||
lastCommittedPoint: null,
|
||||
startBinding: repairBinding(
|
||||
element as ExcalidrawArrowElement,
|
||||
element.startBinding,
|
||||
elementsMap,
|
||||
"start",
|
||||
),
|
||||
endBinding: repairBinding(
|
||||
element as ExcalidrawArrowElement,
|
||||
element.endBinding,
|
||||
elementsMap,
|
||||
"end",
|
||||
),
|
||||
startArrowhead,
|
||||
endArrowhead,
|
||||
points,
|
||||
@@ -378,15 +421,13 @@ export const restoreElement = (
|
||||
y,
|
||||
elbowed: (element as ExcalidrawArrowElement).elbowed,
|
||||
...getSizeFromPoints(points),
|
||||
} as const;
|
||||
};
|
||||
|
||||
// TODO: Separate arrow from linear element
|
||||
return isElbowArrow(element)
|
||||
const restoredElement = isElbowArrow(element)
|
||||
? restoreElementWithProperties(element as ExcalidrawElbowArrowElement, {
|
||||
...base,
|
||||
elbowed: true,
|
||||
startBinding: repairBinding(element, element.startBinding),
|
||||
endBinding: repairBinding(element, element.endBinding),
|
||||
fixedSegments:
|
||||
element.fixedSegments?.length && base.points.length >= 4
|
||||
? element.fixedSegments
|
||||
@@ -395,6 +436,13 @@ export const restoreElement = (
|
||||
endIsSpecial: element.endIsSpecial,
|
||||
})
|
||||
: restoreElementWithProperties(element as ExcalidrawArrowElement, base);
|
||||
|
||||
return {
|
||||
...restoredElement,
|
||||
...LinearElementEditor.getNormalizeElementPointsAndCoords(
|
||||
restoredElement,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// generic elements
|
||||
@@ -525,7 +573,7 @@ const repairFrameMembership = (
|
||||
};
|
||||
|
||||
export const restoreElements = (
|
||||
elements: ImportedDataState["elements"],
|
||||
targetElements: ImportedDataState["elements"],
|
||||
/** NOTE doesn't serve for reconciliation */
|
||||
localElements: readonly ExcalidrawElement[] | null | undefined,
|
||||
opts?:
|
||||
@@ -538,18 +586,23 @@ export const restoreElements = (
|
||||
): OrderedExcalidrawElement[] => {
|
||||
// used to detect duplicate top-level element ids
|
||||
const existingIds = new Set<string>();
|
||||
const elementsMap = arrayToMap(targetElements || []);
|
||||
const localElementsMap = localElements ? arrayToMap(localElements) : null;
|
||||
const restoredElements = syncInvalidIndices(
|
||||
(elements || []).reduce((elements, element) => {
|
||||
(targetElements || []).reduce((elements, element) => {
|
||||
// filtering out selection, which is legacy, no longer kept in elements,
|
||||
// and causing issues if retained
|
||||
if (element.type === "selection") {
|
||||
return elements;
|
||||
}
|
||||
|
||||
let migratedElement: ExcalidrawElement | null = restoreElement(element, {
|
||||
deleteInvisibleElements: opts?.deleteInvisibleElements,
|
||||
});
|
||||
let migratedElement: ExcalidrawElement | null = restoreElement(
|
||||
element,
|
||||
elementsMap,
|
||||
{
|
||||
deleteInvisibleElements: opts?.deleteInvisibleElements,
|
||||
},
|
||||
);
|
||||
if (migratedElement) {
|
||||
const localElement = localElementsMap?.get(element.id);
|
||||
|
||||
|
||||
@@ -432,12 +432,9 @@ describe("Test Transform", () => {
|
||||
boundElements: [{ id: text.id, type: "text" }],
|
||||
startBinding: {
|
||||
elementId: rectangle.id,
|
||||
focus: 0,
|
||||
gap: 1,
|
||||
},
|
||||
endBinding: {
|
||||
elementId: ellipse.id,
|
||||
focus: -0,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -517,12 +514,9 @@ describe("Test Transform", () => {
|
||||
boundElements: [{ id: text1.id, type: "text" }],
|
||||
startBinding: {
|
||||
elementId: text2.id,
|
||||
focus: 0,
|
||||
gap: 1,
|
||||
},
|
||||
endBinding: {
|
||||
elementId: text3.id,
|
||||
focus: -0,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -780,8 +774,8 @@ describe("Test Transform", () => {
|
||||
const [arrow, rect] = excalidrawElements;
|
||||
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
||||
elementId: "rect-1",
|
||||
focus: -0,
|
||||
gap: 25,
|
||||
fixedPoint: [-2.05, 0.5001],
|
||||
mode: "orbit",
|
||||
});
|
||||
expect(rect.boundElements).toStrictEqual([
|
||||
{
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
getLineHeight,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { bindLinearElement } from "@excalidraw/element";
|
||||
import { bindBindingElement } from "@excalidraw/element";
|
||||
import {
|
||||
newArrowElement,
|
||||
newElement,
|
||||
@@ -330,9 +330,10 @@ const bindLinearElementToElement = (
|
||||
}
|
||||
}
|
||||
|
||||
bindLinearElement(
|
||||
bindBindingElement(
|
||||
linearElement,
|
||||
startBoundElement as ExcalidrawBindableElement,
|
||||
"orbit",
|
||||
"start",
|
||||
scene,
|
||||
);
|
||||
@@ -405,9 +406,10 @@ const bindLinearElementToElement = (
|
||||
}
|
||||
}
|
||||
|
||||
bindLinearElement(
|
||||
bindBindingElement(
|
||||
linearElement,
|
||||
endBoundElement as ExcalidrawBindableElement,
|
||||
"orbit",
|
||||
"end",
|
||||
scene,
|
||||
);
|
||||
|
||||
Vendored
+4
-1
@@ -101,7 +101,10 @@ declare module "image-blob-reduce" {
|
||||
|
||||
interface CustomMatchers {
|
||||
toBeNonNaNNumber(): void;
|
||||
toCloselyEqualPoints(points: readonly [number, number][]): void;
|
||||
toCloselyEqualPoints(
|
||||
points: readonly [number, number][],
|
||||
precision?: number,
|
||||
): void;
|
||||
}
|
||||
|
||||
declare namespace jest {
|
||||
|
||||
@@ -341,6 +341,7 @@
|
||||
"canvasPanning": "To move canvas, hold {{shortcut_1}} or {{shortcut_2}} while dragging, or use the hand tool",
|
||||
"linearElement": "Click to start multiple points, drag for single line",
|
||||
"arrowTool": "Click to start multiple points, drag for single line. Press {{shortcut}} again to change arrow type.",
|
||||
"arrowBindModifiers": "Hold {{shortcut_1}} to bind inside, or {{shortcut_2}} to disable binding",
|
||||
"freeDraw": "Click and drag, release when you're finished",
|
||||
"text": "Tip: you can also add text by double-clicking anywhere with the selection tool",
|
||||
"embeddable": "Click-drag to create a website embed",
|
||||
|
||||
@@ -81,8 +81,8 @@
|
||||
"@braintree/sanitize-url": "6.0.2",
|
||||
"@excalidraw/common": "0.18.0",
|
||||
"@excalidraw/element": "0.18.0",
|
||||
"@excalidraw/math": "0.18.0",
|
||||
"@excalidraw/laser-pointer": "1.3.1",
|
||||
"@excalidraw/math": "0.18.0",
|
||||
"@excalidraw/mermaid-to-excalidraw": "1.1.3",
|
||||
"@excalidraw/random-username": "1.1.0",
|
||||
"@radix-ui/react-popover": "1.1.6",
|
||||
@@ -97,8 +97,8 @@
|
||||
"image-blob-reduce": "3.0.1",
|
||||
"jotai": "2.11.0",
|
||||
"jotai-scope": "0.7.2",
|
||||
"lodash.throttle": "4.1.1",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"lodash.throttle": "4.1.1",
|
||||
"nanoid": "3.3.3",
|
||||
"open-color": "1.9.1",
|
||||
"pako": "2.0.3",
|
||||
|
||||
@@ -1,26 +1,5 @@
|
||||
import { THEME, THEME_FILTER } from "@excalidraw/common";
|
||||
|
||||
import { FIXED_BINDING_DISTANCE } from "@excalidraw/element";
|
||||
import { getDiamondPoints } from "@excalidraw/element";
|
||||
import { elementCenterPoint, getCornerRadius } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
curve,
|
||||
curveCatmullRomCubicApproxPoints,
|
||||
curveCatmullRomQuadraticApproxPoints,
|
||||
curveOffsetPoints,
|
||||
type GlobalPoint,
|
||||
offsetPointsForQuadraticBezier,
|
||||
pointFrom,
|
||||
pointRotateRads,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawRectanguloidElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type { StaticCanvasRenderConfig } from "../scene/types";
|
||||
import type { AppState, StaticCanvasAppState } from "../types";
|
||||
|
||||
@@ -97,164 +76,7 @@ export const bootstrapCanvas = ({
|
||||
return context;
|
||||
};
|
||||
|
||||
function drawCatmullRomQuadraticApprox(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
points: GlobalPoint[],
|
||||
tension = 0.5,
|
||||
) {
|
||||
const pointSets = curveCatmullRomQuadraticApproxPoints(points, tension);
|
||||
if (pointSets) {
|
||||
for (let i = 0; i < pointSets.length - 1; i++) {
|
||||
const [[cpX, cpY], [p2X, p2Y]] = pointSets[i];
|
||||
|
||||
ctx.quadraticCurveTo(cpX, cpY, p2X, p2Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawCatmullRomCubicApprox(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
points: GlobalPoint[],
|
||||
tension = 0.5,
|
||||
) {
|
||||
const pointSets = curveCatmullRomCubicApproxPoints(points, tension);
|
||||
if (pointSets) {
|
||||
for (let i = 0; i < pointSets.length; i++) {
|
||||
const [[cp1x, cp1y], [cp2x, cp2y], [x, y]] = pointSets[i];
|
||||
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const drawHighlightForRectWithRotation = (
|
||||
context: CanvasRenderingContext2D,
|
||||
element: ExcalidrawRectanguloidElement,
|
||||
elementsMap: ElementsMap,
|
||||
padding: number,
|
||||
) => {
|
||||
const [x, y] = pointRotateRads(
|
||||
pointFrom<GlobalPoint>(element.x, element.y),
|
||||
elementCenterPoint(element, elementsMap),
|
||||
element.angle,
|
||||
);
|
||||
|
||||
context.save();
|
||||
context.translate(x, y);
|
||||
context.rotate(element.angle);
|
||||
|
||||
let radius = getCornerRadius(
|
||||
Math.min(element.width, element.height),
|
||||
element,
|
||||
);
|
||||
if (radius === 0) {
|
||||
radius = 0.01;
|
||||
}
|
||||
|
||||
context.beginPath();
|
||||
|
||||
{
|
||||
const topLeftApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(0, 0 + radius),
|
||||
pointFrom(0, 0),
|
||||
pointFrom(0 + radius, 0),
|
||||
padding,
|
||||
);
|
||||
const topRightApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(element.width - radius, 0),
|
||||
pointFrom(element.width, 0),
|
||||
pointFrom(element.width, radius),
|
||||
padding,
|
||||
);
|
||||
const bottomRightApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(element.width, element.height - radius),
|
||||
pointFrom(element.width, element.height),
|
||||
pointFrom(element.width - radius, element.height),
|
||||
padding,
|
||||
);
|
||||
const bottomLeftApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(radius, element.height),
|
||||
pointFrom(0, element.height),
|
||||
pointFrom(0, element.height - radius),
|
||||
padding,
|
||||
);
|
||||
|
||||
context.moveTo(
|
||||
topLeftApprox[topLeftApprox.length - 1][0],
|
||||
topLeftApprox[topLeftApprox.length - 1][1],
|
||||
);
|
||||
context.lineTo(topRightApprox[0][0], topRightApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, topRightApprox);
|
||||
context.lineTo(bottomRightApprox[0][0], bottomRightApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, bottomRightApprox);
|
||||
context.lineTo(bottomLeftApprox[0][0], bottomLeftApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, bottomLeftApprox);
|
||||
context.lineTo(topLeftApprox[0][0], topLeftApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, topLeftApprox);
|
||||
}
|
||||
|
||||
// Counter-clockwise for the cutout in the middle. We need to have an "inverse
|
||||
// mask" on a filled shape for the diamond highlight, because stroking creates
|
||||
// sharp inset edges on line joins < 90 degrees.
|
||||
{
|
||||
const topLeftApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(0 + radius, 0),
|
||||
pointFrom(0, 0),
|
||||
pointFrom(0, 0 + radius),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const topRightApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(element.width, radius),
|
||||
pointFrom(element.width, 0),
|
||||
pointFrom(element.width - radius, 0),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const bottomRightApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(element.width - radius, element.height),
|
||||
pointFrom(element.width, element.height),
|
||||
pointFrom(element.width, element.height - radius),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const bottomLeftApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(0, element.height - radius),
|
||||
pointFrom(0, element.height),
|
||||
pointFrom(radius, element.height),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
|
||||
context.moveTo(
|
||||
topLeftApprox[topLeftApprox.length - 1][0],
|
||||
topLeftApprox[topLeftApprox.length - 1][1],
|
||||
);
|
||||
context.lineTo(bottomLeftApprox[0][0], bottomLeftApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, bottomLeftApprox);
|
||||
context.lineTo(bottomRightApprox[0][0], bottomRightApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, bottomRightApprox);
|
||||
context.lineTo(topRightApprox[0][0], topRightApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, topRightApprox);
|
||||
context.lineTo(topLeftApprox[0][0], topLeftApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, topLeftApprox);
|
||||
}
|
||||
|
||||
context.closePath();
|
||||
context.fill();
|
||||
|
||||
context.restore();
|
||||
};
|
||||
|
||||
export const strokeEllipseWithRotation = (
|
||||
context: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number,
|
||||
cx: number,
|
||||
cy: number,
|
||||
angle: number,
|
||||
) => {
|
||||
context.beginPath();
|
||||
context.ellipse(cx, cy, width / 2, height / 2, angle, 0, Math.PI * 2);
|
||||
context.stroke();
|
||||
};
|
||||
|
||||
export const strokeRectWithRotation = (
|
||||
export const strokeRectWithRotation_simple = (
|
||||
context: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
@@ -283,147 +105,3 @@ export const strokeRectWithRotation = (
|
||||
}
|
||||
context.restore();
|
||||
};
|
||||
|
||||
export const drawHighlightForDiamondWithRotation = (
|
||||
context: CanvasRenderingContext2D,
|
||||
padding: number,
|
||||
element: ExcalidrawDiamondElement,
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
const [x, y] = pointRotateRads(
|
||||
pointFrom<GlobalPoint>(element.x, element.y),
|
||||
elementCenterPoint(element, elementsMap),
|
||||
element.angle,
|
||||
);
|
||||
context.save();
|
||||
context.translate(x, y);
|
||||
context.rotate(element.angle);
|
||||
|
||||
{
|
||||
context.beginPath();
|
||||
|
||||
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
||||
getDiamondPoints(element);
|
||||
const verticalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(topX - leftX), element)
|
||||
: (topX - leftX) * 0.01;
|
||||
const horizontalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(rightY - topY), element)
|
||||
: (rightY - topY) * 0.01;
|
||||
const topApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
||||
),
|
||||
padding,
|
||||
);
|
||||
const rightApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
||||
),
|
||||
padding,
|
||||
);
|
||||
const bottomApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
||||
),
|
||||
padding,
|
||||
);
|
||||
const leftApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
||||
),
|
||||
padding,
|
||||
);
|
||||
|
||||
context.moveTo(
|
||||
topApprox[topApprox.length - 1][0],
|
||||
topApprox[topApprox.length - 1][1],
|
||||
);
|
||||
context.lineTo(rightApprox[1][0], rightApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, rightApprox);
|
||||
context.lineTo(bottomApprox[1][0], bottomApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, bottomApprox);
|
||||
context.lineTo(leftApprox[1][0], leftApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, leftApprox);
|
||||
context.lineTo(topApprox[1][0], topApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, topApprox);
|
||||
}
|
||||
|
||||
// Counter-clockwise for the cutout in the middle. We need to have an "inverse
|
||||
// mask" on a filled shape for the diamond highlight, because stroking creates
|
||||
// sharp inset edges on line joins < 90 degrees.
|
||||
{
|
||||
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
||||
getDiamondPoints(element);
|
||||
const verticalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(topX - leftX), element)
|
||||
: (topX - leftX) * 0.01;
|
||||
const horizontalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(rightY - topY), element)
|
||||
: (rightY - topY) * 0.01;
|
||||
const topApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
||||
),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const rightApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
||||
),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const bottomApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
||||
),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const leftApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
||||
),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
|
||||
context.moveTo(
|
||||
topApprox[topApprox.length - 1][0],
|
||||
topApprox[topApprox.length - 1][1],
|
||||
);
|
||||
context.lineTo(leftApprox[1][0], leftApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, leftApprox);
|
||||
context.lineTo(bottomApprox[1][0], bottomApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, bottomApprox);
|
||||
context.lineTo(rightApprox[1][0], rightApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, rightApprox);
|
||||
context.lineTo(topApprox[1][0], topApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, topApprox);
|
||||
}
|
||||
context.closePath();
|
||||
context.fill();
|
||||
context.restore();
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
clamp,
|
||||
pointFrom,
|
||||
pointsEqual,
|
||||
type GlobalPoint,
|
||||
@@ -9,27 +10,29 @@ import oc from "open-color";
|
||||
|
||||
import {
|
||||
arrayToMap,
|
||||
BIND_MODE_TIMEOUT,
|
||||
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
||||
FRAME_STYLE,
|
||||
getFeatureFlag,
|
||||
invariant,
|
||||
THEME,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { FIXED_BINDING_DISTANCE, maxBindingGap } from "@excalidraw/element";
|
||||
import { LinearElementEditor } from "@excalidraw/element";
|
||||
import {
|
||||
deconstructDiamondElement,
|
||||
deconstructRectanguloidElement,
|
||||
elementCenterPoint,
|
||||
getOmitSidesForEditorInterface,
|
||||
getTransformHandles,
|
||||
getTransformHandlesFromCoords,
|
||||
hasBoundingBox,
|
||||
} from "@excalidraw/element";
|
||||
import {
|
||||
isElbowArrow,
|
||||
isFrameLikeElement,
|
||||
isImageElement,
|
||||
isLinearElement,
|
||||
isLineElement,
|
||||
isTextElement,
|
||||
LinearElementEditor,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { renderSelectionElement } from "@excalidraw/element";
|
||||
@@ -43,11 +46,6 @@ import {
|
||||
|
||||
import { getCommonBounds, getElementAbsoluteCoords } from "@excalidraw/element";
|
||||
|
||||
import type {
|
||||
SuggestedBinding,
|
||||
SuggestedPointBinding,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import type {
|
||||
TransformHandles,
|
||||
TransformHandleType,
|
||||
@@ -63,6 +61,7 @@ import type {
|
||||
ExcalidrawTextElement,
|
||||
GroupId,
|
||||
NonDeleted,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import { renderSnaps } from "../renderer/renderSnaps";
|
||||
@@ -72,18 +71,19 @@ import {
|
||||
SCROLLBAR_COLOR,
|
||||
SCROLLBAR_WIDTH,
|
||||
} from "../scene/scrollbars";
|
||||
import { type InteractiveCanvasAppState } from "../types";
|
||||
|
||||
import {
|
||||
type AppClassProperties,
|
||||
type InteractiveCanvasAppState,
|
||||
} from "../types";
|
||||
|
||||
import { getClientColor, renderRemoteCursors } from "../clients";
|
||||
|
||||
import {
|
||||
bootstrapCanvas,
|
||||
drawHighlightForDiamondWithRotation,
|
||||
drawHighlightForRectWithRotation,
|
||||
fillCircle,
|
||||
getNormalizedCanvasDimensions,
|
||||
strokeEllipseWithRotation,
|
||||
strokeRectWithRotation,
|
||||
strokeRectWithRotation_simple,
|
||||
} from "./helpers";
|
||||
|
||||
import type {
|
||||
@@ -187,83 +187,485 @@ const renderSingleLinearPoint = <Point extends GlobalPoint | LocalPoint>(
|
||||
);
|
||||
};
|
||||
|
||||
const renderBindingHighlightForBindableElement = (
|
||||
const renderBindingHighlightForBindableElement_simple = (
|
||||
context: CanvasRenderingContext2D,
|
||||
element: ExcalidrawBindableElement,
|
||||
elementsMap: ElementsMap,
|
||||
zoom: InteractiveCanvasAppState["zoom"],
|
||||
appState: InteractiveCanvasAppState,
|
||||
) => {
|
||||
const padding = maxBindingGap(element, element.width, element.height, zoom);
|
||||
const enclosingFrame = element.frameId && elementsMap.get(element.frameId);
|
||||
if (enclosingFrame && isFrameLikeElement(enclosingFrame)) {
|
||||
context.translate(
|
||||
enclosingFrame.x + appState.scrollX,
|
||||
enclosingFrame.y + appState.scrollY,
|
||||
);
|
||||
|
||||
context.fillStyle = "rgba(0,0,0,.05)";
|
||||
context.beginPath();
|
||||
|
||||
if (FRAME_STYLE.radius && context.roundRect) {
|
||||
context.roundRect(
|
||||
-1,
|
||||
-1,
|
||||
enclosingFrame.width + 1,
|
||||
enclosingFrame.height + 1,
|
||||
FRAME_STYLE.radius / appState.zoom.value,
|
||||
);
|
||||
} else {
|
||||
context.rect(-1, -1, enclosingFrame.width + 1, enclosingFrame.height + 1);
|
||||
}
|
||||
|
||||
context.clip();
|
||||
|
||||
context.translate(
|
||||
-(enclosingFrame.x + appState.scrollX),
|
||||
-(enclosingFrame.y + appState.scrollY),
|
||||
);
|
||||
}
|
||||
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "text":
|
||||
case "image":
|
||||
case "iframe":
|
||||
case "embeddable":
|
||||
case "frame":
|
||||
case "magicframe":
|
||||
drawHighlightForRectWithRotation(context, element, elementsMap, padding);
|
||||
break;
|
||||
case "diamond":
|
||||
drawHighlightForDiamondWithRotation(
|
||||
context,
|
||||
padding,
|
||||
element,
|
||||
elementsMap,
|
||||
);
|
||||
break;
|
||||
case "ellipse": {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||
const width = x2 - x1;
|
||||
const height = y2 - y1;
|
||||
case "frame":
|
||||
context.save();
|
||||
|
||||
context.strokeStyle = "rgba(0,0,0,.05)";
|
||||
context.lineWidth = padding - FIXED_BINDING_DISTANCE;
|
||||
|
||||
strokeEllipseWithRotation(
|
||||
context,
|
||||
width + padding + FIXED_BINDING_DISTANCE,
|
||||
height + padding + FIXED_BINDING_DISTANCE,
|
||||
x1 + width / 2,
|
||||
y1 + height / 2,
|
||||
element.angle,
|
||||
context.translate(
|
||||
element.x + appState.scrollX,
|
||||
element.y + appState.scrollY,
|
||||
);
|
||||
|
||||
context.lineWidth = FRAME_STYLE.strokeWidth / appState.zoom.value;
|
||||
context.strokeStyle =
|
||||
appState.theme === THEME.DARK
|
||||
? `rgba(3, 93, 161, 1)`
|
||||
: `rgba(106, 189, 252, 1)`;
|
||||
|
||||
if (FRAME_STYLE.radius && context.roundRect) {
|
||||
context.beginPath();
|
||||
context.roundRect(
|
||||
0,
|
||||
0,
|
||||
element.width,
|
||||
element.height,
|
||||
FRAME_STYLE.radius / appState.zoom.value,
|
||||
);
|
||||
context.stroke();
|
||||
context.closePath();
|
||||
} else {
|
||||
context.strokeRect(0, 0, element.width, element.height);
|
||||
}
|
||||
|
||||
context.restore();
|
||||
break;
|
||||
default:
|
||||
context.save();
|
||||
|
||||
const center = elementCenterPoint(element, elementsMap);
|
||||
|
||||
context.translate(center[0], center[1]);
|
||||
context.rotate(element.angle as Radians);
|
||||
context.translate(-center[0], -center[1]);
|
||||
|
||||
context.translate(element.x, element.y);
|
||||
|
||||
context.lineWidth =
|
||||
clamp(1.75, element.strokeWidth, 4) /
|
||||
Math.max(0.25, appState.zoom.value);
|
||||
context.strokeStyle =
|
||||
appState.theme === THEME.DARK
|
||||
? `rgba(3, 93, 161, 1)`
|
||||
: `rgba(106, 189, 252, 1)`;
|
||||
|
||||
switch (element.type) {
|
||||
case "ellipse":
|
||||
context.beginPath();
|
||||
context.ellipse(
|
||||
element.width / 2,
|
||||
element.height / 2,
|
||||
element.width / 2,
|
||||
element.height / 2,
|
||||
0,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
);
|
||||
context.closePath();
|
||||
context.stroke();
|
||||
break;
|
||||
case "diamond":
|
||||
{
|
||||
const [segments, curves] = deconstructDiamondElement(element);
|
||||
|
||||
// Draw each line segment individually
|
||||
segments.forEach((segment) => {
|
||||
context.beginPath();
|
||||
context.moveTo(
|
||||
segment[0][0] - element.x,
|
||||
segment[0][1] - element.y,
|
||||
);
|
||||
context.lineTo(
|
||||
segment[1][0] - element.x,
|
||||
segment[1][1] - element.y,
|
||||
);
|
||||
context.stroke();
|
||||
});
|
||||
|
||||
// Draw each curve individually (for rounded corners)
|
||||
curves.forEach((curve) => {
|
||||
const [start, control1, control2, end] = curve;
|
||||
context.beginPath();
|
||||
context.moveTo(start[0] - element.x, start[1] - element.y);
|
||||
context.bezierCurveTo(
|
||||
control1[0] - element.x,
|
||||
control1[1] - element.y,
|
||||
control2[0] - element.x,
|
||||
control2[1] - element.y,
|
||||
end[0] - element.x,
|
||||
end[1] - element.y,
|
||||
);
|
||||
context.stroke();
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
{
|
||||
const [segments, curves] = deconstructRectanguloidElement(element);
|
||||
|
||||
// Draw each line segment individually
|
||||
segments.forEach((segment) => {
|
||||
context.beginPath();
|
||||
context.moveTo(
|
||||
segment[0][0] - element.x,
|
||||
segment[0][1] - element.y,
|
||||
);
|
||||
context.lineTo(
|
||||
segment[1][0] - element.x,
|
||||
segment[1][1] - element.y,
|
||||
);
|
||||
context.stroke();
|
||||
});
|
||||
|
||||
// Draw each curve individually (for rounded corners)
|
||||
curves.forEach((curve) => {
|
||||
const [start, control1, control2, end] = curve;
|
||||
context.beginPath();
|
||||
context.moveTo(start[0] - element.x, start[1] - element.y);
|
||||
context.bezierCurveTo(
|
||||
control1[0] - element.x,
|
||||
control1[1] - element.y,
|
||||
control2[0] - element.x,
|
||||
control2[1] - element.y,
|
||||
end[0] - element.x,
|
||||
end[1] - element.y,
|
||||
);
|
||||
context.stroke();
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
context.restore();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderBindingHighlightForSuggestedPointBinding = (
|
||||
const renderBindingHighlightForBindableElement_complex = (
|
||||
app: AppClassProperties,
|
||||
context: CanvasRenderingContext2D,
|
||||
suggestedBinding: SuggestedPointBinding,
|
||||
elementsMap: ElementsMap,
|
||||
zoom: InteractiveCanvasAppState["zoom"],
|
||||
element: ExcalidrawBindableElement,
|
||||
allElementsMap: NonDeletedSceneElementsMap,
|
||||
appState: InteractiveCanvasAppState,
|
||||
deltaTime: number,
|
||||
state?: { runtime: number },
|
||||
) => {
|
||||
const [element, startOrEnd, bindableElement] = suggestedBinding;
|
||||
const countdownInProgress =
|
||||
app.state.bindMode === "orbit" && app.bindModeHandler !== null;
|
||||
|
||||
const threshold = maxBindingGap(
|
||||
bindableElement,
|
||||
bindableElement.width,
|
||||
bindableElement.height,
|
||||
zoom,
|
||||
);
|
||||
const remainingTime =
|
||||
BIND_MODE_TIMEOUT -
|
||||
(state?.runtime ?? (countdownInProgress ? 0 : BIND_MODE_TIMEOUT));
|
||||
const opacity = clamp((1 / BIND_MODE_TIMEOUT) * remainingTime, 0.0001, 1);
|
||||
const offset = element.strokeWidth / 2;
|
||||
|
||||
context.strokeStyle = "rgba(0,0,0,0)";
|
||||
context.fillStyle = "rgba(0,0,0,.05)";
|
||||
|
||||
const pointIndices =
|
||||
startOrEnd === "both" ? [0, -1] : startOrEnd === "start" ? [0] : [-1];
|
||||
pointIndices.forEach((index) => {
|
||||
const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
element,
|
||||
index,
|
||||
elementsMap,
|
||||
const enclosingFrame = element.frameId && allElementsMap.get(element.frameId);
|
||||
if (enclosingFrame && isFrameLikeElement(enclosingFrame)) {
|
||||
context.translate(
|
||||
enclosingFrame.x + appState.scrollX,
|
||||
enclosingFrame.y + appState.scrollY,
|
||||
);
|
||||
fillCircle(context, x, y, threshold, true);
|
||||
});
|
||||
|
||||
context.beginPath();
|
||||
|
||||
if (FRAME_STYLE.radius && context.roundRect) {
|
||||
context.roundRect(
|
||||
-1,
|
||||
-1,
|
||||
enclosingFrame.width + 1,
|
||||
enclosingFrame.height + 1,
|
||||
FRAME_STYLE.radius / appState.zoom.value,
|
||||
);
|
||||
} else {
|
||||
context.rect(-1, -1, enclosingFrame.width + 1, enclosingFrame.height + 1);
|
||||
}
|
||||
|
||||
context.clip();
|
||||
|
||||
context.translate(
|
||||
-(enclosingFrame.x + appState.scrollX),
|
||||
-(enclosingFrame.y + appState.scrollY),
|
||||
);
|
||||
}
|
||||
|
||||
switch (element.type) {
|
||||
case "magicframe":
|
||||
case "frame":
|
||||
context.save();
|
||||
|
||||
context.translate(
|
||||
element.x + appState.scrollX,
|
||||
element.y + appState.scrollY,
|
||||
);
|
||||
|
||||
context.lineWidth = FRAME_STYLE.strokeWidth / appState.zoom.value;
|
||||
context.strokeStyle =
|
||||
appState.theme === THEME.DARK
|
||||
? `rgba(3, 93, 161, ${opacity})`
|
||||
: `rgba(106, 189, 252, ${opacity})`;
|
||||
|
||||
if (FRAME_STYLE.radius && context.roundRect) {
|
||||
context.beginPath();
|
||||
context.roundRect(
|
||||
0,
|
||||
0,
|
||||
element.width,
|
||||
element.height,
|
||||
FRAME_STYLE.radius / appState.zoom.value,
|
||||
);
|
||||
context.stroke();
|
||||
context.closePath();
|
||||
} else {
|
||||
context.strokeRect(0, 0, element.width, element.height);
|
||||
}
|
||||
|
||||
context.restore();
|
||||
break;
|
||||
default:
|
||||
context.save();
|
||||
|
||||
const center = elementCenterPoint(element, allElementsMap);
|
||||
const cx = center[0] + appState.scrollX;
|
||||
const cy = center[1] + appState.scrollY;
|
||||
|
||||
context.translate(cx, cy);
|
||||
context.rotate(element.angle as Radians);
|
||||
context.translate(-cx, -cy);
|
||||
|
||||
context.translate(
|
||||
element.x + appState.scrollX - offset,
|
||||
element.y + appState.scrollY - offset,
|
||||
);
|
||||
|
||||
context.lineWidth =
|
||||
clamp(2.5, element.strokeWidth * 1.75, 4) /
|
||||
Math.max(0.25, appState.zoom.value);
|
||||
context.strokeStyle =
|
||||
appState.theme === THEME.DARK
|
||||
? `rgba(3, 93, 161, ${opacity / 2})`
|
||||
: `rgba(106, 189, 252, ${opacity / 2})`;
|
||||
|
||||
switch (element.type) {
|
||||
case "ellipse":
|
||||
context.beginPath();
|
||||
context.ellipse(
|
||||
(element.width + offset * 2) / 2,
|
||||
(element.height + offset * 2) / 2,
|
||||
(element.width + offset * 2) / 2,
|
||||
(element.height + offset * 2) / 2,
|
||||
0,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
);
|
||||
context.closePath();
|
||||
context.stroke();
|
||||
break;
|
||||
case "diamond":
|
||||
{
|
||||
const [segments, curves] = deconstructDiamondElement(
|
||||
element,
|
||||
offset,
|
||||
);
|
||||
|
||||
// Draw each line segment individually
|
||||
segments.forEach((segment) => {
|
||||
context.beginPath();
|
||||
context.moveTo(
|
||||
segment[0][0] - element.x + offset,
|
||||
segment[0][1] - element.y + offset,
|
||||
);
|
||||
context.lineTo(
|
||||
segment[1][0] - element.x + offset,
|
||||
segment[1][1] - element.y + offset,
|
||||
);
|
||||
context.stroke();
|
||||
});
|
||||
|
||||
// Draw each curve individually (for rounded corners)
|
||||
curves.forEach((curve) => {
|
||||
const [start, control1, control2, end] = curve;
|
||||
context.beginPath();
|
||||
context.moveTo(
|
||||
start[0] - element.x + offset,
|
||||
start[1] - element.y + offset,
|
||||
);
|
||||
context.bezierCurveTo(
|
||||
control1[0] - element.x + offset,
|
||||
control1[1] - element.y + offset,
|
||||
control2[0] - element.x + offset,
|
||||
control2[1] - element.y + offset,
|
||||
end[0] - element.x + offset,
|
||||
end[1] - element.y + offset,
|
||||
);
|
||||
context.stroke();
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
{
|
||||
const [segments, curves] = deconstructRectanguloidElement(
|
||||
element,
|
||||
offset,
|
||||
);
|
||||
|
||||
// Draw each line segment individually
|
||||
segments.forEach((segment) => {
|
||||
context.beginPath();
|
||||
context.moveTo(
|
||||
segment[0][0] - element.x + offset,
|
||||
segment[0][1] - element.y + offset,
|
||||
);
|
||||
context.lineTo(
|
||||
segment[1][0] - element.x + offset,
|
||||
segment[1][1] - element.y + offset,
|
||||
);
|
||||
context.stroke();
|
||||
});
|
||||
|
||||
// Draw each curve individually (for rounded corners)
|
||||
curves.forEach((curve) => {
|
||||
const [start, control1, control2, end] = curve;
|
||||
context.beginPath();
|
||||
context.moveTo(
|
||||
start[0] - element.x + offset,
|
||||
start[1] - element.y + offset,
|
||||
);
|
||||
context.bezierCurveTo(
|
||||
control1[0] - element.x + offset,
|
||||
control1[1] - element.y + offset,
|
||||
control2[0] - element.x + offset,
|
||||
control2[1] - element.y + offset,
|
||||
end[0] - element.x + offset,
|
||||
end[1] - element.y + offset,
|
||||
);
|
||||
context.stroke();
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
context.restore();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Middle indicator is not rendered after it expired
|
||||
if (!countdownInProgress || (state?.runtime ?? 0) > BIND_MODE_TIMEOUT) {
|
||||
return;
|
||||
}
|
||||
|
||||
const radius = 0.5 * (Math.min(element.width, element.height) / 2);
|
||||
|
||||
// Draw center snap area
|
||||
if (!isFrameLikeElement(element)) {
|
||||
context.save();
|
||||
context.translate(
|
||||
element.x + appState.scrollX,
|
||||
element.y + appState.scrollY,
|
||||
);
|
||||
|
||||
const PROGRESS_RATIO = (1 / BIND_MODE_TIMEOUT) * remainingTime;
|
||||
|
||||
context.strokeStyle = "rgba(0, 0, 0, 0.2)";
|
||||
context.lineWidth = 1 / appState.zoom.value;
|
||||
context.setLineDash([4 / appState.zoom.value, 4 / appState.zoom.value]);
|
||||
context.lineDashOffset = (-PROGRESS_RATIO * 10) / appState.zoom.value;
|
||||
|
||||
context.beginPath();
|
||||
context.ellipse(
|
||||
element.width / 2,
|
||||
element.height / 2,
|
||||
radius,
|
||||
radius,
|
||||
0,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
);
|
||||
context.stroke();
|
||||
|
||||
// context.strokeStyle = "transparent";
|
||||
context.fillStyle = "rgba(0, 0, 0, 0.04)";
|
||||
context.beginPath();
|
||||
context.ellipse(
|
||||
element.width / 2,
|
||||
element.height / 2,
|
||||
radius * (1 - opacity),
|
||||
radius * (1 - opacity),
|
||||
0,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
);
|
||||
|
||||
context.fill();
|
||||
|
||||
context.restore();
|
||||
}
|
||||
|
||||
return {
|
||||
runtime: (state?.runtime ?? 0) + deltaTime,
|
||||
};
|
||||
};
|
||||
|
||||
const renderBindingHighlightForBindableElement = (
|
||||
app: AppClassProperties,
|
||||
context: CanvasRenderingContext2D,
|
||||
element: ExcalidrawBindableElement,
|
||||
allElementsMap: NonDeletedSceneElementsMap,
|
||||
appState: InteractiveCanvasAppState,
|
||||
deltaTime: number,
|
||||
state?: { runtime: number },
|
||||
) => {
|
||||
if (getFeatureFlag("COMPLEX_BINDINGS")) {
|
||||
return renderBindingHighlightForBindableElement_complex(
|
||||
app,
|
||||
context,
|
||||
element,
|
||||
allElementsMap,
|
||||
appState,
|
||||
deltaTime,
|
||||
state,
|
||||
);
|
||||
}
|
||||
|
||||
context.save();
|
||||
context.translate(appState.scrollX, appState.scrollY);
|
||||
renderBindingHighlightForBindableElement_simple(
|
||||
context,
|
||||
element,
|
||||
allElementsMap,
|
||||
appState,
|
||||
);
|
||||
context.restore();
|
||||
};
|
||||
|
||||
type ElementSelectionBorder = {
|
||||
@@ -321,7 +723,7 @@ const renderSelectionBorder = (
|
||||
]);
|
||||
}
|
||||
context.lineDashOffset = (lineWidth + spaceWidth) * index;
|
||||
strokeRectWithRotation(
|
||||
strokeRectWithRotation_simple(
|
||||
context,
|
||||
x1 - linePadding,
|
||||
y1 - linePadding,
|
||||
@@ -335,23 +737,6 @@ const renderSelectionBorder = (
|
||||
context.restore();
|
||||
};
|
||||
|
||||
const renderBindingHighlight = (
|
||||
context: CanvasRenderingContext2D,
|
||||
appState: InteractiveCanvasAppState,
|
||||
suggestedBinding: SuggestedBinding,
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
const renderHighlight = Array.isArray(suggestedBinding)
|
||||
? renderBindingHighlightForSuggestedPointBinding
|
||||
: renderBindingHighlightForBindableElement;
|
||||
|
||||
context.save();
|
||||
context.translate(appState.scrollX, appState.scrollY);
|
||||
renderHighlight(context, suggestedBinding as any, elementsMap, appState.zoom);
|
||||
|
||||
context.restore();
|
||||
};
|
||||
|
||||
const renderFrameHighlight = (
|
||||
context: CanvasRenderingContext2D,
|
||||
appState: InteractiveCanvasAppState,
|
||||
@@ -367,7 +752,7 @@ const renderFrameHighlight = (
|
||||
|
||||
context.save();
|
||||
context.translate(appState.scrollX, appState.scrollY);
|
||||
strokeRectWithRotation(
|
||||
strokeRectWithRotation_simple(
|
||||
context,
|
||||
x1,
|
||||
y1,
|
||||
@@ -580,7 +965,7 @@ const renderTransformHandles = (
|
||||
context.fill();
|
||||
context.stroke();
|
||||
} else {
|
||||
strokeRectWithRotation(
|
||||
strokeRectWithRotation_simple(
|
||||
context,
|
||||
x,
|
||||
y,
|
||||
@@ -725,6 +1110,7 @@ const renderTextBox = (
|
||||
};
|
||||
|
||||
const _renderInteractiveScene = ({
|
||||
app,
|
||||
canvas,
|
||||
elementsMap,
|
||||
visibleElements,
|
||||
@@ -750,8 +1136,7 @@ const _renderInteractiveScene = ({
|
||||
canvas,
|
||||
scale,
|
||||
);
|
||||
|
||||
const nextAnimationState = animationState;
|
||||
let nextAnimationState = animationState;
|
||||
|
||||
const context = bootstrapCanvas({
|
||||
canvas,
|
||||
@@ -821,17 +1206,24 @@ const _renderInteractiveScene = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (appState.isBindingEnabled) {
|
||||
appState.suggestedBindings
|
||||
.filter((binding) => binding != null)
|
||||
.forEach((suggestedBinding) => {
|
||||
renderBindingHighlight(
|
||||
context,
|
||||
appState,
|
||||
suggestedBinding!,
|
||||
elementsMap,
|
||||
);
|
||||
});
|
||||
if (appState.isBindingEnabled && appState.suggestedBinding) {
|
||||
nextAnimationState = {
|
||||
...animationState,
|
||||
bindingHighlight: renderBindingHighlightForBindableElement(
|
||||
app,
|
||||
context,
|
||||
appState.suggestedBinding,
|
||||
allElementsMap,
|
||||
appState,
|
||||
deltaTime,
|
||||
animationState?.bindingHighlight,
|
||||
),
|
||||
};
|
||||
} else {
|
||||
nextAnimationState = {
|
||||
...animationState,
|
||||
bindingHighlight: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (appState.frameToHighlight) {
|
||||
@@ -886,20 +1278,26 @@ const _renderInteractiveScene = ({
|
||||
(el) => el.id === editor.elementId, // Don't forget bound text elements!
|
||||
);
|
||||
|
||||
if (editor.segmentMidPointHoveredCoords) {
|
||||
renderElbowArrowMidPointHighlight(context, appState);
|
||||
} else if (
|
||||
isElbowArrow(firstSelectedLinear)
|
||||
? editor.hoverPointIndex === 0 ||
|
||||
editor.hoverPointIndex === firstSelectedLinear.points.length - 1
|
||||
: editor.hoverPointIndex >= 0
|
||||
) {
|
||||
renderLinearElementPointHighlight(context, appState, elementsMap);
|
||||
if (!appState.selectedLinearElement.isDragging) {
|
||||
if (editor.segmentMidPointHoveredCoords) {
|
||||
renderElbowArrowMidPointHighlight(context, appState);
|
||||
} else if (
|
||||
isElbowArrow(firstSelectedLinear)
|
||||
? editor.hoverPointIndex === 0 ||
|
||||
editor.hoverPointIndex === firstSelectedLinear.points.length - 1
|
||||
: editor.hoverPointIndex >= 0
|
||||
) {
|
||||
renderLinearElementPointHighlight(context, appState, elementsMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Paint selected elements
|
||||
if (!appState.multiElement && !appState.selectedLinearElement?.isEditing) {
|
||||
if (
|
||||
!appState.multiElement &&
|
||||
!appState.newElement &&
|
||||
!appState.selectedLinearElement?.isEditing
|
||||
) {
|
||||
const showBoundingBox = hasBoundingBox(
|
||||
selectedElements,
|
||||
appState,
|
||||
@@ -1082,7 +1480,7 @@ const _renderInteractiveScene = ({
|
||||
const lineWidth = context.lineWidth;
|
||||
context.lineWidth = 1 / appState.zoom.value;
|
||||
context.strokeStyle = selectionColor;
|
||||
strokeRectWithRotation(
|
||||
strokeRectWithRotation_simple(
|
||||
context,
|
||||
x1 - dashedLinePadding,
|
||||
y1 - dashedLinePadding,
|
||||
|
||||
@@ -65,6 +65,7 @@ export type InteractiveCanvasRenderConfig = {
|
||||
remotePointerUsernames: Map<SocketId, string>;
|
||||
remotePointerButton: Map<SocketId, string | undefined>;
|
||||
selectionColor: string;
|
||||
lastViewportPosition: { x: number; y: number };
|
||||
// extra options passed to the renderer
|
||||
// ---------------------------------------------------------------------------
|
||||
renderScrollbars?: boolean;
|
||||
|
||||
@@ -11,6 +11,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
},
|
||||
"bindMode": "orbit",
|
||||
"collaborators": Map {},
|
||||
"contextMenu": {
|
||||
"items": [
|
||||
@@ -985,7 +986,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
||||
"open": false,
|
||||
"panels": 3,
|
||||
},
|
||||
"suggestedBindings": [],
|
||||
"suggestedBinding": null,
|
||||
"theme": "light",
|
||||
"toast": null,
|
||||
"userToFollow": null,
|
||||
@@ -1086,6 +1087,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
},
|
||||
"bindMode": "orbit",
|
||||
"collaborators": Map {},
|
||||
"contextMenu": null,
|
||||
"croppingElementId": null,
|
||||
@@ -1180,7 +1182,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
||||
"open": false,
|
||||
"panels": 3,
|
||||
},
|
||||
"suggestedBindings": [],
|
||||
"suggestedBinding": null,
|
||||
"theme": "light",
|
||||
"toast": {
|
||||
"message": "Added to library",
|
||||
@@ -1302,6 +1304,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
},
|
||||
"bindMode": "orbit",
|
||||
"collaborators": Map {},
|
||||
"contextMenu": null,
|
||||
"croppingElementId": null,
|
||||
@@ -1396,7 +1399,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
||||
"open": false,
|
||||
"panels": 3,
|
||||
},
|
||||
"suggestedBindings": [],
|
||||
"suggestedBinding": null,
|
||||
"theme": "light",
|
||||
"toast": null,
|
||||
"userToFollow": null,
|
||||
@@ -1635,6 +1638,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
},
|
||||
"bindMode": "orbit",
|
||||
"collaborators": Map {},
|
||||
"contextMenu": null,
|
||||
"croppingElementId": null,
|
||||
@@ -1729,7 +1733,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
||||
"open": false,
|
||||
"panels": 3,
|
||||
},
|
||||
"suggestedBindings": [],
|
||||
"suggestedBinding": null,
|
||||
"theme": "light",
|
||||
"toast": null,
|
||||
"userToFollow": null,
|
||||
@@ -1968,6 +1972,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
},
|
||||
"bindMode": "orbit",
|
||||
"collaborators": Map {},
|
||||
"contextMenu": null,
|
||||
"croppingElementId": null,
|
||||
@@ -2062,7 +2067,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
||||
"open": false,
|
||||
"panels": 3,
|
||||
},
|
||||
"suggestedBindings": [],
|
||||
"suggestedBinding": null,
|
||||
"theme": "light",
|
||||
"toast": {
|
||||
"message": "Copied styles.",
|
||||
@@ -2184,6 +2189,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
},
|
||||
"bindMode": "orbit",
|
||||
"collaborators": Map {},
|
||||
"contextMenu": null,
|
||||
"croppingElementId": null,
|
||||
@@ -2276,7 +2282,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
||||
"open": false,
|
||||
"panels": 3,
|
||||
},
|
||||
"suggestedBindings": [],
|
||||
"suggestedBinding": null,
|
||||
"theme": "light",
|
||||
"toast": null,
|
||||
"userToFollow": null,
|
||||
@@ -2427,6 +2433,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
},
|
||||
"bindMode": "orbit",
|
||||
"collaborators": Map {},
|
||||
"contextMenu": null,
|
||||
"croppingElementId": null,
|
||||
@@ -2521,7 +2528,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"open": false,
|
||||
"panels": 3,
|
||||
},
|
||||
"suggestedBindings": [],
|
||||
"suggestedBinding": null,
|
||||
"theme": "light",
|
||||
"toast": null,
|
||||
"userToFollow": null,
|
||||
@@ -2727,6 +2734,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
},
|
||||
"bindMode": "orbit",
|
||||
"collaborators": Map {},
|
||||
"contextMenu": null,
|
||||
"croppingElementId": null,
|
||||
@@ -2826,7 +2834,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
||||
"open": false,
|
||||
"panels": 3,
|
||||
},
|
||||
"suggestedBindings": [],
|
||||
"suggestedBinding": null,
|
||||
"theme": "light",
|
||||
"toast": null,
|
||||
"userToFollow": null,
|
||||
@@ -3101,6 +3109,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
},
|
||||
"bindMode": "orbit",
|
||||
"collaborators": Map {},
|
||||
"contextMenu": null,
|
||||
"croppingElementId": null,
|
||||
@@ -3195,7 +3204,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
||||
"open": false,
|
||||
"panels": 3,
|
||||
},
|
||||
"suggestedBindings": [],
|
||||
"suggestedBinding": null,
|
||||
"theme": "light",
|
||||
"toast": {
|
||||
"message": "Copied styles.",
|
||||
@@ -3596,6 +3605,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
},
|
||||
"bindMode": "orbit",
|
||||
"collaborators": Map {},
|
||||
"contextMenu": null,
|
||||
"croppingElementId": null,
|
||||
@@ -3690,7 +3700,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
||||
"open": false,
|
||||
"panels": 3,
|
||||
},
|
||||
"suggestedBindings": [],
|
||||
"suggestedBinding": null,
|
||||
"theme": "light",
|
||||
"toast": null,
|
||||
"userToFollow": null,
|
||||
@@ -3921,6 +3931,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
},
|
||||
"bindMode": "orbit",
|
||||
"collaborators": Map {},
|
||||
"contextMenu": null,
|
||||
"croppingElementId": null,
|
||||
@@ -4015,7 +4026,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
||||
"open": false,
|
||||
"panels": 3,
|
||||
},
|
||||
"suggestedBindings": [],
|
||||
"suggestedBinding": null,
|
||||
"theme": "light",
|
||||
"toast": null,
|
||||
"userToFollow": null,
|
||||
@@ -4246,6 +4257,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
},
|
||||
"bindMode": "orbit",
|
||||
"collaborators": Map {},
|
||||
"contextMenu": null,
|
||||
"croppingElementId": null,
|
||||
@@ -4343,7 +4355,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
||||
"open": false,
|
||||
"panels": 3,
|
||||
},
|
||||
"suggestedBindings": [],
|
||||
"suggestedBinding": null,
|
||||
"theme": "light",
|
||||
"toast": null,
|
||||
"userToFollow": null,
|
||||
@@ -4659,6 +4671,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
},
|
||||
"bindMode": "orbit",
|
||||
"collaborators": Map {},
|
||||
"contextMenu": {
|
||||
"items": [
|
||||
@@ -5630,7 +5643,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
||||
"open": false,
|
||||
"panels": 3,
|
||||
},
|
||||
"suggestedBindings": [],
|
||||
"suggestedBinding": null,
|
||||
"theme": "light",
|
||||
"toast": null,
|
||||
"userToFollow": null,
|
||||
@@ -5878,6 +5891,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
},
|
||||
"bindMode": "orbit",
|
||||
"collaborators": Map {},
|
||||
"contextMenu": {
|
||||
"items": [
|
||||
@@ -6851,7 +6865,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
||||
"open": false,
|
||||
"panels": 3,
|
||||
},
|
||||
"suggestedBindings": [],
|
||||
"suggestedBinding": null,
|
||||
"theme": "light",
|
||||
"toast": null,
|
||||
"userToFollow": null,
|
||||
@@ -7148,6 +7162,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
},
|
||||
"bindMode": "orbit",
|
||||
"collaborators": Map {},
|
||||
"contextMenu": {
|
||||
"items": [
|
||||
@@ -7784,7 +7799,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
||||
"open": false,
|
||||
"panels": 3,
|
||||
},
|
||||
"suggestedBindings": [],
|
||||
"suggestedBinding": null,
|
||||
"theme": "light",
|
||||
"toast": null,
|
||||
"userToFollow": null,
|
||||
@@ -7817,6 +7832,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
},
|
||||
"bindMode": "orbit",
|
||||
"collaborators": Map {},
|
||||
"contextMenu": {
|
||||
"items": [
|
||||
@@ -8785,7 +8801,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||
"open": false,
|
||||
"panels": 3,
|
||||
},
|
||||
"suggestedBindings": [],
|
||||
"suggestedBinding": null,
|
||||
"theme": "light",
|
||||
"toast": null,
|
||||
"userToFollow": null,
|
||||
@@ -8810,6 +8826,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
},
|
||||
"bindMode": "orbit",
|
||||
"collaborators": Map {},
|
||||
"contextMenu": {
|
||||
"items": [
|
||||
@@ -9781,7 +9798,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||
"open": false,
|
||||
"panels": 3,
|
||||
},
|
||||
"suggestedBindings": [],
|
||||
"suggestedBinding": null,
|
||||
"theme": "light",
|
||||
"toast": null,
|
||||
"userToFollow": null,
|
||||
|
||||
@@ -18,7 +18,6 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
||||
"id": "id0",
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
@@ -135,7 +134,6 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
||||
"id": "id0",
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -126,7 +126,7 @@ exports[`move element > rectangles with binding arrow 5`] = `
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 1006504105,
|
||||
"versionNonce": 760410951,
|
||||
"width": 100,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
@@ -163,7 +163,7 @@ exports[`move element > rectangles with binding arrow 6`] = `
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 7,
|
||||
"versionNonce": 1984422985,
|
||||
"versionNonce": 888958951,
|
||||
"width": 300,
|
||||
"x": 201,
|
||||
"y": 2,
|
||||
@@ -180,19 +180,22 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
||||
"endArrowhead": "arrow",
|
||||
"endBinding": {
|
||||
"elementId": "id3",
|
||||
"focus": "-0.46667",
|
||||
"gap": 10,
|
||||
"fixedPoint": [
|
||||
"-0.03667",
|
||||
"0.42773",
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "81.40630",
|
||||
"height": "124.16785",
|
||||
"id": "id6",
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"moveMidPointsWithElement": false,
|
||||
"opacity": 100,
|
||||
"points": [
|
||||
[
|
||||
@@ -200,8 +203,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
||||
0,
|
||||
],
|
||||
[
|
||||
"81.00000",
|
||||
"81.40630",
|
||||
79,
|
||||
"124.16785",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@@ -212,18 +215,21 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
||||
"startArrowhead": null,
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"focus": "-0.60000",
|
||||
"gap": 10,
|
||||
"fixedPoint": [
|
||||
"1.10000",
|
||||
"-0.04577",
|
||||
],
|
||||
"mode": "orbit",
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 11,
|
||||
"versionNonce": 1573789895,
|
||||
"width": "81.00000",
|
||||
"x": "110.00000",
|
||||
"y": 50,
|
||||
"version": 12,
|
||||
"versionNonce": 2066753033,
|
||||
"width": 79,
|
||||
"x": 111,
|
||||
"y": "6.14995",
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -16,10 +16,6 @@ exports[`multi point mode in linear elements > arrow 3`] = `
|
||||
"id": "id0",
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": [
|
||||
70,
|
||||
110,
|
||||
],
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
@@ -49,8 +45,8 @@ exports[`multi point mode in linear elements > arrow 3`] = `
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 8,
|
||||
"versionNonce": 1604849351,
|
||||
"version": 5,
|
||||
"versionNonce": 1014066025,
|
||||
"width": 70,
|
||||
"x": 30,
|
||||
"y": 30,
|
||||
@@ -72,10 +68,6 @@ exports[`multi point mode in linear elements > line 3`] = `
|
||||
"id": "id0",
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": [
|
||||
70,
|
||||
110,
|
||||
],
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
@@ -104,8 +96,8 @@ exports[`multi point mode in linear elements > line 3`] = `
|
||||
"strokeWidth": 2,
|
||||
"type": "line",
|
||||
"updated": 1,
|
||||
"version": 8,
|
||||
"versionNonce": 1604849351,
|
||||
"version": 5,
|
||||
"versionNonce": 1014066025,
|
||||
"width": 70,
|
||||
"x": 30,
|
||||
"y": 30,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,6 @@ exports[`select single element on the scene > arrow 1`] = `
|
||||
"id": "id0",
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
@@ -65,7 +64,6 @@ exports[`select single element on the scene > arrow escape 1`] = `
|
||||
"id": "id0",
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
|
||||
@@ -16,7 +16,6 @@ exports[`restoreElements > should restore arrow element correctly 1`] = `
|
||||
"id": "id-arrow01",
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
@@ -175,7 +174,6 @@ exports[`restoreElements > should restore freedraw element correctly 1`] = `
|
||||
"id": "id-freedraw01",
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
@@ -222,7 +220,6 @@ exports[`restoreElements > should restore line and draw elements correctly 1`] =
|
||||
"id": "id-line01",
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
@@ -270,7 +267,6 @@ exports[`restoreElements > should restore line and draw elements correctly 2`] =
|
||||
"id": "id-draw01",
|
||||
"index": "a1",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
|
||||
@@ -157,9 +157,9 @@ describe("Test dragCreate", () => {
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`5`,
|
||||
`6`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
|
||||
expect(h.elements.length).toEqual(1);
|
||||
@@ -195,9 +195,9 @@ describe("Test dragCreate", () => {
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`5`,
|
||||
`6`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
|
||||
expect(h.elements.length).toEqual(1);
|
||||
|
||||
@@ -1021,7 +1021,7 @@ describe("history", () => {
|
||||
// leave editor
|
||||
Keyboard.keyPress(KEYS.ESCAPE);
|
||||
|
||||
expect(API.getUndoStack().length).toBe(6);
|
||||
expect(API.getUndoStack().length).toBe(5);
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
expect(assertSelectedElements(h.elements[0]));
|
||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
||||
@@ -1038,7 +1038,7 @@ describe("history", () => {
|
||||
]);
|
||||
|
||||
Keyboard.undo();
|
||||
expect(API.getUndoStack().length).toBe(5);
|
||||
expect(API.getUndoStack().length).toBe(4);
|
||||
expect(API.getRedoStack().length).toBe(1);
|
||||
expect(assertSelectedElements(h.elements[0]));
|
||||
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
|
||||
@@ -1058,11 +1058,11 @@ describe("history", () => {
|
||||
mouse.clickAt(0, 0);
|
||||
mouse.clickAt(10, 10);
|
||||
mouse.clickAt(20, 20);
|
||||
expect(API.getUndoStack().length).toBe(5);
|
||||
expect(API.getUndoStack().length).toBe(4);
|
||||
expect(API.getRedoStack().length).toBe(1);
|
||||
|
||||
Keyboard.undo();
|
||||
expect(API.getUndoStack().length).toBe(4);
|
||||
expect(API.getUndoStack().length).toBe(3);
|
||||
expect(API.getRedoStack().length).toBe(2);
|
||||
expect(assertSelectedElements(h.elements[0]));
|
||||
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
|
||||
@@ -1079,10 +1079,10 @@ describe("history", () => {
|
||||
]);
|
||||
|
||||
Keyboard.undo();
|
||||
expect(API.getUndoStack().length).toBe(3);
|
||||
expect(API.getUndoStack().length).toBe(2);
|
||||
expect(API.getRedoStack().length).toBe(3);
|
||||
expect(assertSelectedElements(h.elements[0]));
|
||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false); // undo `open editor`
|
||||
expect(h.state.selectedLinearElement?.isEditing).toBe(false); // undo `open editor`
|
||||
expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
@@ -1095,29 +1095,29 @@ describe("history", () => {
|
||||
}),
|
||||
]);
|
||||
|
||||
Keyboard.undo();
|
||||
expect(API.getUndoStack().length).toBe(2);
|
||||
expect(API.getRedoStack().length).toBe(4);
|
||||
expect(assertSelectedElements(h.elements[0]));
|
||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
||||
expect(h.state.selectedLinearElement).toBeNull(); // undo `actionFinalize`
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
isDeleted: false,
|
||||
points: [
|
||||
[0, 0],
|
||||
[10, 10],
|
||||
[20, 0],
|
||||
],
|
||||
}),
|
||||
]);
|
||||
// Keyboard.undo();
|
||||
// expect(API.getUndoStack().length).toBe(2);
|
||||
// expect(API.getRedoStack().length).toBe(4);
|
||||
// expect(assertSelectedElements(h.elements[0]));
|
||||
// expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
||||
// expect(h.state.selectedLinearElement).toBeNull(); // undo `actionFinalize`
|
||||
// expect(h.elements).toEqual([
|
||||
// expect.objectContaining({
|
||||
// isDeleted: false,
|
||||
// points: [
|
||||
// [0, 0],
|
||||
// [10, 10],
|
||||
// [20, 0],
|
||||
// ],
|
||||
// }),
|
||||
// ]);
|
||||
|
||||
Keyboard.undo();
|
||||
expect(API.getUndoStack().length).toBe(1);
|
||||
expect(API.getRedoStack().length).toBe(5);
|
||||
expect(API.getRedoStack().length).toBe(4);
|
||||
expect(assertSelectedElements(h.elements[0]));
|
||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
||||
expect(h.state.selectedLinearElement).toBeNull();
|
||||
expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
||||
expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
isDeleted: false,
|
||||
@@ -1130,9 +1130,8 @@ describe("history", () => {
|
||||
|
||||
Keyboard.undo();
|
||||
expect(API.getUndoStack().length).toBe(0);
|
||||
expect(API.getRedoStack().length).toBe(6);
|
||||
expect(API.getRedoStack().length).toBe(5);
|
||||
expect(API.getSelectedElements().length).toBe(0);
|
||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
||||
expect(h.state.selectedLinearElement).toBeNull();
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
@@ -1146,10 +1145,10 @@ describe("history", () => {
|
||||
|
||||
Keyboard.redo();
|
||||
expect(API.getUndoStack().length).toBe(1);
|
||||
expect(API.getRedoStack().length).toBe(5);
|
||||
expect(API.getRedoStack().length).toBe(4);
|
||||
expect(assertSelectedElements(h.elements[0]));
|
||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
||||
expect(h.state.selectedLinearElement).toBeNull();
|
||||
expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
||||
expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
isDeleted: false,
|
||||
@@ -1160,25 +1159,25 @@ describe("history", () => {
|
||||
}),
|
||||
]);
|
||||
|
||||
// Keyboard.redo();
|
||||
// expect(API.getUndoStack().length).toBe(2);
|
||||
// expect(API.getRedoStack().length).toBe(3);
|
||||
// expect(assertSelectedElements(h.elements[0]));
|
||||
// expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
||||
// expect(h.state.selectedLinearElement).toBeNull(); // undo `actionFinalize`
|
||||
// expect(h.elements).toEqual([
|
||||
// expect.objectContaining({
|
||||
// isDeleted: false,
|
||||
// points: [
|
||||
// [0, 0],
|
||||
// [10, 10],
|
||||
// [20, 0],
|
||||
// ],
|
||||
// }),
|
||||
// ]);
|
||||
|
||||
Keyboard.redo();
|
||||
expect(API.getUndoStack().length).toBe(2);
|
||||
expect(API.getRedoStack().length).toBe(4);
|
||||
expect(assertSelectedElements(h.elements[0]));
|
||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
||||
expect(h.state.selectedLinearElement).toBeNull(); // undo `actionFinalize`
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
isDeleted: false,
|
||||
points: [
|
||||
[0, 0],
|
||||
[10, 10],
|
||||
[20, 0],
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
Keyboard.redo();
|
||||
expect(API.getUndoStack().length).toBe(3);
|
||||
expect(API.getRedoStack().length).toBe(3);
|
||||
expect(assertSelectedElements(h.elements[0]));
|
||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false); // undo `open editor`
|
||||
@@ -1195,7 +1194,7 @@ describe("history", () => {
|
||||
]);
|
||||
|
||||
Keyboard.redo();
|
||||
expect(API.getUndoStack().length).toBe(4);
|
||||
expect(API.getUndoStack().length).toBe(3);
|
||||
expect(API.getRedoStack().length).toBe(2);
|
||||
expect(assertSelectedElements(h.elements[0]));
|
||||
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
|
||||
@@ -1212,7 +1211,7 @@ describe("history", () => {
|
||||
]);
|
||||
|
||||
Keyboard.redo();
|
||||
expect(API.getUndoStack().length).toBe(5);
|
||||
expect(API.getUndoStack().length).toBe(4);
|
||||
expect(API.getRedoStack().length).toBe(1);
|
||||
expect(assertSelectedElements(h.elements[0]));
|
||||
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
|
||||
@@ -1229,7 +1228,7 @@ describe("history", () => {
|
||||
]);
|
||||
|
||||
Keyboard.redo();
|
||||
expect(API.getUndoStack().length).toBe(6);
|
||||
expect(API.getUndoStack().length).toBe(5);
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
expect(assertSelectedElements(h.elements[0]));
|
||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
||||
@@ -1581,21 +1580,24 @@ describe("history", () => {
|
||||
|
||||
// bind arrow to rect1 and rect2
|
||||
UI.clickTool("arrow");
|
||||
mouse.down(0, 0);
|
||||
mouse.up(100, 0);
|
||||
mouse.down(3, 0);
|
||||
mouse.moveTo(50, 0);
|
||||
mouse.up(47, 0);
|
||||
|
||||
arrow = h.elements[3] as ExcalidrawLinearElement;
|
||||
|
||||
expect(API.getUndoStack().length).toBe(5);
|
||||
expect(arrow.startBinding).toEqual({
|
||||
elementId: rect1.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([
|
||||
0.5379561888991137, 0.5379561888991137,
|
||||
]),
|
||||
mode: "orbit",
|
||||
});
|
||||
expect(arrow.endBinding).toEqual({
|
||||
elementId: rect2.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
|
||||
mode: "orbit",
|
||||
});
|
||||
expect(rect1.boundElements).toStrictEqual([
|
||||
{ id: text.id, type: "text" },
|
||||
@@ -1612,13 +1614,15 @@ describe("history", () => {
|
||||
expect(API.getRedoStack().length).toBe(1);
|
||||
expect(arrow.startBinding).toEqual({
|
||||
elementId: rect1.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([
|
||||
0.5379561888991137, 0.5379561888991137,
|
||||
]),
|
||||
mode: "orbit",
|
||||
});
|
||||
expect(arrow.endBinding).toEqual({
|
||||
elementId: rect2.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
|
||||
mode: "orbit",
|
||||
});
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
@@ -1635,13 +1639,15 @@ describe("history", () => {
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
expect(arrow.startBinding).toEqual({
|
||||
elementId: rect1.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([
|
||||
0.5379561888991137, 0.5379561888991137,
|
||||
]),
|
||||
mode: "orbit",
|
||||
});
|
||||
expect(arrow.endBinding).toEqual({
|
||||
elementId: rect2.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
|
||||
mode: "orbit",
|
||||
});
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
@@ -1666,13 +1672,15 @@ describe("history", () => {
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
expect(arrow.startBinding).toEqual({
|
||||
elementId: rect1.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([
|
||||
0.5379561888991137, 0.5379561888991137,
|
||||
]),
|
||||
mode: "orbit",
|
||||
});
|
||||
expect(arrow.endBinding).toEqual({
|
||||
elementId: rect2.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
|
||||
mode: "orbit",
|
||||
});
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
@@ -1689,13 +1697,15 @@ describe("history", () => {
|
||||
expect(API.getRedoStack().length).toBe(1);
|
||||
expect(arrow.startBinding).toEqual({
|
||||
elementId: rect1.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([
|
||||
0.5379561888991137, 0.5379561888991137,
|
||||
]),
|
||||
mode: "orbit",
|
||||
});
|
||||
expect(arrow.endBinding).toEqual({
|
||||
elementId: rect2.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
|
||||
mode: "orbit",
|
||||
});
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
@@ -1744,13 +1754,19 @@ describe("history", () => {
|
||||
id: arrow.id,
|
||||
startBinding: expect.objectContaining({
|
||||
elementId: rect1.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
]),
|
||||
mode: "orbit",
|
||||
}),
|
||||
endBinding: expect.objectContaining({
|
||||
elementId: rect2.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
]),
|
||||
mode: "orbit",
|
||||
}),
|
||||
isDeleted: true,
|
||||
}),
|
||||
@@ -1789,13 +1805,19 @@ describe("history", () => {
|
||||
id: arrow.id,
|
||||
startBinding: expect.objectContaining({
|
||||
elementId: rect1.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
]),
|
||||
mode: "orbit",
|
||||
}),
|
||||
endBinding: expect.objectContaining({
|
||||
elementId: rect2.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
]),
|
||||
mode: "orbit",
|
||||
}),
|
||||
isDeleted: false,
|
||||
}),
|
||||
@@ -1833,8 +1855,11 @@ describe("history", () => {
|
||||
startBinding: null,
|
||||
endBinding: expect.objectContaining({
|
||||
elementId: rect2.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
]),
|
||||
mode: "orbit",
|
||||
}),
|
||||
isDeleted: false,
|
||||
}),
|
||||
@@ -1868,13 +1893,19 @@ describe("history", () => {
|
||||
id: arrow.id,
|
||||
startBinding: expect.objectContaining({
|
||||
elementId: rect1.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
]),
|
||||
mode: "orbit",
|
||||
}),
|
||||
endBinding: expect.objectContaining({
|
||||
elementId: rect2.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
]),
|
||||
mode: "orbit",
|
||||
}),
|
||||
isDeleted: false,
|
||||
}),
|
||||
@@ -1941,13 +1972,19 @@ describe("history", () => {
|
||||
id: arrow.id,
|
||||
startBinding: expect.objectContaining({
|
||||
elementId: rect1.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
]),
|
||||
mode: "orbit",
|
||||
}),
|
||||
endBinding: expect.objectContaining({
|
||||
elementId: rect2.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
]),
|
||||
mode: "orbit",
|
||||
}),
|
||||
isDeleted: false,
|
||||
}),
|
||||
@@ -2298,15 +2335,13 @@ describe("history", () => {
|
||||
],
|
||||
startBinding: {
|
||||
elementId: "KPrBI4g_v9qUB1XxYLgSz",
|
||||
focus: -0.001587301587301948,
|
||||
gap: 5,
|
||||
fixedPoint: [1.0318471337579618, 0.49920634920634904],
|
||||
mode: "orbit",
|
||||
} as FixedPointBinding,
|
||||
endBinding: {
|
||||
elementId: "u2JGnnmoJ0VATV4vCNJE5",
|
||||
focus: -0.0016129032258049847,
|
||||
gap: 3.537079145500037,
|
||||
fixedPoint: [0.4991935483870975, -0.03875193720914723],
|
||||
mode: "orbit",
|
||||
} as FixedPointBinding,
|
||||
},
|
||||
],
|
||||
@@ -2421,10 +2456,9 @@ describe("history", () => {
|
||||
captureUpdate: CaptureUpdateAction.NEVER,
|
||||
});
|
||||
|
||||
Keyboard.undo(); // undo `actionFinalize`
|
||||
Keyboard.undo();
|
||||
expect(API.getUndoStack().length).toBe(1);
|
||||
expect(API.getRedoStack().length).toBe(2);
|
||||
expect(API.getRedoStack().length).toBe(1);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
points: [
|
||||
@@ -2438,7 +2472,7 @@ describe("history", () => {
|
||||
|
||||
Keyboard.undo();
|
||||
expect(API.getUndoStack().length).toBe(0);
|
||||
expect(API.getRedoStack().length).toBe(3);
|
||||
expect(API.getRedoStack().length).toBe(2);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
isDeleted: true,
|
||||
@@ -2451,7 +2485,7 @@ describe("history", () => {
|
||||
|
||||
Keyboard.redo();
|
||||
expect(API.getUndoStack().length).toBe(1);
|
||||
expect(API.getRedoStack().length).toBe(2);
|
||||
expect(API.getRedoStack().length).toBe(1);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
isDeleted: false,
|
||||
@@ -2464,21 +2498,6 @@ describe("history", () => {
|
||||
|
||||
Keyboard.redo();
|
||||
expect(API.getUndoStack().length).toBe(2);
|
||||
expect(API.getRedoStack().length).toBe(1);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
points: [
|
||||
[0, 0],
|
||||
[5, 5],
|
||||
[10, 10],
|
||||
[15, 15],
|
||||
[20, 20],
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
Keyboard.redo(); // redo `actionFinalize`
|
||||
expect(API.getUndoStack().length).toBe(3);
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
@@ -2978,7 +2997,7 @@ describe("history", () => {
|
||||
// leave editor
|
||||
Keyboard.keyPress(KEYS.ESCAPE);
|
||||
|
||||
expect(API.getUndoStack().length).toBe(4);
|
||||
expect(API.getUndoStack().length).toBe(3);
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
expect(h.state.selectedLinearElement).not.toBeNull();
|
||||
expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
||||
@@ -2995,11 +3014,11 @@ describe("history", () => {
|
||||
|
||||
Keyboard.undo();
|
||||
expect(API.getUndoStack().length).toBe(0);
|
||||
expect(API.getRedoStack().length).toBe(4);
|
||||
expect(API.getRedoStack().length).toBe(3);
|
||||
expect(h.state.selectedLinearElement).toBeNull();
|
||||
|
||||
Keyboard.redo();
|
||||
expect(API.getUndoStack().length).toBe(4);
|
||||
expect(API.getUndoStack().length).toBe(3);
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
expect(h.state.selectedLinearElement).toBeNull();
|
||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
||||
@@ -4500,16 +4519,30 @@ describe("history", () => {
|
||||
|
||||
// create start binding
|
||||
mouse.downAt(0, 0);
|
||||
mouse.moveTo(0, 1);
|
||||
mouse.moveTo(0, 0);
|
||||
mouse.moveTo(0, 10);
|
||||
mouse.moveTo(0, 10);
|
||||
mouse.up();
|
||||
|
||||
// create end binding
|
||||
mouse.downAt(100, 0);
|
||||
mouse.moveTo(100, 1);
|
||||
mouse.moveTo(100, 0);
|
||||
mouse.moveTo(100, 10);
|
||||
mouse.moveTo(100, 10);
|
||||
mouse.up();
|
||||
|
||||
expect(
|
||||
(h.elements[2] as ExcalidrawElbowArrowElement).startBinding
|
||||
?.fixedPoint,
|
||||
).not.toEqual([1, 0.5001]);
|
||||
expect(
|
||||
(h.elements[2] as ExcalidrawElbowArrowElement).startBinding?.mode,
|
||||
).toBe("orbit");
|
||||
expect(
|
||||
(h.elements[2] as ExcalidrawElbowArrowElement).endBinding,
|
||||
).not.toEqual([1, 0.5001]);
|
||||
expect(
|
||||
(h.elements[2] as ExcalidrawElbowArrowElement).endBinding?.mode,
|
||||
).toBe("orbit");
|
||||
|
||||
expect(h.elements).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
@@ -4524,13 +4557,19 @@ describe("history", () => {
|
||||
id: arrowId,
|
||||
startBinding: expect.objectContaining({
|
||||
elementId: rect1.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
]),
|
||||
mode: "orbit",
|
||||
}),
|
||||
endBinding: expect.objectContaining({
|
||||
elementId: rect2.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
]),
|
||||
mode: "orbit",
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
@@ -4593,13 +4632,13 @@ describe("history", () => {
|
||||
id: arrowId,
|
||||
startBinding: expect.objectContaining({
|
||||
elementId: rect1.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: [0.6363636363636364, 0.6363636363636364],
|
||||
mode: "orbit",
|
||||
}),
|
||||
endBinding: expect.objectContaining({
|
||||
elementId: rect2.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: [0.41019091151895054, 0.5898090884810495],
|
||||
mode: "orbit",
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
@@ -4636,13 +4675,13 @@ describe("history", () => {
|
||||
|
||||
// create start binding
|
||||
mouse.downAt(0, 0);
|
||||
mouse.moveTo(0, 1);
|
||||
mouse.upAt(0, 0);
|
||||
mouse.moveTo(0, 10);
|
||||
mouse.upAt(0, 10);
|
||||
|
||||
// create end binding
|
||||
mouse.downAt(100, 0);
|
||||
mouse.moveTo(100, 1);
|
||||
mouse.upAt(100, 0);
|
||||
mouse.moveTo(100, 10);
|
||||
mouse.upAt(100, 10);
|
||||
|
||||
expect(h.elements).toEqual(
|
||||
expect.arrayContaining([
|
||||
@@ -4658,13 +4697,19 @@ describe("history", () => {
|
||||
id: arrowId,
|
||||
startBinding: expect.objectContaining({
|
||||
elementId: rect1.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
]),
|
||||
mode: "orbit",
|
||||
}),
|
||||
endBinding: expect.objectContaining({
|
||||
elementId: rect2.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
]),
|
||||
mode: "orbit",
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
@@ -4702,9 +4747,8 @@ describe("history", () => {
|
||||
newElementWith(h.elements[2] as ExcalidrawElbowArrowElement, {
|
||||
endBinding: {
|
||||
elementId: remoteContainer.id,
|
||||
gap: 1,
|
||||
focus: 0,
|
||||
fixedPoint: [0.5, 1],
|
||||
fixedPoint: [0.5001, 1],
|
||||
mode: "orbit",
|
||||
},
|
||||
}),
|
||||
remoteContainer,
|
||||
@@ -4731,14 +4775,14 @@ describe("history", () => {
|
||||
id: arrowId,
|
||||
startBinding: expect.objectContaining({
|
||||
elementId: rect1.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: [0.6363636363636364, 0.6363636363636364],
|
||||
mode: "orbit",
|
||||
}),
|
||||
// rebound with previous rectangle
|
||||
endBinding: expect.objectContaining({
|
||||
elementId: rect2.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: [0.39511653718091, 0.6048834628190899],
|
||||
mode: "orbit",
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
@@ -4768,12 +4812,8 @@ describe("history", () => {
|
||||
endBinding: expect.objectContaining({
|
||||
// now we are back in the previous state!
|
||||
elementId: remoteContainer.id,
|
||||
fixedPoint: [
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
],
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: [0.5001, 1],
|
||||
mode: "orbit",
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
@@ -4791,15 +4831,13 @@ describe("history", () => {
|
||||
type: "arrow",
|
||||
startBinding: {
|
||||
elementId: rect1.id,
|
||||
gap: 1,
|
||||
focus: 0,
|
||||
fixedPoint: [1, 0.5],
|
||||
mode: "orbit",
|
||||
},
|
||||
endBinding: {
|
||||
elementId: rect2.id,
|
||||
gap: 1,
|
||||
focus: 0,
|
||||
fixedPoint: [0.5, 1],
|
||||
mode: "orbit",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4853,8 +4891,7 @@ describe("history", () => {
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
],
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
mode: "orbit",
|
||||
}),
|
||||
endBinding: expect.objectContaining({
|
||||
// now we are back in the previous state!
|
||||
@@ -4863,8 +4900,7 @@ describe("history", () => {
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
],
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
mode: "orbit",
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
@@ -4900,15 +4936,13 @@ describe("history", () => {
|
||||
newElementWith(h.elements[0] as ExcalidrawElbowArrowElement, {
|
||||
startBinding: {
|
||||
elementId: rect1.id,
|
||||
gap: 1,
|
||||
focus: 0,
|
||||
fixedPoint: [0.5, 1],
|
||||
mode: "orbit",
|
||||
},
|
||||
endBinding: {
|
||||
elementId: rect2.id,
|
||||
gap: 1,
|
||||
focus: 0,
|
||||
fixedPoint: [1, 0.5],
|
||||
mode: "orbit",
|
||||
},
|
||||
}),
|
||||
newElementWith(rect1, {
|
||||
@@ -4935,8 +4969,7 @@ describe("history", () => {
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
],
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
mode: "orbit",
|
||||
}),
|
||||
endBinding: expect.objectContaining({
|
||||
elementId: rect2.id,
|
||||
@@ -4944,8 +4977,7 @@ describe("history", () => {
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
],
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
mode: "orbit",
|
||||
}),
|
||||
isDeleted: true,
|
||||
}),
|
||||
@@ -4975,8 +5007,7 @@ describe("history", () => {
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
],
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
mode: "orbit",
|
||||
},
|
||||
endBinding: expect.objectContaining({
|
||||
elementId: rect2.id,
|
||||
@@ -4984,8 +5015,7 @@ describe("history", () => {
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
],
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
mode: "orbit",
|
||||
}),
|
||||
isDeleted: false,
|
||||
}),
|
||||
@@ -5010,11 +5040,13 @@ describe("history", () => {
|
||||
// bind arrow to rect1 and rect2
|
||||
UI.clickTool("arrow");
|
||||
mouse.down(0, 0);
|
||||
mouse.up(100, 0);
|
||||
mouse.moveTo(50, 0);
|
||||
mouse.up(50, 0);
|
||||
|
||||
const arrowId = h.elements[2].id;
|
||||
|
||||
Keyboard.undo();
|
||||
|
||||
expect(API.getUndoStack().length).toBe(1);
|
||||
expect(API.getRedoStack().length).toBe(1);
|
||||
expect(h.elements).toEqual(
|
||||
@@ -5028,13 +5060,13 @@ describe("history", () => {
|
||||
id: arrowId,
|
||||
startBinding: expect.objectContaining({
|
||||
elementId: rect1.id,
|
||||
focus: 0,
|
||||
gap: 1,
|
||||
fixedPoint: expect.arrayContaining([
|
||||
0.548442798411514, 0.548442798411514,
|
||||
]),
|
||||
}),
|
||||
endBinding: expect.objectContaining({
|
||||
elementId: rect2.id,
|
||||
focus: -0,
|
||||
gap: 1,
|
||||
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
|
||||
}),
|
||||
isDeleted: true,
|
||||
}),
|
||||
@@ -5076,13 +5108,19 @@ describe("history", () => {
|
||||
id: arrowId,
|
||||
startBinding: expect.objectContaining({
|
||||
elementId: rect1.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
]),
|
||||
mode: "orbit",
|
||||
}),
|
||||
endBinding: expect.objectContaining({
|
||||
elementId: rect2.id,
|
||||
focus: expect.toBeNonNaNNumber(),
|
||||
gap: expect.toBeNonNaNNumber(),
|
||||
fixedPoint: expect.arrayContaining([
|
||||
expect.toBeNonNaNNumber(),
|
||||
expect.toBeNonNaNNumber(),
|
||||
]),
|
||||
mode: "orbit",
|
||||
}),
|
||||
isDeleted: false,
|
||||
}),
|
||||
|
||||
@@ -210,7 +210,6 @@ describe("Basic lasso selection tests", () => {
|
||||
[0, 0],
|
||||
[168.4765625, -153.38671875],
|
||||
],
|
||||
lastCommittedPoint: null,
|
||||
startBinding: null,
|
||||
endBinding: null,
|
||||
startArrowhead: null,
|
||||
@@ -250,7 +249,6 @@ describe("Basic lasso selection tests", () => {
|
||||
[0, 0],
|
||||
[206.12890625, 35.4140625],
|
||||
],
|
||||
lastCommittedPoint: null,
|
||||
startBinding: null,
|
||||
endBinding: null,
|
||||
startArrowhead: null,
|
||||
@@ -354,7 +352,6 @@ describe("Basic lasso selection tests", () => {
|
||||
],
|
||||
pressures: [],
|
||||
simulatePressure: true,
|
||||
lastCommittedPoint: null,
|
||||
},
|
||||
].map(
|
||||
(e) =>
|
||||
@@ -1229,7 +1226,6 @@ describe("Special cases", () => {
|
||||
locked: false,
|
||||
startBinding: null,
|
||||
endBinding: null,
|
||||
lastCommittedPoint: null,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
points: [
|
||||
@@ -1271,7 +1267,6 @@ describe("Special cases", () => {
|
||||
locked: false,
|
||||
startBinding: null,
|
||||
endBinding: null,
|
||||
lastCommittedPoint: null,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
points: [
|
||||
@@ -1312,7 +1307,6 @@ describe("Special cases", () => {
|
||||
locked: false,
|
||||
startBinding: null,
|
||||
endBinding: null,
|
||||
lastCommittedPoint: null,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
points: [
|
||||
@@ -1353,7 +1347,6 @@ describe("Special cases", () => {
|
||||
locked: false,
|
||||
startBinding: null,
|
||||
endBinding: null,
|
||||
lastCommittedPoint: null,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
points: [
|
||||
@@ -1692,7 +1685,6 @@ describe("Special cases", () => {
|
||||
locked: false,
|
||||
startBinding: null,
|
||||
endBinding: null,
|
||||
lastCommittedPoint: null,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
points: [
|
||||
@@ -1744,7 +1736,6 @@ describe("Special cases", () => {
|
||||
locked: false,
|
||||
startBinding: null,
|
||||
endBinding: null,
|
||||
lastCommittedPoint: null,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
points: [
|
||||
|
||||
@@ -67,9 +67,8 @@ describe("library items inserting", () => {
|
||||
type: "arrow",
|
||||
endBinding: {
|
||||
elementId: "rectangle1",
|
||||
focus: -1,
|
||||
gap: 0,
|
||||
fixedPoint: [0.5, 1],
|
||||
mode: "orbit",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import React from "react";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import { bindOrUnbindLinearElement } from "@excalidraw/element";
|
||||
|
||||
import { KEYS, reseed } from "@excalidraw/common";
|
||||
|
||||
import { bindBindingElement } from "@excalidraw/element";
|
||||
import "@excalidraw/utils/test-utils";
|
||||
|
||||
import type {
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawArrowElement,
|
||||
NonDeleted,
|
||||
ExcalidrawRectangleElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import { Excalidraw } from "../index";
|
||||
@@ -83,12 +79,21 @@ describe("move element", () => {
|
||||
const rectA = UI.createElement("rectangle", { size: 100 });
|
||||
const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 });
|
||||
const arrow = UI.createElement("arrow", { x: 110, y: 50, size: 80 });
|
||||
|
||||
act(() => {
|
||||
// bind line to two rectangles
|
||||
bindOrUnbindLinearElement(
|
||||
arrow.get() as NonDeleted<ExcalidrawLinearElement>,
|
||||
rectA.get() as ExcalidrawRectangleElement,
|
||||
rectB.get() as ExcalidrawRectangleElement,
|
||||
bindBindingElement(
|
||||
arrow.get() as NonDeleted<ExcalidrawArrowElement>,
|
||||
rectA.get(),
|
||||
"orbit",
|
||||
"start",
|
||||
h.app.scene,
|
||||
);
|
||||
bindBindingElement(
|
||||
arrow.get() as NonDeleted<ExcalidrawArrowElement>,
|
||||
rectB.get(),
|
||||
"orbit",
|
||||
"end",
|
||||
h.app.scene,
|
||||
);
|
||||
});
|
||||
@@ -97,16 +102,19 @@ describe("move element", () => {
|
||||
new Pointer("mouse").clickOn(rectB);
|
||||
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`17`,
|
||||
`16`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`13`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`15`);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(3);
|
||||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||
expect([rectA.x, rectA.y]).toEqual([0, 0]);
|
||||
expect([rectB.x, rectB.y]).toEqual([200, 0]);
|
||||
expect([arrow.x, arrow.y]).toEqual([110, 50]);
|
||||
expect([arrow.width, arrow.height]).toEqual([80, 80]);
|
||||
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[110, -4.576537]], 0);
|
||||
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints(
|
||||
[[79, 132.89433]],
|
||||
0,
|
||||
);
|
||||
|
||||
renderInteractiveScene.mockClear();
|
||||
renderStaticScene.mockClear();
|
||||
@@ -124,8 +132,11 @@ describe("move element", () => {
|
||||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||
expect([rectA.x, rectA.y]).toEqual([0, 0]);
|
||||
expect([rectB.x, rectB.y]).toEqual([201, 2]);
|
||||
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[110, 50]]);
|
||||
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[81, 81.4]]);
|
||||
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[111, 6.1499]], 0);
|
||||
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints(
|
||||
[[79, 124.1678]],
|
||||
0,
|
||||
);
|
||||
|
||||
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||
});
|
||||
|
||||
@@ -118,8 +118,10 @@ describe("multi point mode in linear elements", () => {
|
||||
key: KEYS.ENTER,
|
||||
});
|
||||
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`11`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
expect(h.elements.length).toEqual(1);
|
||||
|
||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||
@@ -161,8 +163,10 @@ describe("multi point mode in linear elements", () => {
|
||||
fireEvent.keyDown(document, {
|
||||
key: KEYS.ENTER,
|
||||
});
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`11`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
expect(h.elements.length).toEqual(1);
|
||||
|
||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||
|
||||
@@ -369,7 +369,6 @@ describe("regression tests", () => {
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
Keyboard.keyPress(KEYS.Z);
|
||||
Keyboard.keyPress(KEYS.Z);
|
||||
Keyboard.keyPress(KEYS.Z);
|
||||
});
|
||||
expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2);
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
|
||||
@@ -24,7 +24,7 @@ test("unselected bound arrow updates when rotating its target element", async ()
|
||||
const arrow = UI.createElement("arrow", {
|
||||
x: -80,
|
||||
y: 50,
|
||||
width: 70,
|
||||
width: 85,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
@@ -35,8 +35,8 @@ test("unselected bound arrow updates when rotating its target element", async ()
|
||||
expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
||||
expect(arrow.x).toBeCloseTo(-80);
|
||||
expect(arrow.y).toBeCloseTo(50);
|
||||
expect(arrow.width).toBeCloseTo(110.7, 1);
|
||||
expect(arrow.height).toBeCloseTo(0);
|
||||
expect(arrow.width).toBeCloseTo(132.491, 1);
|
||||
expect(arrow.height).toBeCloseTo(82.267, 1);
|
||||
});
|
||||
|
||||
test("unselected bound arrows update when rotating their target elements", async () => {
|
||||
@@ -48,9 +48,10 @@ test("unselected bound arrows update when rotating their target elements", async
|
||||
height: 120,
|
||||
});
|
||||
const ellipseArrow = UI.createElement("arrow", {
|
||||
position: 0,
|
||||
width: 40,
|
||||
height: 80,
|
||||
x: -10,
|
||||
y: 80,
|
||||
width: 50,
|
||||
height: 60,
|
||||
});
|
||||
const text = UI.createElement("text", {
|
||||
position: 220,
|
||||
@@ -59,8 +60,8 @@ test("unselected bound arrows update when rotating their target elements", async
|
||||
const textArrow = UI.createElement("arrow", {
|
||||
x: 360,
|
||||
y: 300,
|
||||
width: -100,
|
||||
height: -40,
|
||||
width: -140,
|
||||
height: -60,
|
||||
});
|
||||
|
||||
expect(ellipseArrow.endBinding?.elementId).toEqual(ellipse.id);
|
||||
@@ -69,16 +70,16 @@ test("unselected bound arrows update when rotating their target elements", async
|
||||
UI.rotate([ellipse, text], [-82, 23], { shift: true });
|
||||
|
||||
expect(ellipseArrow.endBinding?.elementId).toEqual(ellipse.id);
|
||||
expect(ellipseArrow.x).toEqual(0);
|
||||
expect(ellipseArrow.y).toEqual(0);
|
||||
expect(ellipseArrow.x).toEqual(-10);
|
||||
expect(ellipseArrow.y).toEqual(80);
|
||||
expect(ellipseArrow.points[0]).toEqual([0, 0]);
|
||||
expect(ellipseArrow.points[1][0]).toBeCloseTo(48.98, 1);
|
||||
expect(ellipseArrow.points[1][1]).toBeCloseTo(125.79, 1);
|
||||
expect(ellipseArrow.points[1][0]).toBeCloseTo(66.317, 1);
|
||||
expect(ellipseArrow.points[1][1]).toBeCloseTo(144.38, 1);
|
||||
|
||||
expect(textArrow.endBinding?.elementId).toEqual(text.id);
|
||||
expect(textArrow.x).toEqual(360);
|
||||
expect(textArrow.y).toEqual(300);
|
||||
expect(textArrow.points[0]).toEqual([0, 0]);
|
||||
expect(textArrow.points[1][0]).toBeCloseTo(-94, 0);
|
||||
expect(textArrow.points[1][1]).toBeCloseTo(-116.1, 0);
|
||||
expect(textArrow.points[1][0]).toBeCloseTo(-95.74, 0);
|
||||
expect(textArrow.points[1][1]).toBeCloseTo(-119.7354, 0);
|
||||
});
|
||||
|
||||
@@ -425,8 +425,8 @@ describe("select single element on the scene", () => {
|
||||
fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(8);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||
@@ -469,8 +469,8 @@ describe("select single element on the scene", () => {
|
||||
fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(8);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||
@@ -487,7 +487,12 @@ describe("tool locking & selection", () => {
|
||||
expect(h.state.activeTool.locked).toBe(true);
|
||||
|
||||
for (const { value } of Object.values(SHAPES)) {
|
||||
if (value !== "image" && value !== "selection" && value !== "eraser") {
|
||||
if (
|
||||
value !== "image" &&
|
||||
value !== "selection" &&
|
||||
value !== "eraser" &&
|
||||
value !== "arrow"
|
||||
) {
|
||||
const element = UI.createElement(value);
|
||||
expect(h.state.selectedElementIds[element.id]).not.toBe(true);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import type {
|
||||
EditorInterface,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import type { SuggestedBinding } from "@excalidraw/element";
|
||||
|
||||
import type { LinearElementEditor } from "@excalidraw/element";
|
||||
|
||||
import type { MaybeTransformHandleType } from "@excalidraw/element";
|
||||
@@ -34,6 +32,7 @@ import type {
|
||||
ExcalidrawIframeLikeElement,
|
||||
OrderedExcalidrawElement,
|
||||
ExcalidrawNonSelectionElement,
|
||||
BindMode,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type {
|
||||
@@ -205,6 +204,7 @@ export type StaticCanvasAppState = Readonly<
|
||||
frameRendering: AppState["frameRendering"];
|
||||
currentHoveredFontFamily: AppState["currentHoveredFontFamily"];
|
||||
hoveredElementIds: AppState["hoveredElementIds"];
|
||||
suggestedBinding: AppState["suggestedBinding"];
|
||||
// Cropping
|
||||
croppingElementId: AppState["croppingElementId"];
|
||||
}
|
||||
@@ -218,8 +218,9 @@ export type InteractiveCanvasAppState = Readonly<
|
||||
selectedGroupIds: AppState["selectedGroupIds"];
|
||||
selectedLinearElement: AppState["selectedLinearElement"];
|
||||
multiElement: AppState["multiElement"];
|
||||
newElement: AppState["newElement"];
|
||||
isBindingEnabled: AppState["isBindingEnabled"];
|
||||
suggestedBindings: AppState["suggestedBindings"];
|
||||
suggestedBinding: AppState["suggestedBinding"];
|
||||
isRotating: AppState["isRotating"];
|
||||
elementsToHighlight: AppState["elementsToHighlight"];
|
||||
// Collaborators
|
||||
@@ -234,6 +235,11 @@ export type InteractiveCanvasAppState = Readonly<
|
||||
// Search matches
|
||||
searchMatches: AppState["searchMatches"];
|
||||
activeLockedId: AppState["activeLockedId"];
|
||||
// Non-used but needed in binding highlight arrow overdraw
|
||||
hoveredElementIds: AppState["hoveredElementIds"];
|
||||
frameRendering: AppState["frameRendering"];
|
||||
shouldCacheIgnoreZoom: AppState["shouldCacheIgnoreZoom"];
|
||||
exportScale: AppState["exportScale"];
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -293,7 +299,7 @@ export interface AppState {
|
||||
selectionElement: NonDeletedExcalidrawElement | null;
|
||||
isBindingEnabled: boolean;
|
||||
startBoundElement: NonDeleted<ExcalidrawBindableElement> | null;
|
||||
suggestedBindings: SuggestedBinding[];
|
||||
suggestedBinding: NonDeleted<ExcalidrawBindableElement> | null;
|
||||
frameToHighlight: NonDeleted<ExcalidrawFrameLikeElement> | null;
|
||||
frameRendering: {
|
||||
enabled: boolean;
|
||||
@@ -368,6 +374,7 @@ export interface AppState {
|
||||
| { name: "imageExport" | "help" | "jsonExport" }
|
||||
| { name: "ttd"; tab: "text-to-diagram" | "mermaid" }
|
||||
| { name: "commandPalette" }
|
||||
| { name: "settings" }
|
||||
| { name: "elementLinkSelector"; sourceElementId: ExcalidrawElement["id"] };
|
||||
/**
|
||||
* Reflects user preference for whether the default sidebar should be docked.
|
||||
@@ -450,6 +457,7 @@ export interface AppState {
|
||||
// as elements are unlocked, we remove the groupId from the elements
|
||||
// and also remove groupId from this map
|
||||
lockedMultiSelections: { [groupId: string]: true };
|
||||
bindMode: BindMode;
|
||||
}
|
||||
|
||||
export type SearchMatch = {
|
||||
@@ -466,7 +474,7 @@ export type SearchMatch = {
|
||||
|
||||
export type UIAppState = Omit<
|
||||
AppState,
|
||||
| "suggestedBindings"
|
||||
| "suggestedBinding"
|
||||
| "startBoundElement"
|
||||
| "cursorButton"
|
||||
| "scrollX"
|
||||
@@ -750,6 +758,8 @@ export type AppClassProperties = {
|
||||
onPointerUpEmitter: App["onPointerUpEmitter"];
|
||||
updateEditorAtom: App["updateEditorAtom"];
|
||||
onPointerDownEmitter: App["onPointerDownEmitter"];
|
||||
|
||||
bindModeHandler: App["bindModeHandler"];
|
||||
};
|
||||
|
||||
export type PointerDownState = Readonly<{
|
||||
|
||||
@@ -6,11 +6,11 @@ expect.extend({
|
||||
throw new Error("expected and received are not point arrays");
|
||||
}
|
||||
|
||||
const COMPARE = 1 / Math.pow(10, precision || 2);
|
||||
const COMPARE = 1 / precision === 0 ? 1 : Math.pow(10, precision ?? 2);
|
||||
const pass = expected.every(
|
||||
(point, idx) =>
|
||||
Math.abs(received[idx]?.[0] - point[0]) < COMPARE &&
|
||||
Math.abs(received[idx]?.[1] - point[1]) < COMPARE,
|
||||
Math.abs(received[idx][0] - point[0]) < COMPARE &&
|
||||
Math.abs(received[idx][1] - point[1]) < COMPARE,
|
||||
);
|
||||
|
||||
if (!pass) {
|
||||
|
||||
@@ -11,6 +11,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
||||
"locked": false,
|
||||
"type": "selection",
|
||||
},
|
||||
"bindMode": "orbit",
|
||||
"collaborators": Map {},
|
||||
"contextMenu": null,
|
||||
"croppingElementId": null,
|
||||
@@ -104,7 +105,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
||||
"open": false,
|
||||
"panels": 3,
|
||||
},
|
||||
"suggestedBindings": [],
|
||||
"suggestedBinding": null,
|
||||
"theme": "light",
|
||||
"toast": null,
|
||||
"userToFollow": null,
|
||||
|
||||
Reference in New Issue
Block a user