No impure getters pt10 (#2235)

Follow up to #2189 

### Change Type

- [x] `patch` — Bug fix
This commit is contained in:
David Sheldrick 2023-11-16 12:07:33 +00:00 committed by GitHub
parent 81f6fae070
commit 431ce73476
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 751 additions and 171 deletions

View file

@ -44,6 +44,11 @@ module.exports = {
],
'local/no-export-star': 'error',
'no-only-tests/no-only-tests': 'error',
'no-restricted-syntax': [
'error',
{ selector: "MethodDefinition[kind='set']", message: 'Property setters are not allowed' },
{ selector: "MethodDefinition[kind='get']", message: 'Property getters are not allowed' },
],
},
parser: '@typescript-eslint/parser',
parserOptions: {

View file

@ -1803,6 +1803,8 @@ export class SnapManager {
// (undocumented)
getCurrentCommonAncestor(): TLShapeId | undefined;
// (undocumented)
getLines(): SnapLine[];
// (undocumented)
getOutlinesInPageSpace(): Vec2d[][];
// (undocumented)
getSnappablePoints(): SnapPoint[];
@ -1822,7 +1824,7 @@ export class SnapManager {
horizontal: Gap[];
vertical: Gap[];
};
// (undocumented)
// @deprecated (undocumented)
get lines(): SnapLine[];
// @deprecated (undocumented)
get outlinesInPageSpace(): Vec2d[][];
@ -1897,7 +1899,7 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
static children?: () => TLStateNodeConstructor[];
// (undocumented)
children?: Record<string, StateNode>;
// (undocumented)
// @deprecated (undocumented)
get currentToolIdMask(): string | undefined;
set currentToolIdMask(id: string | undefined);
_currentToolIdMask: Atom<string | undefined, unknown>;
@ -1908,6 +1910,8 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
// (undocumented)
exit: (info: any, from: string) => void;
getCurrent(): StateNode | undefined;
// (undocumented)
getCurrentToolIdMask(): string | undefined;
getIsActive(): boolean;
getPath(): string;
// (undocumented)
@ -1959,6 +1963,8 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
// (undocumented)
_path: Computed<string>;
// (undocumented)
setCurrentToolIdMask(id: string | undefined): void;
// (undocumented)
shapeType?: string;
transition: (id: string, info?: any) => this;
// (undocumented)

View file

@ -33889,6 +33889,42 @@
"isAbstract": false,
"name": "getCurrentCommonAncestor"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!SnapManager#getLines:member(1)",
"docComment": "",
"excerptTokens": [
{
"kind": "Content",
"text": "getLines(): "
},
{
"kind": "Reference",
"text": "SnapLine",
"canonicalReference": "@tldraw/editor!SnapLine:type"
},
{
"kind": "Content",
"text": "[]"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 1,
"endIndex": 3
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [],
"isOptional": false,
"isAbstract": false,
"name": "getLines"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!SnapManager#getOutlinesInPageSpace:member(1)",
@ -34209,7 +34245,7 @@
{
"kind": "Property",
"canonicalReference": "@tldraw/editor!SnapManager#lines:member",
"docComment": "",
"docComment": "/**\n * @deprecated\n *\n * use `getLines` instead\n */\n",
"excerptTokens": [
{
"kind": "Content",
@ -35318,7 +35354,7 @@
{
"kind": "Property",
"canonicalReference": "@tldraw/editor!StateNode#currentToolIdMask:member",
"docComment": "",
"docComment": "/**\n * @deprecated\n *\n * use `getCurrentToolIdMask()` instead\n */\n",
"excerptTokens": [
{
"kind": "Content",
@ -35472,6 +35508,37 @@
"isAbstract": false,
"name": "getCurrent"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!StateNode#getCurrentToolIdMask:member(1)",
"docComment": "",
"excerptTokens": [
{
"kind": "Content",
"text": "getCurrentToolIdMask(): "
},
{
"kind": "Content",
"text": "string | undefined"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [],
"isOptional": false,
"isAbstract": false,
"name": "getCurrentToolIdMask"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!StateNode#getIsActive:member(1)",
@ -36329,6 +36396,54 @@
"isProtected": false,
"isAbstract": false
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!StateNode#setCurrentToolIdMask:member(1)",
"docComment": "",
"excerptTokens": [
{
"kind": "Content",
"text": "setCurrentToolIdMask(id: "
},
{
"kind": "Content",
"text": "string | undefined"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "void"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [
{
"parameterName": "id",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
}
],
"isOptional": false,
"isAbstract": false,
"name": "setCurrentToolIdMask"
},
{
"kind": "Property",
"canonicalReference": "@tldraw/editor!StateNode#shapeType:member",

View file

@ -189,7 +189,7 @@ function ZoomBrushWrapper() {
function SnapLinesWrapper() {
const editor = useEditor()
const lines = useValue('snapLines', () => editor.snaps.lines, [editor])
const lines = useValue('snapLines', () => editor.snaps.getLines(), [editor])
const zoomLevel = useValue('zoomLevel', () => editor.getZoomLevel(), [editor])
const { SnapLine } = useEditorComponents()

View file

@ -799,12 +799,13 @@ export class Editor extends EventEmitter<TLEventMap> {
* @public
*/
@computed getCanUndo(): boolean {
return this.history.numUndos > 0
return this.history.getNumUndos() > 0
}
/**
* @deprecated Use `getCanUndo` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get canUndo(): boolean {
return this.getCanUndo()
}
@ -830,12 +831,13 @@ export class Editor extends EventEmitter<TLEventMap> {
* @public
*/
@computed getCanRedo(): boolean {
return this.history.numRedos > 0
return this.history.getNumRedos() > 0
}
/**
* @deprecated Use `getCanRedo` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get canRedo(): boolean {
return this.getCanRedo()
}
@ -1141,6 +1143,7 @@ export class Editor extends EventEmitter<TLEventMap> {
* @deprecated Use `getCurrentTool` instead.
* @public
*/
// eslint-disable-next-line no-restricted-syntax
get currentTool() {
return this.getCurrentTool()
}
@ -1153,12 +1156,13 @@ export class Editor extends EventEmitter<TLEventMap> {
@computed getCurrentToolId(): string {
const currentTool = this.getCurrentTool()
if (!currentTool) return ''
return currentTool.currentToolIdMask ?? currentTool.id
return currentTool.getCurrentToolIdMask() ?? currentTool.id
}
/**
* @deprecated Use `getCurrentToolId` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get currentToolId() {
return this.getCurrentToolId()
}
@ -1203,6 +1207,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getDocumentSettings` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get documentSettings() {
return this.getDocumentSettings()
}
@ -1231,6 +1236,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getInstanceState` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get instanceState() {
return this.getInstanceState()
}
@ -1315,6 +1321,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getOpenMenus` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get openMenus() {
return this.getOpenMenus()
}
@ -1374,6 +1381,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getIsMenuOpen` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get isMenuOpen() {
return this.getIsMenuOpen()
}
@ -1410,6 +1418,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getPageStates` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get pageStates() {
return this.getPageStates()
}
@ -1431,6 +1440,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getCurrentPageState` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get currentPageState() {
return this.getCurrentPageState()
}
@ -1496,6 +1506,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getSelectedShapeIds` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get selectedShapeIds() {
return this.getSelectedShapeIds()
}
@ -1514,6 +1525,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getSelectedShapes` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get selectedShapes() {
return this.getSelectedShapes()
}
@ -1690,6 +1702,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getOnlySelectedShape` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get onlySelectedShape() {
return this.getOnlySelectedShape()
}
@ -1713,6 +1726,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getSelectionPageBounds` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get selectionPageBounds() {
return this.getSelectionPageBounds()
}
@ -1743,6 +1757,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getSelectionRotation` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get selectionRotation() {
return this.getSelectionRotation()
}
@ -1790,6 +1805,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getSelectionRotatedPageBounds` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get selectionRotatedPageBounds() {
return this.getSelectionRotatedPageBounds()
}
@ -1808,6 +1824,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getFocusedGroupId` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get focusedGroupId() {
return this.getFocusedGroupId()
}
@ -1825,6 +1842,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getFocusedGroup` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get focusedGroup() {
return this.getFocusedGroup()
}
@ -1922,6 +1940,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getEditingShapeId` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get editingShapeId() {
return this.getEditingShapeId()
}
@ -1939,6 +1958,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getEditingShape` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get editingShape() {
return this.getEditingShape()
}
@ -1988,6 +2008,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getHoveredShapeId` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get hoveredShapeId() {
return this.getHoveredShapeId()
}
@ -2005,6 +2026,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getHoveredShape` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get hoveredShape() {
return this.getHoveredShape()
}
@ -2043,6 +2065,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getHintingShapeIds` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get hintingShapeIds() {
return this.getHintingShapeIds()
}
@ -2060,6 +2083,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getHintingShape` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get hintingShape() {
return this.getHintingShape()
}
@ -2101,6 +2125,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getErasingShapeIds` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get erasingShapeIds() {
return this.getErasingShapeIds()
}
@ -2118,6 +2143,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getErasingShapes` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get erasingShapes() {
return this.getErasingShapes()
}
@ -2174,6 +2200,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getCroppingShapeId` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get croppingShapeId() {
return this.getCroppingShapeId()
}
@ -2237,6 +2264,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getZoomLevel` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get zoomLevel() {
return this.getZoomLevel()
}
@ -2948,6 +2976,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getViewportScreenBounds` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get viewportScreenBounds() {
return this.getViewportScreenBounds()
}
@ -2964,6 +2993,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getViewportScreenCenter` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get viewportScreenCenter() {
return this.getViewportScreenCenter()
}
@ -2982,6 +3012,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getViewportPageBounds` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get viewportPageBounds() {
return this.getViewportPageBounds()
}
@ -2998,6 +3029,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getViewportPageCenter` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get viewportPageCenter() {
return this.getViewportPageCenter()
}
@ -3201,6 +3233,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getCameraState` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get cameraState() {
return this.getCameraState()
}
@ -3376,6 +3409,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getRenderingShapes` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get renderingShapes() {
return this.getRenderingShapes()
}
@ -3393,6 +3427,7 @@ export class Editor extends EventEmitter<TLEventMap> {
* @deprecated Use `getRenderingBounds` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get renderingBounds() {
return this.getRenderingBounds()
}
@ -3413,6 +3448,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getRenderingBoundsExpanded` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get renderingBoundsExpanded() {
return this.getRenderingBoundsExpanded()
}
@ -3473,6 +3509,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getPages` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get pages() {
return this.getPages()
}
@ -3482,6 +3519,7 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @public
*/
// eslint-disable-next-line no-restricted-syntax
get currentPage(): TLPage {
const page = this.getPage(this.currentPageId)!
return page
@ -3492,6 +3530,7 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @public
*/
// eslint-disable-next-line no-restricted-syntax
get currentPageId(): TLPageId {
return this.getInstanceState().currentPageId
}
@ -3521,6 +3560,7 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @public
*/
// eslint-disable-next-line no-restricted-syntax
get currentPageShapeIds() {
return this._currentPageShapeIds.get()
}
@ -3873,6 +3913,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getAssets` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get assets() {
return this.getAssets()
}
@ -4493,6 +4534,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getCurrentPageBounds` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get currentPageBounds() {
return this.getCurrentPageBounds()
}
@ -4809,6 +4851,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getCurrentPageShapes` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get currentPageShapes() {
return this.getCurrentPageShapes()
}
@ -4850,6 +4893,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getCurrentPageShapesSorted` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get currentPageShapesSorted() {
return this.getCurrentPageShapesSorted()
}
@ -4870,6 +4914,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `getCurrentPageRenderingShapesSorted` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get currentPageRenderingShapesSorted() {
return this.getCurrentPageRenderingShapesSorted()
}
@ -7566,6 +7611,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `editor.sharedStyles` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get sharedStyles() {
return this.getSharedStyles()
}
@ -7615,6 +7661,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/**
* @deprecated Use `editor.sharedOpacity` instead.
*/
// eslint-disable-next-line no-restricted-syntax
get sharedOpacity() {
return this.getSharedOpacity()
}

View file

@ -88,6 +88,7 @@ export class ClickManager {
*
* @public
*/
// eslint-disable-next-line no-restricted-syntax
get clickState() {
return this._clickState
}

View file

@ -207,7 +207,7 @@ describe(HistoryManager, () => {
expect(editor.getCount()).toBe(5)
expect(editor.history.numUndos).toBe(3)
expect(editor.history.getNumUndos()).toBe(3)
})
it('allows ephemeral commands that do not affect the stack', () => {
@ -250,11 +250,11 @@ describe(HistoryManager, () => {
it('does not allow new history entries to be pushed if a command invokes them while doing or undoing', () => {
editor.incrementTwice()
expect(editor.history.numUndos).toBe(1)
expect(editor.history.getNumUndos()).toBe(1)
expect(editor.getCount()).toBe(2)
editor.history.undo()
expect(editor.getCount()).toBe(0)
expect(editor.history.numUndos).toBe(0)
expect(editor.history.getNumUndos()).toBe(0)
})
it('does not allow new history entries to be pushed if a command invokes them while bailing', () => {
@ -263,13 +263,13 @@ describe(HistoryManager, () => {
editor.history.mark('2')
editor.incrementTwice()
editor.incrementTwice()
expect(editor.history.numUndos).toBe(5)
expect(editor.history.getNumUndos()).toBe(5)
expect(editor.getCount()).toBe(6)
editor.history.bail()
expect(editor.getCount()).toBe(2)
expect(editor.history.numUndos).toBe(2)
expect(editor.history.getNumUndos()).toBe(2)
editor.history.bailToMark('0')
expect(editor.history.numUndos).toBe(0)
expect(editor.history.getNumUndos()).toBe(0)
expect(editor.getCount()).toBe(0)
})

View file

@ -33,13 +33,27 @@ export class HistoryManager<
private _commands: Record<string, TLCommandHandler<any>> = {}
get numUndos() {
getNumUndos() {
return this._undos.get().length
}
/**
* @deprecated use `getNumUndos` instead
*/
// eslint-disable-next-line no-restricted-syntax
get numUndos() {
return this.getNumUndos()
}
get numRedos() {
getNumRedos() {
return this._redos.get().length
}
/**
* @deprecated use `getNumRedos` instead
*/
// eslint-disable-next-line no-restricted-syntax
get numRedos() {
return this.getNumRedos()
}
createCommand = <Name extends string, Constructor extends CommandFn<any>>(
name: Name,

View file

@ -73,7 +73,7 @@ type NearestSnap =
}
| {
// selection snaps to create a new gap of equal size to another gap
// on the opposide side of some shape
// on the opposite side of some shape
type: 'gap_duplicate'
gap: Gap
protrusionDirection: 'left' | 'right' | 'top' | 'bottom'
@ -212,12 +212,20 @@ function dedupeGapSnaps(snaps: Array<Extract<SnapLine, { type: 'gaps' }>>) {
export class SnapManager {
private _snapLines = atom<SnapLine[] | undefined>('snapLines', undefined)
get lines() {
getLines() {
return this._snapLines.get() ?? (EMPTY_ARRAY as SnapLine[])
}
/**
* @deprecated use `getLines` instead
*/
// eslint-disable-next-line no-restricted-syntax
get lines() {
return this.getLines()
}
clear() {
if (this.lines.length) {
if (this.getLines().length) {
this._snapLines.set(undefined)
}
}
@ -244,6 +252,7 @@ export class SnapManager {
/**
* @deprecated use `getSnapPointsCache` instead
*/
// eslint-disable-next-line no-restricted-syntax
get snapPointsCache() {
return this.getSnapPointsCache()
}
@ -255,6 +264,7 @@ export class SnapManager {
/**
* @deprecated use `getSnapThreshold` instead
*/
// eslint-disable-next-line no-restricted-syntax
get snapThreshold() {
return this.getSnapThreshold()
}
@ -302,6 +312,7 @@ export class SnapManager {
* @deprecated use `getSnappableShapes` instead
*/
// eslint-disable-next-line no-restricted-syntax
get snappableShapes() {
return this.getSnappableShapes()
}
@ -314,6 +325,7 @@ export class SnapManager {
/**
* @deprecated use `getCurrentCommonAncestor` instead
*/
// eslint-disable-next-line no-restricted-syntax
get currentCommonAncestor() {
return this.getCurrentCommonAncestor()
}
@ -337,6 +349,7 @@ export class SnapManager {
/**
* @deprecated use `getSnappablePoints` instead
*/
// eslint-disable-next-line no-restricted-syntax
get snappablePoints() {
return this.getSnappablePoints()
}
@ -441,6 +454,7 @@ export class SnapManager {
/**
* @deprecated use `getVisibleGaps` instead
*/
// eslint-disable-next-line no-restricted-syntax
get visibleGaps() {
return this.getVisibleGaps()
}
@ -551,6 +565,7 @@ export class SnapManager {
/**
* @deprecated use `getOutlinesInPageSpace` instead
*/
// eslint-disable-next-line no-restricted-syntax
get outlinesInPageSpace() {
return this.getOutlinesInPageSpace()
}

View file

@ -29,6 +29,7 @@ export class UserPreferencesManager {
/**
* @deprecated use `getUserPreferences` instead
*/
// eslint-disable-next-line no-restricted-syntax
get userPreferences() {
return this.getUserPreferences()
}
@ -43,6 +44,7 @@ export class UserPreferencesManager {
/**
* @deprecated use `getIsDarkMode` instead
*/
// eslint-disable-next-line no-restricted-syntax
get isDarkMode() {
return this.getIsDarkMode()
}
@ -54,6 +56,7 @@ export class UserPreferencesManager {
/**
* @deprecated use `getAnimationSpeed` instead
*/
// eslint-disable-next-line no-restricted-syntax
get animationSpeed() {
return this.getAnimationSpeed()
}
@ -65,6 +68,7 @@ export class UserPreferencesManager {
/**
* @deprecated use `getId` instead
*/
// eslint-disable-next-line no-restricted-syntax
get id() {
return this.getId()
}
@ -76,6 +80,7 @@ export class UserPreferencesManager {
/**
* @deprecated use `getName` instead
*/
// eslint-disable-next-line no-restricted-syntax
get name() {
return this.getName()
}
@ -87,6 +92,7 @@ export class UserPreferencesManager {
/**
* @deprecated use `getLocale` instead
*/
// eslint-disable-next-line no-restricted-syntax
get locale() {
return this.getLocale()
}
@ -98,6 +104,7 @@ export class UserPreferencesManager {
/**
* @deprecated use `getColor` instead
*/
// eslint-disable-next-line no-restricted-syntax
get color() {
return this.getColor()
}
@ -109,6 +116,7 @@ export class UserPreferencesManager {
/**
* @deprecated use `getIsSnapMode` instead
*/
// eslint-disable-next-line no-restricted-syntax
get isSnapMode() {
return this.getIsSnapMode()
}

View file

@ -75,7 +75,7 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
*
* @public
*/
@computed getPath() {
getPath() {
return this._path.get()
}
_path: Computed<string>
@ -85,7 +85,7 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
*
* @public
*/
@computed getCurrent() {
getCurrent() {
return this._current.get()
}
private _current: Atom<StateNode | undefined>
@ -95,7 +95,7 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
*
* @public
*/
@computed getIsActive() {
getIsActive() {
return this._isActive.get()
}
private _isActive: Atom<boolean>
@ -182,11 +182,23 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
*/
_currentToolIdMask = atom('curent tool id mask', undefined as string | undefined)
@computed get currentToolIdMask() {
/**
* @deprecated use `getCurrentToolIdMask()` instead
*/
// eslint-disable-next-line no-restricted-syntax
get currentToolIdMask() {
return this._currentToolIdMask.get()
}
// eslint-disable-next-line no-restricted-syntax
set currentToolIdMask(id: string | undefined) {
this._currentToolIdMask.set(id)
}
getCurrentToolIdMask() {
return this._currentToolIdMask.get()
}
set currentToolIdMask(id: string | undefined) {
setCurrentToolIdMask(id: string | undefined) {
this._currentToolIdMask.set(id)
}

View file

@ -36,76 +36,94 @@ export class Box2d {
w = 0
h = 0
// eslint-disable-next-line no-restricted-syntax
get point() {
return new Vec2d(this.x, this.y)
}
// eslint-disable-next-line no-restricted-syntax
set point(val: Vec2d) {
this.x = val.x
this.y = val.y
}
// eslint-disable-next-line no-restricted-syntax
get minX() {
return this.x
}
// eslint-disable-next-line no-restricted-syntax
set minX(n: number) {
this.x = n
}
// eslint-disable-next-line no-restricted-syntax
get midX() {
return this.x + this.w / 2
}
// eslint-disable-next-line no-restricted-syntax
get maxX() {
return this.x + this.w
}
// eslint-disable-next-line no-restricted-syntax
get minY() {
return this.y
}
// eslint-disable-next-line no-restricted-syntax
set minY(n: number) {
this.y = n
}
// eslint-disable-next-line no-restricted-syntax
get midY() {
return this.y + this.h / 2
}
// eslint-disable-next-line no-restricted-syntax
get maxY() {
return this.y + this.h
}
// eslint-disable-next-line no-restricted-syntax
get width() {
return this.w
}
// eslint-disable-next-line no-restricted-syntax
set width(n: number) {
this.w = n
}
// eslint-disable-next-line no-restricted-syntax
get height() {
return this.h
}
// eslint-disable-next-line no-restricted-syntax
set height(n: number) {
this.h = n
}
// eslint-disable-next-line no-restricted-syntax
get aspectRatio() {
return this.width / this.height
}
// eslint-disable-next-line no-restricted-syntax
get center() {
return new Vec2d(this.midX, this.midY)
}
// eslint-disable-next-line no-restricted-syntax
set center(v: Vec2d) {
this.minX = v.x - this.width / 2
this.minY = v.y - this.height / 2
}
// eslint-disable-next-line no-restricted-syntax
get corners() {
return [
new Vec2d(this.minX, this.minY),
@ -115,6 +133,7 @@ export class Box2d {
]
}
// eslint-disable-next-line no-restricted-syntax
get snapPoints() {
return [
new Vec2d(this.minX, this.minY),
@ -125,6 +144,7 @@ export class Box2d {
]
}
// eslint-disable-next-line no-restricted-syntax
get sides(): Array<[Vec2d, Vec2d]> {
const { corners } = this
return [
@ -135,6 +155,7 @@ export class Box2d {
]
}
// eslint-disable-next-line no-restricted-syntax
get size(): Vec2d {
return new Vec2d(this.w, this.h)
}

View file

@ -8,6 +8,7 @@ export type VecLike = Vec2d | Vec2dModel
export class Vec2d {
constructor(public x = 0, public y = 0, public z = 1) {}
// eslint-disable-next-line no-restricted-syntax
get pressure() {
return this.z
}

View file

@ -15,6 +15,7 @@ export class CubicSpline2d extends Geometry2d {
_segments?: CubicBezier2d[]
// eslint-disable-next-line no-restricted-syntax
get segments() {
if (!this._segments) {
this._segments = []
@ -49,6 +50,7 @@ export class CubicSpline2d extends Geometry2d {
_length?: number
// eslint-disable-next-line no-restricted-syntax
get length() {
if (!this._length) {
this._length = this.segments.reduce((acc, segment) => acc + segment.length, 0)

View file

@ -24,6 +24,7 @@ export class Edge2d extends Geometry2d {
_length?: number
// eslint-disable-next-line no-restricted-syntax
get length() {
if (!this._length) {
return this.d.len()

View file

@ -24,6 +24,7 @@ export class Ellipse2d extends Geometry2d {
_edges?: Edge2d[]
// eslint-disable-next-line no-restricted-syntax
get edges() {
if (!this._edges) {
const { vertices } = this

View file

@ -78,6 +78,7 @@ export abstract class Geometry2d {
_vertices: Vec2d[] | undefined
// eslint-disable-next-line no-restricted-syntax
get vertices(): Vec2d[] {
if (!this._vertices) {
this._vertices = this.getVertices()
@ -92,6 +93,7 @@ export abstract class Geometry2d {
_bounds: Box2d | undefined
// eslint-disable-next-line no-restricted-syntax
get bounds(): Box2d {
if (!this._bounds) {
this._bounds = this.getBounds()
@ -101,6 +103,7 @@ export abstract class Geometry2d {
_snapPoints: Vec2d[] | undefined
// eslint-disable-next-line no-restricted-syntax
get snapPoints() {
if (!this._snapPoints) {
this._snapPoints = this.bounds.snapPoints
@ -108,12 +111,14 @@ export abstract class Geometry2d {
return this._snapPoints
}
// eslint-disable-next-line no-restricted-syntax
get center() {
return this.bounds.center
}
_area: number | undefined
// eslint-disable-next-line no-restricted-syntax
get area() {
if (!this._area) {
this._area = this.getArea()

View file

@ -14,6 +14,7 @@ export class Polyline2d extends Geometry2d {
_segments?: Edge2d[]
// eslint-disable-next-line no-restricted-syntax
get segments() {
if (!this._segments) {
this._segments = []
@ -34,6 +35,7 @@ export class Polyline2d extends Geometry2d {
_length?: number
// eslint-disable-next-line no-restricted-syntax
get length() {
if (!this._length) {
this._length = this.segments.reduce((acc, segment) => acc + segment.length, 0)

View file

@ -16,7 +16,7 @@ class C extends StateNode {
static override id = 'C'
override onEnter = () => {
this.currentToolIdMask = 'A'
this.setCurrentToolIdMask('A')
}
}

View file

@ -52,6 +52,7 @@ export class ReadonlySharedStyleMap {
return value.value
}
// eslint-disable-next-line no-restricted-syntax
get size() {
return this.map.size
}

View file

@ -596,7 +596,7 @@
{
"kind": "Function",
"canonicalReference": "@tldraw/state!computed:function(1)",
"docComment": "/**\n * Creates a computed signal.\n *\n * @param name - The name of the signal.\n *\n * @param compute - The function that computes the value of the signal.\n *\n * @param options - Options for the signal.\n *\n * @example\n * ```ts\n * const name = atom('name', 'John')\n * const greeting = computed('greeting', () => `Hello ${name.value}!`)\n * console.log(greeting.value) // 'Hello John!'\n * ```\n *\n * `computed` may also be used as a decorator for creating computed class properties.\n *\n * @example\n * ```ts\n * class Counter {\n * max = 100\n * count = atom<number>(0)\n *\n * @computed get remaining() {\n * return this.max - this.count.value\n * }\n * }\n * ```\n *\n * You may optionally pass in a [[ComputedOptions]] when used as a decorator:\n *\n * @example\n * ```ts\n * class Counter {\n * max = 100\n * count = atom<number>(0)\n *\n * @computed({isEqual: (a, b) => a === b})\n * get remaining() {\n * return this.max - this.count.value\n * }\n * }\n * ```\n *\n * @public\n */\n",
"docComment": "/**\n * Creates a computed signal.\n *\n * @param name - The name of the signal.\n *\n * @param compute - The function that computes the value of the signal.\n *\n * @param options - Options for the signal.\n *\n * @example\n * ```ts\n * const name = atom('name', 'John')\n * const greeting = computed('greeting', () => `Hello ${name.value}!`)\n * console.log(greeting.value) // 'Hello John!'\n * ```\n *\n * `computed` may also be used as a decorator for creating computed getter methods.\n *\n * @example\n * ```ts\n * class Counter {\n * max = 100\n * count = atom<number>(0)\n *\n * @computed getRemaining() {\n * return this.max - this.count.value\n * }\n * }\n * ```\n *\n * You may optionally pass in a [[ComputedOptions]] when used as a decorator:\n *\n * @example\n * ```ts\n * class Counter {\n * max = 100\n * count = atom<number>(0)\n *\n * @computed({isEqual: (a, b) => a === b})\n * getRemaining() {\n * return this.max - this.count.value\n * }\n * }\n * ```\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
@ -1434,7 +1434,7 @@
{
"kind": "Function",
"canonicalReference": "@tldraw/state!getComputedInstance:function(1)",
"docComment": "/**\n * Retrieves the underlying computed instance for a given property created with the [[computed]] decorator.\n *\n * @param obj - The object\n *\n * @param propertyName - The property name\n *\n * @example\n * ```ts\n * class Counter {\n * max = 100\n * count = atom(0)\n *\n * @computed get remaining() {\n * return this.max - this.count.value\n * }\n * }\n *\n * const c = new Counter()\n * const remaining = getComputedInstance(c, 'remaining')\n * remaining.value === 100 // true\n * c.count.set(13)\n * remaining.value === 87 // true\n * ```\n *\n * @public\n */\n",
"docComment": "/**\n * Retrieves the underlying computed instance for a given property created with the [[computed]] decorator.\n *\n * @param obj - The object\n *\n * @param propertyName - The property name\n *\n * @example\n * ```ts\n * class Counter {\n * max = 100\n * count = atom(0)\n *\n * @computed getRemaining() {\n * return this.max - this.count.value\n * }\n * }\n *\n * const c = new Counter()\n * const remaining = getComputedInstance(c, 'getRemaining')\n * remaining.value === 100 // true\n * c.count.set(13)\n * remaining.value === 87 // true\n * ```\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",

View file

@ -18,6 +18,7 @@ export class ArraySet<T> {
*
* @returns True if this ArraySet has any elements, false otherwise.
*/
// eslint-disable-next-line no-restricted-syntax
get isEmpty() {
if (this.array) {
return this.arraySize === 0

View file

@ -108,6 +108,7 @@ export class _Atom<Value, Diff = unknown> implements Atom<Value, Diff> {
/**
* @deprecated Use [[Atom.get]] instead.
*/
// eslint-disable-next-line no-restricted-syntax
get value() {
logDotValueWarning()
return this.get()

View file

@ -135,6 +135,7 @@ export class _Computed<Value, Diff = unknown> implements Computed<Value, Diff> {
children = new ArraySet<Child>()
// eslint-disable-next-line no-restricted-syntax
get isActivelyListening(): boolean {
return !this.children.isEmpty
}
@ -212,6 +213,7 @@ export class _Computed<Value, Diff = unknown> implements Computed<Value, Diff> {
/**
* @deprecated Use [[get]] instead.
*/
// eslint-disable-next-line no-restricted-syntax
get value() {
logDotValueWarning()
return this.get()
@ -311,13 +313,13 @@ const isComputedMethodKey = '@@__isComputedMethod__@@'
* max = 100
* count = atom(0)
*
* @computed get remaining() {
* @computed getRemaining() {
* return this.max - this.count.value
* }
* }
*
* const c = new Counter()
* const remaining = getComputedInstance(c, 'remaining')
* const remaining = getComputedInstance(c, 'getRemaining')
* remaining.value === 100 // true
* c.count.set(13)
* remaining.value === 87 // true
@ -355,7 +357,7 @@ export function getComputedInstance<Obj extends object, Prop extends keyof Obj>(
* console.log(greeting.value) // 'Hello John!'
* ```
*
* `computed` may also be used as a decorator for creating computed class properties.
* `computed` may also be used as a decorator for creating computed getter methods.
*
* @example
* ```ts
@ -363,7 +365,7 @@ export function getComputedInstance<Obj extends object, Prop extends keyof Obj>(
* max = 100
* count = atom<number>(0)
*
* @computed get remaining() {
* @computed getRemaining() {
* return this.max - this.count.value
* }
* }
@ -378,7 +380,7 @@ export function getComputedInstance<Obj extends object, Prop extends keyof Obj>(
* count = atom<number>(0)
*
* @computed({isEqual: (a, b) => a === b})
* get remaining() {
* getRemaining() {
* return this.max - this.count.value
* }
* }

View file

@ -60,6 +60,7 @@ export class EffectScheduler<Result> {
* Whether this scheduler is attached and actively listening to its parents.
* @public
*/
// eslint-disable-next-line no-restricted-syntax
get isActivelyListening() {
return this._isActivelyListening
}
@ -73,6 +74,7 @@ export class EffectScheduler<Result> {
* The number of times this effect has been scheduled.
* @public
*/
// eslint-disable-next-line no-restricted-syntax
get scheduleCount() {
return this._scheduleCount
}

View file

@ -22,6 +22,7 @@ class Transaction {
*
* @public
*/
// eslint-disable-next-line no-restricted-syntax
get isRoot() {
return this.parent === null
}

View file

@ -69,6 +69,7 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
private readonly options: StoreSchemaOptions<R, P>
) {}
// eslint-disable-next-line no-restricted-syntax
get currentStoreVersion(): number {
return this.options.snapshotMigrations?.currentVersion ?? 0
}

View file

@ -50,7 +50,7 @@ export class DraggingHandle extends StateNode {
) => {
const { shape, isCreating, handle } = info
this.info = info
this.parent.currentToolIdMask = info.onInteractionEnd
this.parent.setCurrentToolIdMask(info.onInteractionEnd)
this.shapeId = shape.id
this.markId = isCreating ? `creating:${shape.id}` : 'dragging handle'
if (!isCreating) this.editor.mark(this.markId)
@ -166,7 +166,7 @@ export class DraggingHandle extends StateNode {
}
override onExit = () => {
this.parent.currentToolIdMask = undefined
this.parent.setCurrentToolIdMask(undefined)
this.editor.setHintingShapes([])
this.editor.snaps.clear()
this.editor.updateInstanceState(

View file

@ -22,7 +22,7 @@ export class Idle extends StateNode {
static override id = 'idle'
override onEnter = () => {
this.parent.currentToolIdMask = undefined
this.parent.setCurrentToolIdMask(undefined)
updateHoveredId(this.editor)
this.editor.updateInstanceState(
{ cursor: { type: 'default', rotation: 0 } },

View file

@ -24,7 +24,7 @@ export class PointingCropHandle extends StateNode {
override onEnter = (info: TLPointingCropHandleInfo) => {
this.info = info
this.parent.currentToolIdMask = info.onInteractionEnd
this.parent.setCurrentToolIdMask(info.onInteractionEnd)
const selectedShape = this.editor.getSelectedShapes()[0]
if (!selectedShape) return
@ -37,7 +37,7 @@ export class PointingCropHandle extends StateNode {
{ cursor: { type: 'default', rotation: 0 } },
{ ephemeral: true }
)
this.parent.currentToolIdMask = undefined
this.parent.setCurrentToolIdMask(undefined)
}
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {

View file

@ -21,13 +21,13 @@ export class PointingRotateHandle extends StateNode {
}
override onEnter = (info: PointingRotateHandleInfo) => {
this.parent.currentToolIdMask = info.onInteractionEnd
this.parent.setCurrentToolIdMask(info.onInteractionEnd)
this.info = info
this.updateCursor()
}
override onExit = () => {
this.parent.currentToolIdMask = undefined
this.parent.setCurrentToolIdMask(undefined)
this.editor.updateInstanceState(
{ cursor: { type: 'default', rotation: 0 } },
{ ephemeral: true }

View file

@ -51,7 +51,7 @@ export class Resizing extends StateNode {
this.info = info
this.parent.currentToolIdMask = info.onInteractionEnd
this.parent.setCurrentToolIdMask(info.onInteractionEnd)
this.editAfterComplete = editAfterComplete
this.creationCursorOffset = creationCursorOffset
@ -358,7 +358,7 @@ export class Resizing extends StateNode {
}
override onExit = () => {
this.parent.currentToolIdMask = undefined
this.parent.setCurrentToolIdMask(undefined)
this.editor.updateInstanceState(
{ cursor: { type: 'default', rotation: 0 } },
{ ephemeral: true }

View file

@ -27,7 +27,7 @@ export class Rotating extends StateNode {
) => {
// Store the event information
this.info = info
this.parent.currentToolIdMask = info.onInteractionEnd
this.parent.setCurrentToolIdMask(info.onInteractionEnd)
this.markId = 'rotate start'
this.editor.mark(this.markId)
@ -42,7 +42,7 @@ export class Rotating extends StateNode {
override onExit = () => {
this.editor.setCursor({ type: 'default', rotation: 0 })
this.parent.currentToolIdMask = undefined
this.parent.setCurrentToolIdMask(undefined)
this.snapshot = {} as TLRotationSnapshot
}

View file

@ -49,7 +49,7 @@ export class Translating extends StateNode {
const { isCreating = false, editAfterComplete = false } = info
this.info = info
this.parent.currentToolIdMask = info.onInteractionEnd
this.parent.setCurrentToolIdMask(info.onInteractionEnd)
this.isCreating = isCreating
this.editAfterComplete = editAfterComplete
@ -76,7 +76,7 @@ export class Translating extends StateNode {
}
override onExit = () => {
this.parent.currentToolIdMask = undefined
this.parent.setCurrentToolIdMask(undefined)
this.editor.off('tick', this.updateParent)
this.selectionSnapshot = {} as any
this.snapshot = {} as any

View file

@ -0,0 +1,308 @@
import {
Matrix2d,
StateNode,
TLArrowShape,
TLArrowShapeTerminal,
TLCancelEvent,
TLEnterEventHandler,
TLEventHandlers,
TLHandle,
TLKeyboardEvent,
TLPointerEventInfo,
TLShapeId,
TLShapePartial,
Vec2d,
deepCopy,
snapAngle,
sortByIndex,
} from '@tldraw/editor'
export class DraggingHandle extends StateNode {
static override id = 'dragging_handle'
shapeId = '' as TLShapeId
initialHandle = {} as TLHandle
initialAdjacentHandle = null as TLHandle | null
initialPagePoint = {} as Vec2d
markId = ''
initialPageTransform: any
initialPageRotation: any
info = {} as TLPointerEventInfo & {
shape: TLArrowShape
target: 'handle'
onInteractionEnd?: string
isCreating: boolean
}
isPrecise = false
isPreciseId = null as TLShapeId | null
pointingId = null as TLShapeId | null
override onEnter: TLEnterEventHandler = (
info: TLPointerEventInfo & {
shape: TLArrowShape
target: 'handle'
onInteractionEnd?: string
isCreating: boolean
}
) => {
const { shape, isCreating, handle } = info
this.info = info
this.parent.setCurrentToolIdMask(info.onInteractionEnd)
this.shapeId = shape.id
this.markId = isCreating ? `creating:${shape.id}` : 'dragging handle'
if (!isCreating) this.editor.mark(this.markId)
this.initialHandle = deepCopy(handle)
this.initialPageTransform = this.editor.getShapePageTransform(shape)!
this.initialPageRotation = this.initialPageTransform.rotation()
this.initialPagePoint = this.editor.inputs.originPagePoint.clone()
this.editor.updateInstanceState(
{ cursor: { type: isCreating ? 'cross' : 'grabbing', rotation: 0 } },
{ ephemeral: true }
)
// <!-- Only relevant to arrows
const handles = this.editor.getShapeHandles(shape)!.sort(sortByIndex)
const index = handles.findIndex((h) => h.id === info.handle.id)
// Find the adjacent handle
this.initialAdjacentHandle = null
// Start from the handle and work forward
for (let i = index + 1; i < handles.length; i++) {
const handle = handles[i]
if (handle.type === 'vertex' && handle.id !== 'middle' && handle.id !== info.handle.id) {
this.initialAdjacentHandle = handle
break
}
}
// If still no handle, start from the end and work backward
if (!this.initialAdjacentHandle) {
for (let i = handles.length - 1; i >= 0; i--) {
const handle = handles[i]
if (handle.type === 'vertex' && handle.id !== 'middle' && handle.id !== info.handle.id) {
this.initialAdjacentHandle = handle
break
}
}
}
const initialTerminal = shape.props[info.handle.id as 'start' | 'end']
this.isPrecise = false
if (initialTerminal?.type === 'binding') {
this.editor.setHintingShapes([initialTerminal.boundShapeId])
this.isPrecise = !Vec2d.Equals(initialTerminal.normalizedAnchor, { x: 0.5, y: 0.5 })
if (this.isPrecise) {
this.isPreciseId = initialTerminal.boundShapeId
} else {
this.resetExactTimeout()
}
} else {
this.editor.setHintingShapes([])
}
// -->
this.update()
this.editor.select(this.shapeId)
}
// Only relevant to arrows
private exactTimeout = -1 as any
// Only relevant to arrows
private resetExactTimeout() {
if (this.exactTimeout !== -1) {
this.clearExactTimeout()
}
this.exactTimeout = setTimeout(() => {
if (this.getIsActive() && !this.isPrecise) {
this.isPrecise = true
this.isPreciseId = this.pointingId
this.update()
}
this.exactTimeout = -1
}, 750)
}
// Only relevant to arrows
private clearExactTimeout() {
if (this.exactTimeout !== -1) {
clearTimeout(this.exactTimeout)
this.exactTimeout = -1
}
}
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
this.update()
}
override onKeyDown: TLKeyboardEvent | undefined = () => {
this.update()
}
override onKeyUp: TLKeyboardEvent | undefined = () => {
this.update()
}
override onPointerUp: TLEventHandlers['onPointerUp'] = () => {
this.complete()
}
override onComplete: TLEventHandlers['onComplete'] = () => {
this.complete()
}
override onCancel: TLCancelEvent = () => {
this.cancel()
}
override onExit = () => {
this.parent.setCurrentToolIdMask(undefined)
this.editor.setHintingShapes([])
this.editor.snaps.clear()
this.editor.updateInstanceState(
{ cursor: { type: 'default', rotation: 0 } },
{ ephemeral: true }
)
}
private complete() {
this.editor.snaps.clear()
const { onInteractionEnd } = this.info
if (this.editor.getInstanceState().isToolLocked && onInteractionEnd) {
// Return to the tool that was active before this one,
// but only if tool lock is turned on!
this.editor.setCurrentTool(onInteractionEnd, { shapeId: this.shapeId })
return
}
this.parent.transition('idle')
}
private cancel() {
this.editor.bailToMark(this.markId)
this.editor.snaps.clear()
const { onInteractionEnd } = this.info
if (onInteractionEnd) {
// Return to the tool that was active before this one,
// whether tool lock is turned on or not!
this.editor.setCurrentTool(onInteractionEnd, { shapeId: this.shapeId })
return
}
this.parent.transition('idle')
}
private update() {
const { editor, shapeId, initialPagePoint } = this
const { initialHandle, initialPageRotation, initialAdjacentHandle } = this
const hintingShapeIds = this.editor.getHintingShapeIds()
const {
user: { isSnapMode },
snaps,
inputs: { currentPagePoint, shiftKey, ctrlKey, altKey, pointerVelocity },
} = editor
const initial = this.info.shape
const shape = editor.getShape(shapeId)
if (!shape) return
const util = editor.getShapeUtil(shape)
let point = currentPagePoint
.clone()
.sub(initialPagePoint)
.rot(-initialPageRotation)
.add(initialHandle)
if (shiftKey && initialAdjacentHandle && initialHandle.id !== 'middle') {
const angle = Vec2d.Angle(initialAdjacentHandle, point)
const snappedAngle = snapAngle(angle, 24)
const angleDifference = snappedAngle - angle
point = Vec2d.RotWith(point, initialAdjacentHandle, angleDifference)
}
// Clear any existing snaps
editor.snaps.clear()
if (initialHandle.canSnap && (isSnapMode ? !ctrlKey : ctrlKey)) {
// We're snapping
const pageTransform = editor.getShapePageTransform(shape.id)
if (!pageTransform) throw Error('Expected a page transform')
// We want to skip the segments that include the handle, so
// find the index of the handle that shares the same index property
// as the initial dragging handle; this catches a quirk of create handles
const handleIndex = editor
.getShapeHandles(shape)!
.filter(({ type }) => type === 'vertex')
.sort(sortByIndex)
.findIndex(({ index }) => initialHandle.index === index)
// Get all the outline segments from the shape
const additionalSegments = util
.getOutlineSegments(shape)
.map((segment) => Matrix2d.applyToPoints(pageTransform, segment))
.filter((_segment, i) => i !== handleIndex - 1 && i !== handleIndex)
const snapDelta = snaps.getSnappingHandleDelta({
additionalSegments,
handlePoint: Matrix2d.applyToPoint(pageTransform, point),
})
if (snapDelta) {
snapDelta.rot(-editor.getShapeParentTransform(shape)!.rotation())
point.add(snapDelta)
}
}
const changes = util.onHandleChange?.(shape, {
handle: {
...initialHandle,
x: point.x,
y: point.y,
},
isPrecise: this.isPrecise || altKey,
initial: initial,
})
const next: TLShapePartial<any> = { ...shape, ...changes }
// Arrows
if (initialHandle.canBind) {
const bindingAfter = (next.props as any)[initialHandle.id] as TLArrowShapeTerminal | undefined
if (bindingAfter?.type === 'binding') {
if (hintingShapeIds[0] !== bindingAfter.boundShapeId) {
editor.setHintingShapes([bindingAfter.boundShapeId])
this.pointingId = bindingAfter.boundShapeId
this.isPrecise = pointerVelocity.len() < 0.5 || altKey
this.isPreciseId = this.isPrecise ? bindingAfter.boundShapeId : null
this.resetExactTimeout()
}
} else {
if (hintingShapeIds.length > 0) {
editor.setHintingShapes([])
this.pointingId = null
this.isPrecise = false
this.isPreciseId = null
this.resetExactTimeout()
}
}
}
if (changes) {
editor.updateShapes([next], { squashing: true })
}
}
}

View file

@ -13,17 +13,17 @@ export class ZoomTool extends StateNode {
override onEnter = (info: TLPointerEventInfo & { onInteractionEnd: string }) => {
this.info = info
this.currentToolIdMask = info.onInteractionEnd
this.parent.setCurrentToolIdMask(info.onInteractionEnd)
this.updateCursor()
}
override onExit = () => {
this.currentToolIdMask = undefined
this.parent.setCurrentToolIdMask(undefined)
this.editor.updateInstanceState(
{ zoomBrush: null, cursor: { type: 'default', rotation: 0 } },
{ ephemeral: true }
)
this.currentToolIdMask = undefined
this.parent.setCurrentToolIdMask(undefined)
}
override onKeyDown: TLKeyboardEvent | undefined = () => {

View file

@ -477,7 +477,11 @@ export class TestEditor extends Editor {
.clone()
.rotWith(this.getSelectionRotatedPageBounds()!.point, this.getSelectionRotation())
const targetHandlePoint = Vec2d.RotWith(handlePoint, this.selectionPageCenter!, angleRadians)
const targetHandlePoint = Vec2d.RotWith(
handlePoint,
this.getSelectionPageCenter()!,
angleRadians
)
this.pointerDown(handlePoint.x, handlePoint.y, { target: 'selection', handle })
this.pointerMove(targetHandlePoint.x, targetHandlePoint.y, { shiftKey })
@ -491,7 +495,7 @@ export class TestEditor extends Editor {
* @readonly
* @public
*/
get selectionPageCenter() {
getSelectionPageCenter() {
const selectionRotation = this.getSelectionRotation()
const selectionBounds = this.getSelectionRotatedPageBounds()
if (!selectionBounds) return null
@ -504,7 +508,7 @@ export class TestEditor extends Editor {
}
this.setCurrentTool('select')
const center = this.selectionPageCenter!
const center = this.getSelectionPageCenter()!
this.pointerDown(center.x, center.y, this.getSelectedShapeIds()[0])
const numSteps = 10

View file

@ -61,25 +61,25 @@ describe('Editor.moveShapesToPage', () => {
it('Adds undo items', () => {
editor.history.clear()
editor.moveShapesToPage([ids.box1], ids.page2)
expect(editor.history.numUndos).toBeGreaterThan(1)
expect(editor.history.getNumUndos()).toBeGreaterThan(1)
})
it('Does nothing on an empty ids array', () => {
editor.history.clear()
editor.moveShapesToPage([], ids.page2)
expect(editor.history.numUndos).toBe(0)
expect(editor.history.getNumUndos()).toBe(0)
})
it('Does nothing if the new page is not found or is deleted', () => {
editor.history.clear()
editor.moveShapesToPage([ids.box1], PageRecordType.createId('missing'))
expect(editor.history.numUndos).toBe(0)
expect(editor.history.getNumUndos()).toBe(0)
})
it('Does not move shapes to the current page', () => {
editor.history.clear()
editor.moveShapesToPage([ids.box1], ids.page1)
expect(editor.history.numUndos).toBe(0)
expect(editor.history.getNumUndos()).toBe(0)
})
it('Restores on undo / redo', () => {

View file

@ -24,13 +24,13 @@ describe('resizing a shape', () => {
editor.createShapes([{ id: ids.boxA, type: 'geo', props: { w: 100, h: 100 } }])
editor.mark('start')
const startHistoryLength = editor.history.numUndos
const startHistoryLength = editor.history.getNumUndos()
editor.resizeShape(ids.boxA, { x: 2, y: 2 })
expect(editor.history.numUndos).toBe(startHistoryLength + 1)
expect(editor.history.getNumUndos()).toBe(startHistoryLength + 1)
editor.resizeShape(ids.boxA, { x: 2, y: 2 })
expect(editor.history.numUndos).toBe(startHistoryLength + 1)
expect(editor.history.getNumUndos()).toBe(startHistoryLength + 1)
editor.resizeShape(ids.boxA, { x: 2, y: 2 })
expect(editor.history.numUndos).toBe(startHistoryLength + 1)
expect(editor.history.getNumUndos()).toBe(startHistoryLength + 1)
expect(editor.getShapePageBounds(ids.boxA)).toCloselyMatchObject({
w: 800,

View file

@ -57,7 +57,7 @@ describe('editor.rotateShapes', () => {
// Select the shape...
editor.select(ids.box1, ids.box2)
const { selectionPageCenter } = editor
const selectionPageCenter = editor.getSelectionPageCenter()
// Rotate the shape...
editor.rotateShapesBy(editor.getSelectedShapeIds(), Math.PI)
@ -77,6 +77,6 @@ describe('editor.rotateShapes', () => {
.expectShapeToMatch({ id: ids.box2, rotation: Math.PI })
// Are the centers the same?
expect(selectionPageCenter).toCloselyMatchObject(editor.selectionPageCenter!)
expect(selectionPageCenter).toCloselyMatchObject(editor.getSelectionPageCenter()!)
})
})

View file

@ -53,7 +53,7 @@ describe('setCurrentPage', () => {
editor.setCurrentPage(editor.getPages()[1].id)
editor.setCurrentPage(editor.getPages()[0].id)
editor.setCurrentPage(editor.getPages()[0].id)
expect(editor.history.numUndos).toBe(1)
expect(editor.history.getNumUndos()).toBe(1)
})
it('preserves the undo stack', () => {
@ -68,7 +68,7 @@ describe('setCurrentPage', () => {
editor.setCurrentPage(editor.getPages()[0].id)
editor.setCurrentPage(editor.getPages()[0].id)
expect(editor.getShape(boxId)).toBeUndefined()
expect(editor.history.numUndos).toBe(1)
expect(editor.history.getNumUndos()).toBe(1)
editor.redo()
expect(editor.getShape(boxId)).not.toBeUndefined()
})

View file

@ -127,7 +127,7 @@ describe('creating frames', () => {
// x should snap
editor.keyDown('Control')
expect(editor.snaps.lines).toHaveLength(1)
expect(editor.snaps.getLines()).toHaveLength(1)
expect(editor.getShapePageBounds(editor.getOnlySelectedShape()!)).toMatchObject({
x: 50,
y: 100,
@ -346,7 +346,7 @@ describe('frame shapes', () => {
expect(editor.getShapePageBounds(ids.boxA)).toMatchObject({ y: 49 })
editor.keyDown('Control')
expect(editor.getShapePageBounds(ids.boxA)).toMatchObject({ y: 50 })
expect(editor.snaps.lines).toHaveLength(1)
expect(editor.snaps.getLines()).toHaveLength(1)
})
it("does not allow outside shapes to snap to the frame's children", () => {
@ -382,12 +382,12 @@ describe('frame shapes', () => {
w: 5,
h: 5,
})
expect(editor.snaps.lines).toHaveLength(0)
expect(editor.snaps.getLines()).toHaveLength(0)
// and if we unparent the box it should snap
editor.reparentShapes([innerBoxId], editor.currentPageId)
editor.pointerMove(287.5, 126.5).pointerMove(277.5, 126.5)
expect(editor.snaps.lines).toHaveLength(1)
expect(editor.snaps.getLines()).toHaveLength(1)
expect(editor.getShapePageBounds(editor.getOnlySelectedShape()!)).toMatchObject({
x: 275,
y: 125,
@ -414,12 +414,12 @@ describe('frame shapes', () => {
editor.setCurrentTool('select')
editor.pointerDown(150, 150, innerBoxId).pointerMove(150, 50).pointerMove(150, 148)
editor.keyDown('Control')
expect(editor.snaps.lines).toHaveLength(0)
expect(editor.snaps.getLines()).toHaveLength(0)
// move shape inside the frame to make sure it snaps in there
editor.reparentShapes([outerBoxId], frameId).pointerMove(150, 149, { ctrlKey: true })
expect(editor.snaps.lines).toHaveLength(1)
expect(editor.snaps.getLines()).toHaveLength(1)
})
it('masks its children', () => {

View file

@ -8,7 +8,7 @@ const simplifyNumber = (n: number) => {
}
export const getSnapLines = (scene: Editor) => {
const result = []
for (const snap of scene.snaps.lines) {
for (const snap of scene.snaps.getLines()) {
if (snap.type !== 'points') {
throw new Error('Expected only points snap')
}

View file

@ -1922,14 +1922,14 @@ describe('snapping', () => {
editor.select(groupCId)
editor.pointerDown(10, 5, groupCId)
editor.pointerMove(80, 5, groupCId, { ctrlKey: true })
expect(editor.snaps.lines.length).toBe(0)
expect(editor.snaps.getLines().length).toBe(0)
})
it('does not happen between children and thier group', () => {
editor.select(ids.boxD)
editor.pointerDown(65, 5, ids.boxD)
editor.pointerMove(80, 105, ids.boxD, { ctrlKey: true })
expect(editor.snaps.lines.length).toBe(0)
expect(editor.snaps.getLines().length).toBe(0)
})
})

View file

@ -224,7 +224,7 @@ describe('When pasting', () => {
// Should put the pasted shapes centered in the frame
editor.select(shapes.new.box1!.id, shapes.new.box1!.id)
expect(editor.selectionPageCenter).toMatchObject(
expect(editor.getSelectionPageCenter()).toMatchObject(
editor.getPageCenter(editor.getShape(ids.frame1)!)!
)
})
@ -317,7 +317,7 @@ describe('When pasting', () => {
// Should put the pasted shapes centered in the frame
editor.select(shapes.new.box1!.id, shapes.new.box1!.id)
expect(editor.selectionPageCenter).toMatchObject(editor.getPageCenter(shapes.old.frame1)!)
expect(editor.getSelectionPageCenter()).toMatchObject(editor.getPageCenter(shapes.old.frame1)!)
})
})

View file

@ -940,8 +940,10 @@ describe('When resizing a shape with children', () => {
})
function getGapAndPointLines() {
const gapLines = editor.snaps.lines.filter((snap) => snap.type === 'gaps') as GapsSnapLine[]
const pointLines = editor.snaps.lines.filter((snap) => snap.type === 'points') as PointsSnapLine[]
const gapLines = editor.snaps.getLines().filter((snap) => snap.type === 'gaps') as GapsSnapLine[]
const pointLines = editor.snaps
.getLines()
.filter((snap) => snap.type === 'points') as PointsSnapLine[]
return { gapLines, pointLines }
}
@ -985,12 +987,12 @@ describe('snapping while resizing', () => {
.pointerMove(115, 59, { ctrlKey: true })
expect(editor.getShape(ids.boxX)).toMatchObject({ x: 80, y: 60, props: { w: 60, h: 80 } })
expect(editor.snaps.lines.length).toBe(1)
expect(editor.snaps.getLines().length).toBe(1)
// moving the mouse horizontally should not change things
editor.pointerMove(15, 65, { ctrlKey: true })
expect(editor.getShape(ids.boxX)).toMatchObject({ x: 80, y: 60, props: { w: 60, h: 80 } })
expect(editor.snaps.lines.length).toBe(1)
expect(editor.snaps.getLines().length).toBe(1)
expect(getGapAndPointLines().pointLines[0].points).toHaveLength(6)
@ -998,7 +1000,7 @@ describe('snapping while resizing', () => {
editor.pointerMove(15, 43, { ctrlKey: true })
expect(editor.getShape(ids.boxX)).toMatchObject({ x: 80, y: 40, props: { w: 60, h: 100 } })
expect(editor.snaps.lines.length).toBe(1)
expect(editor.snaps.getLines().length).toBe(1)
expect(getGapAndPointLines().pointLines[0].points).toHaveLength(4)
})
@ -1015,7 +1017,7 @@ describe('snapping while resizing', () => {
.pointerMove(156, 115, { ctrlKey: true })
expect(editor.getShape(ids.boxX)).toMatchObject({ x: 80, y: 80, props: { w: 80, h: 60 } })
expect(editor.snaps.lines.length).toBe(1)
expect(editor.snaps.getLines().length).toBe(1)
expect(getGapAndPointLines().pointLines[0].points).toHaveLength(6)
@ -1026,7 +1028,7 @@ describe('snapping while resizing', () => {
// snap to left edge of B
editor.pointerMove(173, 280, { ctrlKey: true })
expect(editor.getShape(ids.boxX)).toMatchObject({ x: 80, y: 80, props: { w: 100, h: 60 } })
expect(editor.snaps.lines.length).toBe(1)
expect(editor.snaps.getLines().length).toBe(1)
expect(getGapAndPointLines().pointLines[0].points).toHaveLength(4)
})
@ -1041,19 +1043,19 @@ describe('snapping while resizing', () => {
.pointerMove(115, 159, { ctrlKey: true })
expect(editor.getShape(ids.boxX)).toMatchObject({ x: 80, y: 80, props: { w: 60, h: 80 } })
expect(editor.snaps.lines.length).toBe(1)
expect(editor.snaps.getLines().length).toBe(1)
expect(getGapAndPointLines().pointLines[0].points).toHaveLength(6)
// changing horzontal mouse position should not change things
editor.pointerMove(315, 163, { ctrlKey: true })
expect(editor.getShape(ids.boxX)).toMatchObject({ x: 80, y: 80, props: { w: 60, h: 80 } })
expect(editor.snaps.lines.length).toBe(1)
expect(editor.snaps.getLines().length).toBe(1)
// snap to top edge of C
editor.pointerMove(115, 183, { ctrlKey: true })
expect(editor.getShape(ids.boxX)).toMatchObject({ x: 80, y: 80, props: { w: 60, h: 100 } })
expect(editor.snaps.lines.length).toBe(1)
expect(editor.snaps.getLines().length).toBe(1)
expect(getGapAndPointLines().pointLines[0].points).toHaveLength(4)
})
@ -1070,7 +1072,7 @@ describe('snapping while resizing', () => {
expect(editor.getShape(ids.boxX)).toMatchObject({ x: 60, y: 80, props: { w: 80, h: 60 } })
expect(editor.snaps.lines.length).toBe(1)
expect(editor.snaps.getLines().length).toBe(1)
expect(getGapAndPointLines().pointLines[0].points).toHaveLength(6)
// moving the mouse vertically should not change things
@ -1081,7 +1083,7 @@ describe('snapping while resizing', () => {
editor.pointerMove(39, 280, { ctrlKey: true })
expect(editor.getShape(ids.boxX)).toMatchObject({ x: 40, y: 80, props: { w: 100, h: 60 } })
expect(editor.snaps.lines.length).toBe(1)
expect(editor.snaps.getLines().length).toBe(1)
expect(getGapAndPointLines().pointLines[0].points).toHaveLength(4)
})
it('works for dragging the top left corner', () => {
@ -3020,7 +3022,7 @@ describe('resizing a shape with a child', () => {
.pointerDown(0, 0, { target: 'selection', handle: 'top_left' })
.pointerMove(25, 25, { ctrlKey: true })
expect(editor.snaps.lines.length).toBe(0)
expect(editor.snaps.getLines().length).toBe(0)
expect(editor.getShape(ids.boxA)).toMatchObject({ x: 25, y: 25, props: { w: 25, h: 25 } })
expect(editor.getShape(ids.boxB)).toMatchObject({ x: 0.5, y: 0.5, props: { w: 5, h: 5 } })
expect(editor.getShapePageBounds(ids.boxB)).toMatchObject({

View file

@ -8,7 +8,7 @@ const simplifyNumber = (n: number) => {
}
export const getSnapLines = (scene: Editor) => {
const result = []
for (const snap of scene.snaps.lines) {
for (const snap of scene.snaps.getLines()) {
if (snap.type !== 'points') {
throw new Error('Expected only points snap')
}

View file

@ -117,8 +117,8 @@ describe.skip('custom snapping points', () => {
// └───────┘ x───────x
editor.pointerDown(250, 250, ids.box1).pointerMove(250, 51, { ctrlKey: true })
expect(editor.getShape(ids.box1)).toMatchObject({ x: 200, y: 0 })
expect(editor.snaps.lines?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.lines![0])).toBe(3)
expect(editor.snaps.getLines()?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.getLines()![0])).toBe(3)
// should not snap to 100 on y axis
// x───────┐
@ -132,7 +132,7 @@ describe.skip('custom snapping points', () => {
// x───────x
editor.pointerMove(250, 151, { ctrlKey: true })
expect(editor.getShape(ids.box1)).toMatchObject({ x: 200, y: 101 })
expect(editor.snaps.lines?.length).toBe(0)
expect(editor.snaps.getLines()?.length).toBe(0)
// should not snap to 50 on y axis
// x───────┐
@ -144,7 +144,7 @@ describe.skip('custom snapping points', () => {
// x───────x
editor.pointerMove(250, 101, { ctrlKey: true })
expect(editor.getShape(ids.box1)).toMatchObject({ x: 200, y: 51 })
expect(editor.snaps.lines?.length).toBe(0)
expect(editor.snaps.getLines()?.length).toBe(0)
// should snap to 0 on x axis
// x x───────┐
@ -160,8 +160,8 @@ describe.skip('custom snapping points', () => {
// x x───────x
editor.pointerMove(51, 250, { ctrlKey: true })
expect(editor.getShape(ids.box1)).toMatchObject({ x: 0, y: 200 })
expect(editor.snaps.lines?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.lines![0])).toBe(3)
expect(editor.snaps.getLines()?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.getLines()![0])).toBe(3)
// should not snap to 100 on x axis
// x───────┐
@ -177,7 +177,7 @@ describe.skip('custom snapping points', () => {
// x───────x
editor.pointerMove(151, 250, { ctrlKey: true })
expect(editor.getShape(ids.box1)).toMatchObject({ x: 101, y: 200 })
expect(editor.snaps.lines?.length).toBe(0)
expect(editor.snaps.getLines()?.length).toBe(0)
// should not snap to 50 on x axis
// x───────┐
@ -193,7 +193,7 @@ describe.skip('custom snapping points', () => {
// x───────x
editor.pointerMove(101, 250, { ctrlKey: true })
expect(editor.getShape(ids.box1)).toMatchObject({ x: 51, y: 200 })
expect(editor.snaps.lines?.length).toBe(0)
expect(editor.snaps.getLines()?.length).toBe(0)
})
it('allows shapes with custom points to snap to other shapes', () => {
@ -206,8 +206,8 @@ describe.skip('custom snapping points', () => {
// └───────┘ x───────x
editor.pointerDown(50, 50, ids.boxT).pointerMove(50, 251, { ctrlKey: true })
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 0, y: 200 })
expect(editor.snaps.lines?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.lines![0])).toBe(3)
expect(editor.snaps.getLines()?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.getLines()![0])).toBe(3)
// should snap to 250 on y axis
// x─────────────────x
@ -220,8 +220,8 @@ describe.skip('custom snapping points', () => {
// └───────┘
editor.pointerMove(50, 301, { ctrlKey: true })
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 0, y: 250 })
expect(editor.snaps.lines?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.lines![0])).toBe(2)
expect(editor.snaps.getLines()?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.getLines()![0])).toBe(2)
// should snap to 300 on y axis
// x─────────────x───────x
@ -236,8 +236,8 @@ describe.skip('custom snapping points', () => {
// └───────┘
editor.pointerMove(50, 351, { ctrlKey: true })
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 0, y: 300 })
expect(editor.snaps.lines?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.lines![0])).toBe(3)
expect(editor.snaps.getLines()?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.getLines()![0])).toBe(3)
// should snap to 200 on x axis
// x x───────┐
@ -253,8 +253,8 @@ describe.skip('custom snapping points', () => {
// x x───────x
editor.pointerMove(251, 50, { ctrlKey: true })
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 200, y: 0 })
expect(editor.snaps.lines?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.lines![0])).toBe(3)
expect(editor.snaps.getLines()?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.getLines()![0])).toBe(3)
// should snap to 250 on x axis
// x x───────┐
@ -270,8 +270,8 @@ describe.skip('custom snapping points', () => {
// x───────x
editor.pointerMove(301, 50, { ctrlKey: true })
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 250, y: 0 })
expect(editor.snaps.lines?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.lines![0])).toBe(2)
expect(editor.snaps.getLines()?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.getLines()![0])).toBe(2)
// should snap to 300 on x axis
// x x───────┐
@ -287,8 +287,8 @@ describe.skip('custom snapping points', () => {
// x x───────x
editor.pointerMove(351, 50, { ctrlKey: true })
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 300, y: 0 })
expect(editor.snaps.lines?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.lines![0])).toBe(3)
expect(editor.snaps.getLines()?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.getLines()![0])).toBe(3)
})
it('becomes part of the selection bounding box if there is more than one shape in the selection', () => {
@ -315,9 +315,9 @@ describe.skip('custom snapping points', () => {
// x───────x
editor.select(ids.boxT, ids.box1)
editor.pointerDown(50, 50, ids.boxT).pointerMove(351, 50, { ctrlKey: true })
expect(editor.snaps.lines?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.lines![0])).toBe(2)
expect(getSnapPoints(editor.snaps.lines![0])?.map(({ x }) => x)).toEqual([450, 450])
expect(editor.snaps.getLines()?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.getLines()![0])).toBe(2)
expect(getSnapPoints(editor.snaps.getLines()![0])?.map(({ x }) => x)).toEqual([450, 450])
})
})
@ -379,8 +379,8 @@ describe.skip('custom snapping points', () => {
// └───────┘ x───────x
editor.pointerDown(250, 250, ids.box1).pointerMove(250, 51, { ctrlKey: true })
expect(editor.getShape(ids.box1)).toMatchObject({ x: 200, y: 0 })
expect(editor.snaps.lines?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.lines![0])).toBe(3)
expect(editor.snaps.getLines()?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.getLines()![0])).toBe(3)
// should not snap to 100 on y axis
// x───────┐
@ -394,7 +394,7 @@ describe.skip('custom snapping points', () => {
// x───────x
editor.pointerMove(250, 151, { ctrlKey: true })
expect(editor.getShape(ids.box1)).toMatchObject({ x: 200, y: 101 })
expect(editor.snaps.lines?.length).toBe(0)
expect(editor.snaps.getLines()?.length).toBe(0)
// should not snap to 50 on y axis
// x───────┐
@ -406,7 +406,7 @@ describe.skip('custom snapping points', () => {
// x───────x
editor.pointerMove(250, 101, { ctrlKey: true })
expect(editor.getShape(ids.box1)).toMatchObject({ x: 200, y: 51 })
expect(editor.snaps.lines?.length).toBe(0)
expect(editor.snaps.getLines()?.length).toBe(0)
// should snap to 0 on x axis
// x x───────┐
@ -422,8 +422,8 @@ describe.skip('custom snapping points', () => {
// x x───────x
editor.pointerMove(51, 250, { ctrlKey: true })
expect(editor.getShape(ids.box1)).toMatchObject({ x: 0, y: 200 })
expect(editor.snaps.lines?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.lines![0])).toBe(3)
expect(editor.snaps.getLines()?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.getLines()![0])).toBe(3)
// should not snap to 100 on x axis
// x───────┐
@ -439,7 +439,7 @@ describe.skip('custom snapping points', () => {
// x───────x
editor.pointerMove(151, 250, { ctrlKey: true })
expect(editor.getShape(ids.box1)).toMatchObject({ x: 101, y: 200 })
expect(editor.snaps.lines?.length).toBe(0)
expect(editor.snaps.getLines()?.length).toBe(0)
// should not snap to 50 on x axis
// x───────┐
@ -455,7 +455,7 @@ describe.skip('custom snapping points', () => {
// x───────x
editor.pointerMove(101, 250, { ctrlKey: true })
expect(editor.getShape(ids.box1)).toMatchObject({ x: 51, y: 200 })
expect(editor.snaps.lines?.length).toBe(0)
expect(editor.snaps.getLines()?.length).toBe(0)
})
it('allows shapes with custom points to snap to other shapes', () => {
@ -468,8 +468,8 @@ describe.skip('custom snapping points', () => {
// └───────┘ x───────x
editor.pointerDown(50, 50, ids.boxT).pointerMove(50, 251, { ctrlKey: true })
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 0, y: 200 })
expect(editor.snaps.lines?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.lines![0])).toBe(3)
expect(editor.snaps.getLines()?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.getLines()![0])).toBe(3)
// should snap to 250 on y axis
// x─────────────────x
@ -482,8 +482,8 @@ describe.skip('custom snapping points', () => {
// └───────┘
editor.pointerMove(50, 301, { ctrlKey: true })
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 0, y: 250 })
expect(editor.snaps.lines?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.lines![0])).toBe(2)
expect(editor.snaps.getLines()?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.getLines()![0])).toBe(2)
// should snap to 300 on y axis
// x─────────────x───────x
@ -498,8 +498,8 @@ describe.skip('custom snapping points', () => {
// └───────┘
editor.pointerMove(50, 351, { ctrlKey: true })
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 0, y: 300 })
expect(editor.snaps.lines?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.lines![0])).toBe(3)
expect(editor.snaps.getLines()?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.getLines()![0])).toBe(3)
// should snap to 200 on x axis
// x x───────┐
@ -515,8 +515,8 @@ describe.skip('custom snapping points', () => {
// x x───────x
editor.pointerMove(251, 50, { ctrlKey: true })
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 200, y: 0 })
expect(editor.snaps.lines?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.lines![0])).toBe(3)
expect(editor.snaps.getLines()?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.getLines()![0])).toBe(3)
// should snap to 250 on x axis
// x x───────┐
@ -532,8 +532,8 @@ describe.skip('custom snapping points', () => {
// x───────x
editor.pointerMove(301, 50, { ctrlKey: true })
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 250, y: 0 })
expect(editor.snaps.lines?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.lines![0])).toBe(2)
expect(editor.snaps.getLines()?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.getLines()![0])).toBe(2)
// should snap to 300 on x axis
// x x───────┐
@ -549,8 +549,8 @@ describe.skip('custom snapping points', () => {
// x x───────x
editor.pointerMove(351, 50, { ctrlKey: true })
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 300, y: 0 })
expect(editor.snaps.lines?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.lines![0])).toBe(3)
expect(editor.snaps.getLines()?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.getLines()![0])).toBe(3)
})
it('becomes part of the selection bounding box if there is more than one shape in the selection', () => {
@ -577,8 +577,8 @@ describe.skip('custom snapping points', () => {
// x───────x
editor.select(ids.boxT, ids.box1)
editor.pointerDown(50, 50, ids.boxT).pointerMove(351, 50, { ctrlKey: true })
expect(editor.snaps.lines?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.lines![0])).toBe(2)
expect(getSnapPoints(editor.snaps.lines![0])?.map(({ x }) => x)).toEqual([450, 450])
expect(editor.snaps.getLines()?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.getLines()![0])).toBe(2)
expect(getSnapPoints(editor.snaps.getLines()![0])?.map(({ x }) => x)).toEqual([450, 450])
})
})

View file

@ -510,9 +510,9 @@ describe('snapping with single shapes', () => {
// ┼ └──────┘
editor.pointerDown(25, 5, ids.box2).pointerMove(16, 35, { ctrlKey: true })
expect(editor.snaps.lines?.length).toBe(1)
expect(editor.snaps.getLines()?.length).toBe(1)
expect(getNumSnapPoints(editor.snaps.lines![0])).toBe(4)
expect(getNumSnapPoints(editor.snaps.getLines()![0])).toBe(4)
})
it('shows all the horizonal lines + points where the bounding boxes align', () => {
@ -523,7 +523,7 @@ describe('snapping with single shapes', () => {
// x─────x────────────────────x─────x
editor.pointerDown(25, 5, ids.box2).pointerMove(36, 5, { ctrlKey: true })
const snaps = editor.snaps.lines!.sort((a, b) => getNumSnapPoints(a) - getNumSnapPoints(b))
const snaps = editor.snaps.getLines()!.sort((a, b) => getNumSnapPoints(a) - getNumSnapPoints(b))
expect(snaps.length).toBe(3)
// center snap line
@ -544,7 +544,7 @@ describe('snapping with single shapes', () => {
// x └─────┘ x
editor.pointerDown(25, 5, ids.box2).pointerMove(5, 45, { ctrlKey: true })
const snaps = editor.snaps.lines!.sort((a, b) => getNumSnapPoints(a) - getNumSnapPoints(b))
const snaps = editor.snaps.getLines()!.sort((a, b) => getNumSnapPoints(a) - getNumSnapPoints(b))
expect(snaps.length).toBe(3)
// center snap line
@ -560,23 +560,23 @@ describe('snapping with single shapes', () => {
editor.updateShapes([{ id: ids.box1, type: 'geo', x: -20 }])
editor.pointerDown(25, 5, ids.box2).pointerMove(36, 5, { ctrlKey: true })
expect(editor.snaps.lines!.length).toBe(0)
expect(editor.snaps.getLines()!.length).toBe(0)
editor.updateShapes([{ id: ids.box1, type: 'geo', x: editor.getViewportScreenBounds().w + 10 }])
editor.pointerMove(33, 5, { ctrlKey: true })
expect(editor.snaps.lines!.length).toBe(0)
expect(editor.snaps.getLines()!.length).toBe(0)
editor.updateShapes([{ id: ids.box1, type: 'geo', y: -20 }])
editor.pointerMove(5, 5, { ctrlKey: true })
expect(editor.snaps.lines!.length).toBe(0)
expect(editor.snaps.getLines()!.length).toBe(0)
editor.updateShapes([
{ id: ids.box1, type: 'geo', x: 0, y: editor.getViewportScreenBounds().h + 10 },
])
editor.pointerMove(5, 5, { ctrlKey: true })
expect(editor.snaps.lines!.length).toBe(0)
expect(editor.snaps.getLines()!.length).toBe(0)
})
it('does not snap on the Y axis if the shift key is pressed', () => {
@ -740,9 +740,10 @@ describe('Snap-between behavior', () => {
// the midpoint is 125 and c is 10 wide so it should snap to 120 if we put it at 121
editor.pointerDown(55, 5, ids.line1).pointerMove(126, 67, { ctrlKey: true })
expect(editor.getShape(ids.line1)).toMatchObject({ x: 120, y: 62 })
expect(editor.snaps.lines?.length).toBe(1)
assertGaps(editor.snaps.lines![0])
expect(editor.snaps.lines![0].gaps.length).toBe(2)
expect(editor.snaps.getLines()?.length).toBe(1)
const line = editor.snaps.getLines()![0]
assertGaps(line)
expect(line.gaps.length).toBe(2)
})
it('shows horizontal point snaps at the same time as horizontal gap snaps', () => {
// ┌─────┐ ┌─────┐
@ -763,7 +764,7 @@ describe('Snap-between behavior', () => {
editor.pointerDown(55, 5, ids.line1).pointerMove(126, 94, { ctrlKey: true })
expect(editor.getShape(ids.line1)).toMatchObject({ x: 120, y: 90 })
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.lines!)
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.getLines()!)
expect(gapLines).toHaveLength(1)
expect(pointLines).toHaveLength(1)
expect(gapLines[0].gaps.length).toBe(2)
@ -793,7 +794,7 @@ describe('Snap-between behavior', () => {
// the midpoint is 125 and c is 10 wide so it should snap to 120 if we put it at 121
editor.pointerDown(55, 5, ids.line1).pointerMove(126, 67, { ctrlKey: true })
expect(editor.getShape(ids.line1)).toMatchObject({ x: 120, y: 62 })
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.lines!)
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.getLines()!)
expect(gapLines).toHaveLength(1)
expect(pointLines).toHaveLength(1)
@ -828,9 +829,9 @@ describe('Snap-between behavior', () => {
// the midpoint is 125 and c is 10 wide so it should snap to 120 if we put it at 121
editor.pointerDown(55, 155, ids.line1).pointerMove(27, 126, { ctrlKey: true })
expect(editor.getShape(ids.line1)).toMatchObject({ x: 22, y: 120 })
expect(editor.snaps.lines?.length).toBe(1)
assertGaps(editor.snaps.lines![0])
const { gapLines } = getGapAndPointLines(editor.snaps.lines!)
expect(editor.snaps.getLines()?.length).toBe(1)
assertGaps(editor.snaps.getLines()![0])
const { gapLines } = getGapAndPointLines(editor.snaps.getLines()!)
expect(gapLines[0].gaps.length).toBe(2)
})
it('shows vertical snap points at the same time as vertical gaps', () => {
@ -862,7 +863,7 @@ describe('Snap-between behavior', () => {
editor.pointerDown(55, 155, ids.line1).pointerMove(6, 126, { ctrlKey: true })
expect(editor.getShape(ids.line1)).toMatchObject({ x: 0, y: 120 })
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.lines!)
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.getLines()!)
expect(gapLines).toHaveLength(1)
expect(pointLines).toHaveLength(1)
expect(gapLines[0].gaps.length).toBe(2)
@ -898,7 +899,7 @@ describe('Snap-between behavior', () => {
editor.pointerDown(55, 155, ids.line1).pointerMove(27, 126, { ctrlKey: true })
expect(editor.getShape(ids.line1)).toMatchObject({ x: 22, y: 120 })
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.lines!)
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.getLines()!)
expect(gapLines).toHaveLength(1)
expect(pointLines).toHaveLength(1)
expect(gapLines[0].gaps).toHaveLength(2)
@ -933,10 +934,10 @@ describe('Snap-between behavior', () => {
])
editor.pointerDown(5, 5, ids.boxE).pointerMove(101, 126, { ctrlKey: true })
expect(editor.getShape(ids.boxE)).toMatchObject({ x: 95, y: 120 })
expect(editor.snaps.lines?.length).toBe(2)
assertGaps(editor.snaps.lines![0])
assertGaps(editor.snaps.lines![1])
const { gapLines } = getGapAndPointLines(editor.snaps.lines!)
expect(editor.snaps.getLines()?.length).toBe(2)
assertGaps(editor.snaps.getLines()![0])
assertGaps(editor.snaps.getLines()![1])
const { gapLines } = getGapAndPointLines(editor.snaps.getLines()!)
expect(gapLines[0].gaps.length).toBe(2)
expect(gapLines[1].gaps.length).toBe(2)
})
@ -978,7 +979,7 @@ describe('Snap-between behavior', () => {
editor.pointerDown(5, 5, ids.boxX).pointerMove(46, 46, { ctrlKey: true })
expect(editor.getShape(ids.boxX)).toMatchObject({ x: 40, y: 40 })
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.lines!)
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.getLines()!)
expect(gapLines).toHaveLength(2)
expect(gapLines[0].gaps).toHaveLength(4)
expect(gapLines[1].gaps).toHaveLength(4)
@ -1011,7 +1012,7 @@ describe('Snap-between behavior', () => {
editor.pointerDown(65, 25, ids.boxX).pointerMove(16, 25, { ctrlKey: true })
expect(editor.getShape(ids.boxX)).toMatchObject({ x: 0, y: 20 })
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.lines!)
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.getLines()!)
expect(gapLines).toHaveLength(2)
expect(gapLines[0].gaps).toHaveLength(2)
expect(gapLines[1].gaps).toHaveLength(2)
@ -1048,7 +1049,7 @@ describe('Snap-between behavior', () => {
editor.pointerDown(50, 55, ids.boxX).pointerMove(51, 66, { ctrlKey: true })
expect(editor.getShape(ids.boxX)).toMatchObject({ x: 1, y: 61 })
expect(editor.snaps.lines?.length).toBe(0)
expect(editor.snaps.getLines()?.length).toBe(0)
})
it('should work if the thing being dragged is a selection', () => {
@ -1076,7 +1077,7 @@ describe('Snap-between behavior', () => {
expect(editor.getShape(ids.line1)).toMatchObject({ x: 200, y: 21 })
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.lines!)
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.getLines()!)
expect(gapLines).toHaveLength(1)
expect(pointLines).toHaveLength(0)
@ -1115,7 +1116,7 @@ describe('Snap-next-to behavior', () => {
editor.pointerDown(5, 5, ids.boxX).pointerMove(6, 16, { ctrlKey: true })
expect(editor.getShape(ids.boxX)).toMatchObject({ x: 0, y: 10 })
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.lines!)
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.getLines()!)
expect(gapLines).toHaveLength(1)
expect(gapLines[0].gaps).toHaveLength(2)
@ -1151,7 +1152,7 @@ describe('Snap-next-to behavior', () => {
editor.pointerDown(5, 5, ids.boxX).pointerMove(6, 16, { ctrlKey: true })
expect(editor.getShape(ids.boxX)).toMatchObject({ x: 0, y: 10 })
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.lines!)
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.getLines()!)
expect(gapLines).toHaveLength(1)
expect(gapLines[0].gaps).toHaveLength(4)
@ -1178,7 +1179,7 @@ describe('Snap-next-to behavior', () => {
editor.pointerDown(105, 5, ids.boxX).pointerMove(106, 16, { ctrlKey: true })
expect(editor.getShape(ids.boxX)).toMatchObject({ x: 100, y: 10 })
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.lines!)
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.getLines()!)
expect(gapLines).toHaveLength(1)
expect(gapLines[0].gaps).toHaveLength(2)
@ -1212,7 +1213,7 @@ describe('Snap-next-to behavior', () => {
editor.pointerDown(205, 5, ids.boxX).pointerMove(206, 16, { ctrlKey: true })
expect(editor.getShape(ids.boxX)).toMatchObject({ x: 200, y: 10 })
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.lines!)
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.getLines()!)
expect(gapLines).toHaveLength(1)
expect(gapLines[0].gaps).toHaveLength(4)
@ -1238,7 +1239,7 @@ describe('Snap-next-to behavior', () => {
expect(editor.getShape(ids.boxX)).toMatchObject({ x: 10, y: 0 })
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.lines!)
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.getLines()!)
expect(gapLines).toHaveLength(1)
expect(gapLines[0].gaps).toHaveLength(2)
@ -1280,7 +1281,7 @@ describe('Snap-next-to behavior', () => {
expect(editor.getShape(ids.boxX)).toMatchObject({ x: 10, y: 0 })
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.lines!)
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.getLines()!)
expect(gapLines).toHaveLength(1)
expect(gapLines[0].gaps).toHaveLength(4)
@ -1307,7 +1308,7 @@ describe('Snap-next-to behavior', () => {
expect(editor.getShape(ids.boxX)).toMatchObject({ x: 10, y: 40 })
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.lines!)
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.getLines()!)
expect(gapLines).toHaveLength(1)
expect(gapLines[0].gaps).toHaveLength(2)
@ -1348,7 +1349,7 @@ describe('Snap-next-to behavior', () => {
expect(editor.getShape(ids.boxX)).toMatchObject({ x: 10, y: 80 })
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.lines!)
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.getLines()!)
expect(gapLines).toHaveLength(1)
expect(gapLines[0].gaps).toHaveLength(4)
@ -1382,7 +1383,7 @@ describe('Snap-next-to behavior', () => {
expect(editor.getShape(ids.boxD)).toMatchObject({ x: 200, y: 131 })
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.lines!)
const { gapLines, pointLines } = getGapAndPointLines(editor.snaps.getLines()!)
expect(gapLines).toHaveLength(1)
expect(pointLines).toHaveLength(0)
@ -1533,7 +1534,7 @@ describe('translating a shape with a child', () => {
editor.pointerDown(25, 25, ids.box1).pointerMove(50, 25, { ctrlKey: true })
expect(editor.snaps.lines?.length).toBe(0)
expect(editor.snaps.getLines()?.length).toBe(0)
expect(editor.getShape(ids.box1)).toMatchObject({
x: 25,
y: 0,
@ -1575,7 +1576,7 @@ describe('translating a shape with a bound shape', () => {
editor.pointerDown(50, 50, ids.box1).pointerMove(84, 110, { ctrlKey: true })
expect(editor.snaps.lines.length).toBe(0)
expect(editor.snaps.getLines().length).toBe(0)
})
it('should preserve arrow bindings', () => {