2023-04-25 11:01:25 +00:00
|
|
|
import {
|
tldraw zero - package shuffle (#1710)
This PR moves code between our packages so that:
- @tldraw/editor is a “core” library with the engine and canvas but no
shapes, tools, or other things
- @tldraw/tldraw contains everything particular to the experience we’ve
built for tldraw
At first look, this might seem like a step away from customization and
configuration, however I believe it greatly increases the configuration
potential of the @tldraw/editor while also providing a more accurate
reflection of what configuration options actually exist for
@tldraw/tldraw.
## Library changes
@tldraw/editor re-exports its dependencies and @tldraw/tldraw re-exports
@tldraw/editor.
- users of @tldraw/editor WITHOUT @tldraw/tldraw should almost always
only import things from @tldraw/editor.
- users of @tldraw/tldraw should almost always only import things from
@tldraw/tldraw.
- @tldraw/polyfills is merged into @tldraw/editor
- @tldraw/indices is merged into @tldraw/editor
- @tldraw/primitives is merged mostly into @tldraw/editor, partially
into @tldraw/tldraw
- @tldraw/file-format is merged into @tldraw/tldraw
- @tldraw/ui is merged into @tldraw/tldraw
Many (many) utils and other code is moved from the editor to tldraw. For
example, embeds now are entirely an feature of @tldraw/tldraw. The only
big chunk of code left in core is related to arrow handling.
## API Changes
The editor can now be used without tldraw's assets. We load them in
@tldraw/tldraw instead, so feel free to use whatever fonts or images or
whatever that you like with the editor.
All tools and shapes (except for the `Group` shape) are moved to
@tldraw/tldraw. This includes the `select` tool.
You should use the editor with at least one tool, however, so you now
also need to send in an `initialState` prop to the Editor /
<TldrawEditor> component indicating which state the editor should begin
in.
The `components` prop now also accepts `SelectionForeground`.
The complex selection component that we use for tldraw is moved to
@tldraw/tldraw. The default component is quite basic but can easily be
replaced via the `components` prop. We pass down our tldraw-flavored
SelectionFg via `components`.
Likewise with the `Scribble` component: the `DefaultScribble` no longer
uses our freehand tech and is a simple path instead. We pass down the
tldraw-flavored scribble via `components`.
The `ExternalContentManager` (`Editor.externalContentManager`) is
removed and replaced with a mapping of types to handlers.
- Register new content handlers with
`Editor.registerExternalContentHandler`.
- Register new asset creation handlers (for files and URLs) with
`Editor.registerExternalAssetHandler`
### Change Type
- [x] `major` — Breaking change
### Test Plan
- [x] Unit Tests
- [x] End to end tests
### Release Notes
- [@tldraw/editor] lots, wip
- [@tldraw/ui] gone, merged to tldraw/tldraw
- [@tldraw/polyfills] gone, merged to tldraw/editor
- [@tldraw/primitives] gone, merged to tldraw/editor / tldraw/tldraw
- [@tldraw/indices] gone, merged to tldraw/editor
- [@tldraw/file-format] gone, merged to tldraw/tldraw
---------
Co-authored-by: alex <alex@dytry.ch>
2023-07-17 21:22:34 +00:00
|
|
|
DRAG_DISTANCE,
|
2024-01-03 12:13:15 +00:00
|
|
|
Mat,
|
tldraw zero - package shuffle (#1710)
This PR moves code between our packages so that:
- @tldraw/editor is a “core” library with the engine and canvas but no
shapes, tools, or other things
- @tldraw/tldraw contains everything particular to the experience we’ve
built for tldraw
At first look, this might seem like a step away from customization and
configuration, however I believe it greatly increases the configuration
potential of the @tldraw/editor while also providing a more accurate
reflection of what configuration options actually exist for
@tldraw/tldraw.
## Library changes
@tldraw/editor re-exports its dependencies and @tldraw/tldraw re-exports
@tldraw/editor.
- users of @tldraw/editor WITHOUT @tldraw/tldraw should almost always
only import things from @tldraw/editor.
- users of @tldraw/tldraw should almost always only import things from
@tldraw/tldraw.
- @tldraw/polyfills is merged into @tldraw/editor
- @tldraw/indices is merged into @tldraw/editor
- @tldraw/primitives is merged mostly into @tldraw/editor, partially
into @tldraw/tldraw
- @tldraw/file-format is merged into @tldraw/tldraw
- @tldraw/ui is merged into @tldraw/tldraw
Many (many) utils and other code is moved from the editor to tldraw. For
example, embeds now are entirely an feature of @tldraw/tldraw. The only
big chunk of code left in core is related to arrow handling.
## API Changes
The editor can now be used without tldraw's assets. We load them in
@tldraw/tldraw instead, so feel free to use whatever fonts or images or
whatever that you like with the editor.
All tools and shapes (except for the `Group` shape) are moved to
@tldraw/tldraw. This includes the `select` tool.
You should use the editor with at least one tool, however, so you now
also need to send in an `initialState` prop to the Editor /
<TldrawEditor> component indicating which state the editor should begin
in.
The `components` prop now also accepts `SelectionForeground`.
The complex selection component that we use for tldraw is moved to
@tldraw/tldraw. The default component is quite basic but can easily be
replaced via the `components` prop. We pass down our tldraw-flavored
SelectionFg via `components`.
Likewise with the `Scribble` component: the `DefaultScribble` no longer
uses our freehand tech and is a simple path instead. We pass down the
tldraw-flavored scribble via `components`.
The `ExternalContentManager` (`Editor.externalContentManager`) is
removed and replaced with a mapping of types to handlers.
- Register new content handlers with
`Editor.registerExternalContentHandler`.
- Register new asset creation handlers (for files and URLs) with
`Editor.registerExternalAssetHandler`
### Change Type
- [x] `major` — Breaking change
### Test Plan
- [x] Unit Tests
- [x] End to end tests
### Release Notes
- [@tldraw/editor] lots, wip
- [@tldraw/ui] gone, merged to tldraw/tldraw
- [@tldraw/polyfills] gone, merged to tldraw/editor
- [@tldraw/primitives] gone, merged to tldraw/editor / tldraw/tldraw
- [@tldraw/indices] gone, merged to tldraw/editor
- [@tldraw/file-format] gone, merged to tldraw/tldraw
---------
Co-authored-by: alex <alex@dytry.ch>
2023-07-17 21:22:34 +00:00
|
|
|
StateNode,
|
2023-06-16 10:33:47 +00:00
|
|
|
TLDefaultSizeStyle,
|
2023-04-25 11:01:25 +00:00
|
|
|
TLDrawShape,
|
|
|
|
TLDrawShapeSegment,
|
tldraw zero - package shuffle (#1710)
This PR moves code between our packages so that:
- @tldraw/editor is a “core” library with the engine and canvas but no
shapes, tools, or other things
- @tldraw/tldraw contains everything particular to the experience we’ve
built for tldraw
At first look, this might seem like a step away from customization and
configuration, however I believe it greatly increases the configuration
potential of the @tldraw/editor while also providing a more accurate
reflection of what configuration options actually exist for
@tldraw/tldraw.
## Library changes
@tldraw/editor re-exports its dependencies and @tldraw/tldraw re-exports
@tldraw/editor.
- users of @tldraw/editor WITHOUT @tldraw/tldraw should almost always
only import things from @tldraw/editor.
- users of @tldraw/tldraw should almost always only import things from
@tldraw/tldraw.
- @tldraw/polyfills is merged into @tldraw/editor
- @tldraw/indices is merged into @tldraw/editor
- @tldraw/primitives is merged mostly into @tldraw/editor, partially
into @tldraw/tldraw
- @tldraw/file-format is merged into @tldraw/tldraw
- @tldraw/ui is merged into @tldraw/tldraw
Many (many) utils and other code is moved from the editor to tldraw. For
example, embeds now are entirely an feature of @tldraw/tldraw. The only
big chunk of code left in core is related to arrow handling.
## API Changes
The editor can now be used without tldraw's assets. We load them in
@tldraw/tldraw instead, so feel free to use whatever fonts or images or
whatever that you like with the editor.
All tools and shapes (except for the `Group` shape) are moved to
@tldraw/tldraw. This includes the `select` tool.
You should use the editor with at least one tool, however, so you now
also need to send in an `initialState` prop to the Editor /
<TldrawEditor> component indicating which state the editor should begin
in.
The `components` prop now also accepts `SelectionForeground`.
The complex selection component that we use for tldraw is moved to
@tldraw/tldraw. The default component is quite basic but can easily be
replaced via the `components` prop. We pass down our tldraw-flavored
SelectionFg via `components`.
Likewise with the `Scribble` component: the `DefaultScribble` no longer
uses our freehand tech and is a simple path instead. We pass down the
tldraw-flavored scribble via `components`.
The `ExternalContentManager` (`Editor.externalContentManager`) is
removed and replaced with a mapping of types to handlers.
- Register new content handlers with
`Editor.registerExternalContentHandler`.
- Register new asset creation handlers (for files and URLs) with
`Editor.registerExternalAssetHandler`
### Change Type
- [x] `major` — Breaking change
### Test Plan
- [x] Unit Tests
- [x] End to end tests
### Release Notes
- [@tldraw/editor] lots, wip
- [@tldraw/ui] gone, merged to tldraw/tldraw
- [@tldraw/polyfills] gone, merged to tldraw/editor
- [@tldraw/primitives] gone, merged to tldraw/editor / tldraw/tldraw
- [@tldraw/indices] gone, merged to tldraw/editor
- [@tldraw/file-format] gone, merged to tldraw/tldraw
---------
Co-authored-by: alex <alex@dytry.ch>
2023-07-17 21:22:34 +00:00
|
|
|
TLEventHandlers,
|
2023-06-01 12:46:13 +00:00
|
|
|
TLHighlightShape,
|
tldraw zero - package shuffle (#1710)
This PR moves code between our packages so that:
- @tldraw/editor is a “core” library with the engine and canvas but no
shapes, tools, or other things
- @tldraw/tldraw contains everything particular to the experience we’ve
built for tldraw
At first look, this might seem like a step away from customization and
configuration, however I believe it greatly increases the configuration
potential of the @tldraw/editor while also providing a more accurate
reflection of what configuration options actually exist for
@tldraw/tldraw.
## Library changes
@tldraw/editor re-exports its dependencies and @tldraw/tldraw re-exports
@tldraw/editor.
- users of @tldraw/editor WITHOUT @tldraw/tldraw should almost always
only import things from @tldraw/editor.
- users of @tldraw/tldraw should almost always only import things from
@tldraw/tldraw.
- @tldraw/polyfills is merged into @tldraw/editor
- @tldraw/indices is merged into @tldraw/editor
- @tldraw/primitives is merged mostly into @tldraw/editor, partially
into @tldraw/tldraw
- @tldraw/file-format is merged into @tldraw/tldraw
- @tldraw/ui is merged into @tldraw/tldraw
Many (many) utils and other code is moved from the editor to tldraw. For
example, embeds now are entirely an feature of @tldraw/tldraw. The only
big chunk of code left in core is related to arrow handling.
## API Changes
The editor can now be used without tldraw's assets. We load them in
@tldraw/tldraw instead, so feel free to use whatever fonts or images or
whatever that you like with the editor.
All tools and shapes (except for the `Group` shape) are moved to
@tldraw/tldraw. This includes the `select` tool.
You should use the editor with at least one tool, however, so you now
also need to send in an `initialState` prop to the Editor /
<TldrawEditor> component indicating which state the editor should begin
in.
The `components` prop now also accepts `SelectionForeground`.
The complex selection component that we use for tldraw is moved to
@tldraw/tldraw. The default component is quite basic but can easily be
replaced via the `components` prop. We pass down our tldraw-flavored
SelectionFg via `components`.
Likewise with the `Scribble` component: the `DefaultScribble` no longer
uses our freehand tech and is a simple path instead. We pass down the
tldraw-flavored scribble via `components`.
The `ExternalContentManager` (`Editor.externalContentManager`) is
removed and replaced with a mapping of types to handlers.
- Register new content handlers with
`Editor.registerExternalContentHandler`.
- Register new asset creation handlers (for files and URLs) with
`Editor.registerExternalAssetHandler`
### Change Type
- [x] `major` — Breaking change
### Test Plan
- [x] Unit Tests
- [x] End to end tests
### Release Notes
- [@tldraw/editor] lots, wip
- [@tldraw/ui] gone, merged to tldraw/tldraw
- [@tldraw/polyfills] gone, merged to tldraw/editor
- [@tldraw/primitives] gone, merged to tldraw/editor / tldraw/tldraw
- [@tldraw/indices] gone, merged to tldraw/editor
- [@tldraw/file-format] gone, merged to tldraw/tldraw
---------
Co-authored-by: alex <alex@dytry.ch>
2023-07-17 21:22:34 +00:00
|
|
|
TLPointerEventInfo,
|
2023-06-13 18:02:17 +00:00
|
|
|
TLShapePartial,
|
2024-01-03 12:13:15 +00:00
|
|
|
Vec,
|
|
|
|
VecModel,
|
tldraw zero - package shuffle (#1710)
This PR moves code between our packages so that:
- @tldraw/editor is a “core” library with the engine and canvas but no
shapes, tools, or other things
- @tldraw/tldraw contains everything particular to the experience we’ve
built for tldraw
At first look, this might seem like a step away from customization and
configuration, however I believe it greatly increases the configuration
potential of the @tldraw/editor while also providing a more accurate
reflection of what configuration options actually exist for
@tldraw/tldraw.
## Library changes
@tldraw/editor re-exports its dependencies and @tldraw/tldraw re-exports
@tldraw/editor.
- users of @tldraw/editor WITHOUT @tldraw/tldraw should almost always
only import things from @tldraw/editor.
- users of @tldraw/tldraw should almost always only import things from
@tldraw/tldraw.
- @tldraw/polyfills is merged into @tldraw/editor
- @tldraw/indices is merged into @tldraw/editor
- @tldraw/primitives is merged mostly into @tldraw/editor, partially
into @tldraw/tldraw
- @tldraw/file-format is merged into @tldraw/tldraw
- @tldraw/ui is merged into @tldraw/tldraw
Many (many) utils and other code is moved from the editor to tldraw. For
example, embeds now are entirely an feature of @tldraw/tldraw. The only
big chunk of code left in core is related to arrow handling.
## API Changes
The editor can now be used without tldraw's assets. We load them in
@tldraw/tldraw instead, so feel free to use whatever fonts or images or
whatever that you like with the editor.
All tools and shapes (except for the `Group` shape) are moved to
@tldraw/tldraw. This includes the `select` tool.
You should use the editor with at least one tool, however, so you now
also need to send in an `initialState` prop to the Editor /
<TldrawEditor> component indicating which state the editor should begin
in.
The `components` prop now also accepts `SelectionForeground`.
The complex selection component that we use for tldraw is moved to
@tldraw/tldraw. The default component is quite basic but can easily be
replaced via the `components` prop. We pass down our tldraw-flavored
SelectionFg via `components`.
Likewise with the `Scribble` component: the `DefaultScribble` no longer
uses our freehand tech and is a simple path instead. We pass down the
tldraw-flavored scribble via `components`.
The `ExternalContentManager` (`Editor.externalContentManager`) is
removed and replaced with a mapping of types to handlers.
- Register new content handlers with
`Editor.registerExternalContentHandler`.
- Register new asset creation handlers (for files and URLs) with
`Editor.registerExternalAssetHandler`
### Change Type
- [x] `major` — Breaking change
### Test Plan
- [x] Unit Tests
- [x] End to end tests
### Release Notes
- [@tldraw/editor] lots, wip
- [@tldraw/ui] gone, merged to tldraw/tldraw
- [@tldraw/polyfills] gone, merged to tldraw/editor
- [@tldraw/primitives] gone, merged to tldraw/editor / tldraw/tldraw
- [@tldraw/indices] gone, merged to tldraw/editor
- [@tldraw/file-format] gone, merged to tldraw/tldraw
---------
Co-authored-by: alex <alex@dytry.ch>
2023-07-17 21:22:34 +00:00
|
|
|
createShapeId,
|
|
|
|
last,
|
|
|
|
snapAngle,
|
2024-02-24 20:02:17 +00:00
|
|
|
structuredClone,
|
tldraw zero - package shuffle (#1710)
This PR moves code between our packages so that:
- @tldraw/editor is a “core” library with the engine and canvas but no
shapes, tools, or other things
- @tldraw/tldraw contains everything particular to the experience we’ve
built for tldraw
At first look, this might seem like a step away from customization and
configuration, however I believe it greatly increases the configuration
potential of the @tldraw/editor while also providing a more accurate
reflection of what configuration options actually exist for
@tldraw/tldraw.
## Library changes
@tldraw/editor re-exports its dependencies and @tldraw/tldraw re-exports
@tldraw/editor.
- users of @tldraw/editor WITHOUT @tldraw/tldraw should almost always
only import things from @tldraw/editor.
- users of @tldraw/tldraw should almost always only import things from
@tldraw/tldraw.
- @tldraw/polyfills is merged into @tldraw/editor
- @tldraw/indices is merged into @tldraw/editor
- @tldraw/primitives is merged mostly into @tldraw/editor, partially
into @tldraw/tldraw
- @tldraw/file-format is merged into @tldraw/tldraw
- @tldraw/ui is merged into @tldraw/tldraw
Many (many) utils and other code is moved from the editor to tldraw. For
example, embeds now are entirely an feature of @tldraw/tldraw. The only
big chunk of code left in core is related to arrow handling.
## API Changes
The editor can now be used without tldraw's assets. We load them in
@tldraw/tldraw instead, so feel free to use whatever fonts or images or
whatever that you like with the editor.
All tools and shapes (except for the `Group` shape) are moved to
@tldraw/tldraw. This includes the `select` tool.
You should use the editor with at least one tool, however, so you now
also need to send in an `initialState` prop to the Editor /
<TldrawEditor> component indicating which state the editor should begin
in.
The `components` prop now also accepts `SelectionForeground`.
The complex selection component that we use for tldraw is moved to
@tldraw/tldraw. The default component is quite basic but can easily be
replaced via the `components` prop. We pass down our tldraw-flavored
SelectionFg via `components`.
Likewise with the `Scribble` component: the `DefaultScribble` no longer
uses our freehand tech and is a simple path instead. We pass down the
tldraw-flavored scribble via `components`.
The `ExternalContentManager` (`Editor.externalContentManager`) is
removed and replaced with a mapping of types to handlers.
- Register new content handlers with
`Editor.registerExternalContentHandler`.
- Register new asset creation handlers (for files and URLs) with
`Editor.registerExternalAssetHandler`
### Change Type
- [x] `major` — Breaking change
### Test Plan
- [x] Unit Tests
- [x] End to end tests
### Release Notes
- [@tldraw/editor] lots, wip
- [@tldraw/ui] gone, merged to tldraw/tldraw
- [@tldraw/polyfills] gone, merged to tldraw/editor
- [@tldraw/primitives] gone, merged to tldraw/editor / tldraw/tldraw
- [@tldraw/indices] gone, merged to tldraw/editor
- [@tldraw/file-format] gone, merged to tldraw/tldraw
---------
Co-authored-by: alex <alex@dytry.ch>
2023-07-17 21:22:34 +00:00
|
|
|
toFixed,
|
|
|
|
uniqueId,
|
|
|
|
} from '@tldraw/editor'
|
2023-06-16 10:33:47 +00:00
|
|
|
import { STROKE_SIZES } from '../../shared/default-shape-constants'
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2023-06-01 12:46:13 +00:00
|
|
|
type DrawableShape = TLDrawShape | TLHighlightShape
|
|
|
|
|
2023-04-25 11:01:25 +00:00
|
|
|
export class Drawing extends StateNode {
|
|
|
|
static override id = 'drawing'
|
|
|
|
|
|
|
|
info = {} as TLPointerEventInfo
|
|
|
|
|
2023-06-01 12:46:13 +00:00
|
|
|
initialShape?: DrawableShape
|
|
|
|
|
tldraw zero - package shuffle (#1710)
This PR moves code between our packages so that:
- @tldraw/editor is a “core” library with the engine and canvas but no
shapes, tools, or other things
- @tldraw/tldraw contains everything particular to the experience we’ve
built for tldraw
At first look, this might seem like a step away from customization and
configuration, however I believe it greatly increases the configuration
potential of the @tldraw/editor while also providing a more accurate
reflection of what configuration options actually exist for
@tldraw/tldraw.
## Library changes
@tldraw/editor re-exports its dependencies and @tldraw/tldraw re-exports
@tldraw/editor.
- users of @tldraw/editor WITHOUT @tldraw/tldraw should almost always
only import things from @tldraw/editor.
- users of @tldraw/tldraw should almost always only import things from
@tldraw/tldraw.
- @tldraw/polyfills is merged into @tldraw/editor
- @tldraw/indices is merged into @tldraw/editor
- @tldraw/primitives is merged mostly into @tldraw/editor, partially
into @tldraw/tldraw
- @tldraw/file-format is merged into @tldraw/tldraw
- @tldraw/ui is merged into @tldraw/tldraw
Many (many) utils and other code is moved from the editor to tldraw. For
example, embeds now are entirely an feature of @tldraw/tldraw. The only
big chunk of code left in core is related to arrow handling.
## API Changes
The editor can now be used without tldraw's assets. We load them in
@tldraw/tldraw instead, so feel free to use whatever fonts or images or
whatever that you like with the editor.
All tools and shapes (except for the `Group` shape) are moved to
@tldraw/tldraw. This includes the `select` tool.
You should use the editor with at least one tool, however, so you now
also need to send in an `initialState` prop to the Editor /
<TldrawEditor> component indicating which state the editor should begin
in.
The `components` prop now also accepts `SelectionForeground`.
The complex selection component that we use for tldraw is moved to
@tldraw/tldraw. The default component is quite basic but can easily be
replaced via the `components` prop. We pass down our tldraw-flavored
SelectionFg via `components`.
Likewise with the `Scribble` component: the `DefaultScribble` no longer
uses our freehand tech and is a simple path instead. We pass down the
tldraw-flavored scribble via `components`.
The `ExternalContentManager` (`Editor.externalContentManager`) is
removed and replaced with a mapping of types to handlers.
- Register new content handlers with
`Editor.registerExternalContentHandler`.
- Register new asset creation handlers (for files and URLs) with
`Editor.registerExternalAssetHandler`
### Change Type
- [x] `major` — Breaking change
### Test Plan
- [x] Unit Tests
- [x] End to end tests
### Release Notes
- [@tldraw/editor] lots, wip
- [@tldraw/ui] gone, merged to tldraw/tldraw
- [@tldraw/polyfills] gone, merged to tldraw/editor
- [@tldraw/primitives] gone, merged to tldraw/editor / tldraw/tldraw
- [@tldraw/indices] gone, merged to tldraw/editor
- [@tldraw/file-format] gone, merged to tldraw/tldraw
---------
Co-authored-by: alex <alex@dytry.ch>
2023-07-17 21:22:34 +00:00
|
|
|
override shapeType = this.parent.id === 'highlight' ? ('highlight' as const) : ('draw' as const)
|
2023-06-16 10:33:47 +00:00
|
|
|
|
|
|
|
util = this.editor.getShapeUtil(this.shapeType)
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
isPen = false
|
|
|
|
|
|
|
|
segmentMode = 'free' as 'free' | 'straight' | 'starting_straight' | 'starting_free'
|
|
|
|
|
|
|
|
didJustShiftClickToExtendPreviousShapeLine = false
|
|
|
|
|
2024-01-03 12:13:15 +00:00
|
|
|
pagePointWhereCurrentSegmentChanged = {} as Vec
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2024-01-03 12:13:15 +00:00
|
|
|
pagePointWhereNextSegmentChanged = null as Vec | null
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2024-01-03 12:13:15 +00:00
|
|
|
lastRecordedPoint = {} as Vec
|
2023-04-25 11:01:25 +00:00
|
|
|
mergeNextPoint = false
|
|
|
|
currentLineLength = 0
|
|
|
|
|
|
|
|
canDraw = false
|
|
|
|
|
2023-09-18 10:50:15 +00:00
|
|
|
markId = null as null | string
|
|
|
|
|
tldraw zero - package shuffle (#1710)
This PR moves code between our packages so that:
- @tldraw/editor is a “core” library with the engine and canvas but no
shapes, tools, or other things
- @tldraw/tldraw contains everything particular to the experience we’ve
built for tldraw
At first look, this might seem like a step away from customization and
configuration, however I believe it greatly increases the configuration
potential of the @tldraw/editor while also providing a more accurate
reflection of what configuration options actually exist for
@tldraw/tldraw.
## Library changes
@tldraw/editor re-exports its dependencies and @tldraw/tldraw re-exports
@tldraw/editor.
- users of @tldraw/editor WITHOUT @tldraw/tldraw should almost always
only import things from @tldraw/editor.
- users of @tldraw/tldraw should almost always only import things from
@tldraw/tldraw.
- @tldraw/polyfills is merged into @tldraw/editor
- @tldraw/indices is merged into @tldraw/editor
- @tldraw/primitives is merged mostly into @tldraw/editor, partially
into @tldraw/tldraw
- @tldraw/file-format is merged into @tldraw/tldraw
- @tldraw/ui is merged into @tldraw/tldraw
Many (many) utils and other code is moved from the editor to tldraw. For
example, embeds now are entirely an feature of @tldraw/tldraw. The only
big chunk of code left in core is related to arrow handling.
## API Changes
The editor can now be used without tldraw's assets. We load them in
@tldraw/tldraw instead, so feel free to use whatever fonts or images or
whatever that you like with the editor.
All tools and shapes (except for the `Group` shape) are moved to
@tldraw/tldraw. This includes the `select` tool.
You should use the editor with at least one tool, however, so you now
also need to send in an `initialState` prop to the Editor /
<TldrawEditor> component indicating which state the editor should begin
in.
The `components` prop now also accepts `SelectionForeground`.
The complex selection component that we use for tldraw is moved to
@tldraw/tldraw. The default component is quite basic but can easily be
replaced via the `components` prop. We pass down our tldraw-flavored
SelectionFg via `components`.
Likewise with the `Scribble` component: the `DefaultScribble` no longer
uses our freehand tech and is a simple path instead. We pass down the
tldraw-flavored scribble via `components`.
The `ExternalContentManager` (`Editor.externalContentManager`) is
removed and replaced with a mapping of types to handlers.
- Register new content handlers with
`Editor.registerExternalContentHandler`.
- Register new asset creation handlers (for files and URLs) with
`Editor.registerExternalAssetHandler`
### Change Type
- [x] `major` — Breaking change
### Test Plan
- [x] Unit Tests
- [x] End to end tests
### Release Notes
- [@tldraw/editor] lots, wip
- [@tldraw/ui] gone, merged to tldraw/tldraw
- [@tldraw/polyfills] gone, merged to tldraw/editor
- [@tldraw/primitives] gone, merged to tldraw/editor / tldraw/tldraw
- [@tldraw/indices] gone, merged to tldraw/editor
- [@tldraw/file-format] gone, merged to tldraw/tldraw
---------
Co-authored-by: alex <alex@dytry.ch>
2023-07-17 21:22:34 +00:00
|
|
|
override onEnter = (info: TLPointerEventInfo) => {
|
2023-09-18 10:50:15 +00:00
|
|
|
this.markId = null
|
2023-04-25 11:01:25 +00:00
|
|
|
this.info = info
|
2023-11-13 12:42:07 +00:00
|
|
|
this.canDraw = !this.editor.getIsMenuOpen()
|
2023-06-02 15:21:45 +00:00
|
|
|
this.lastRecordedPoint = this.editor.inputs.currentPagePoint.clone()
|
2023-04-25 11:01:25 +00:00
|
|
|
if (this.canDraw) {
|
|
|
|
this.startShape()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
tldraw zero - package shuffle (#1710)
This PR moves code between our packages so that:
- @tldraw/editor is a “core” library with the engine and canvas but no
shapes, tools, or other things
- @tldraw/tldraw contains everything particular to the experience we’ve
built for tldraw
At first look, this might seem like a step away from customization and
configuration, however I believe it greatly increases the configuration
potential of the @tldraw/editor while also providing a more accurate
reflection of what configuration options actually exist for
@tldraw/tldraw.
## Library changes
@tldraw/editor re-exports its dependencies and @tldraw/tldraw re-exports
@tldraw/editor.
- users of @tldraw/editor WITHOUT @tldraw/tldraw should almost always
only import things from @tldraw/editor.
- users of @tldraw/tldraw should almost always only import things from
@tldraw/tldraw.
- @tldraw/polyfills is merged into @tldraw/editor
- @tldraw/indices is merged into @tldraw/editor
- @tldraw/primitives is merged mostly into @tldraw/editor, partially
into @tldraw/tldraw
- @tldraw/file-format is merged into @tldraw/tldraw
- @tldraw/ui is merged into @tldraw/tldraw
Many (many) utils and other code is moved from the editor to tldraw. For
example, embeds now are entirely an feature of @tldraw/tldraw. The only
big chunk of code left in core is related to arrow handling.
## API Changes
The editor can now be used without tldraw's assets. We load them in
@tldraw/tldraw instead, so feel free to use whatever fonts or images or
whatever that you like with the editor.
All tools and shapes (except for the `Group` shape) are moved to
@tldraw/tldraw. This includes the `select` tool.
You should use the editor with at least one tool, however, so you now
also need to send in an `initialState` prop to the Editor /
<TldrawEditor> component indicating which state the editor should begin
in.
The `components` prop now also accepts `SelectionForeground`.
The complex selection component that we use for tldraw is moved to
@tldraw/tldraw. The default component is quite basic but can easily be
replaced via the `components` prop. We pass down our tldraw-flavored
SelectionFg via `components`.
Likewise with the `Scribble` component: the `DefaultScribble` no longer
uses our freehand tech and is a simple path instead. We pass down the
tldraw-flavored scribble via `components`.
The `ExternalContentManager` (`Editor.externalContentManager`) is
removed and replaced with a mapping of types to handlers.
- Register new content handlers with
`Editor.registerExternalContentHandler`.
- Register new asset creation handlers (for files and URLs) with
`Editor.registerExternalAssetHandler`
### Change Type
- [x] `major` — Breaking change
### Test Plan
- [x] Unit Tests
- [x] End to end tests
### Release Notes
- [@tldraw/editor] lots, wip
- [@tldraw/ui] gone, merged to tldraw/tldraw
- [@tldraw/polyfills] gone, merged to tldraw/editor
- [@tldraw/primitives] gone, merged to tldraw/editor / tldraw/tldraw
- [@tldraw/indices] gone, merged to tldraw/editor
- [@tldraw/file-format] gone, merged to tldraw/tldraw
---------
Co-authored-by: alex <alex@dytry.ch>
2023-07-17 21:22:34 +00:00
|
|
|
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
Input buffering (#3223)
This PR buffs input events.
## The story so far
In the olde days, we throttled events from the canvas events hook so
that a pointer event would only be sent every 1/60th of a second. This
was fine but made drawing on the iPad / 120FPS displays a little sad.
Then we removed this throttle. It seemed fine! Drawing at 120FPS was
great. We improved some rendering speeds and tightened some loops so
that the engine could keep up with 2x the number of points in a line.
Then we started noticing that iPads and other screens could start
choking on events as it received new inputs and tried to process and
render inputs while still recovering from a previous dropped frame. Even
worse, on iPad the work of rendering at 120FPS was causing the browser
to throttle the app after some sustained drawing. Yikes!
### Batching
I did an experimental PR (#3180) to bring back batching but do it in the
editor instead. What we would do is: rather than immediately processing
an event when we get it, we would instead put the event into a buffer.
On the next 60FPS tick, we would flush the buffer and process all of the
events. We'd have them all in the same transaction so that the app would
only render once.
### Render batching?
We then tried batching the renders, so that the app would only ever
render once per (next) frame. This added a bunch of complexity around
events that needed to happen synchronously, such as writing text in a
text field. Some inputs could "lag" in a way familiar to anyone who's
tried to update an input's state asynchronously. So we backed out of
this.
### Coalescing?
Another idea from @ds300 was to "coalesce" the events. This would be
useful because, while some interactions like drawing would require the
in-between frames in order to avoid data loss, most interactions (like
resizing) didn't actually need the in-between frames, they could just
use the last input of a given type.
Coalescing turned out to be trickier than we thought, though. Often a
state node required information from elsewhere in the app when
processing an event (such as camera position or page point, which is
derived from the camera position), and so the coalesced events would
need to also include this information or else the handlers wouldn't work
the way they should when processing the "final" event during a tick.
So we backed out of the coalescing strategy for now. Here's the [PR that
removes](https://github.com/tldraw/tldraw/pull/3223/commits/937469d69d4474fe9d1ff98604acb8f55a49f3fa)
it.
### Let's just buffer the fuckers
So this PR now should only include input buffering.
I think there are ways to achieve the same coalescing-like results
through the state nodes, which could gather information during the
`onPointerMove` handler and then actually make changes during the
`onTick` handler, so that the changes are only done as many time as
necessary. This should help with e.g. resizing lots of shapes at once.
But first let's land the buffering!
---
Mitja's original text:
This PR builds on top of Steve's [experiment
PR](https://github.com/tldraw/tldraw/pull/3180) here. It also adds event
coalescing for [`pointerMove`
events](https://github.com/tldraw/tldraw/blob/mitja/input-buffering/packages/editor/src/lib/editor/Editor.ts#L8364-L8368).
The API is [somewhat similar
](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/getCoalescedEvents)
to `getCoalescedEvent`. In `StateNodes` we register an `onPointerMove`
handler. When the event happens it gets called with the event `info`.
There's now an additional field on `TLMovePointerEvent` called
`coalescedInfo` which includes all the events. It's then on the user to
process all of these.
I decided on this API since it allows us to only expose one event
handler, but it still gives the users access to all events if they need
them.
We would otherwise either need to:
- Expose two events (coalesced and non-coalesced one and complicate the
api) so that state nodes like Resizing would not be triggered for each
pointer move.
- Offer some methods on the editor that would allow use to get the
coalesced information. Then the nodes that need that info could request
it. I [tried
this](https://github.com/tldraw/tldraw/pull/3223/commits/9ad973da3aa287e7974067ac923193530d29c188#diff-32f1de9a5a9ec72aa49a8d18a237fbfff301610f4689a4af6b37f47af435aafcR67),
but it didn't feel good.
This also complicated the editor inputs. The events need to store
information about the event (like the mouse position when the event
happened for `onPointerMove`). But we cannot immediately update inputs
when the event happens. To make this work for `pointerMove` events I've
added `pagePoint`. It's
[calculated](https://github.com/tldraw/tldraw/pull/3223/files#diff-980beb0aa0ee9aa6d1cd386cef3dc05a500c030638ffb58d45fd11b79126103fR71)
when the event triggers and then consumers can get it straight from the
event (like
[Drawing](https://github.com/tldraw/tldraw/pull/3223/files#diff-32f1de9a5a9ec72aa49a8d18a237fbfff301610f4689a4af6b37f47af435aafcR104)).
### Change Type
<!-- ❗ Please select a 'Scope' label ❗️ -->
- [x] `sdk` — Changes the tldraw SDK
- [ ] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff
<!-- ❗ Please select a 'Type' label ❗️ -->
- [ ] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [x] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know
### Test Plan
1. Add a step-by-step description of how to test your PR here.
4.
- [ ] Unit Tests
- [ ] End to end tests
### Release Notes
- Add a brief release note for your PR here.
---------
Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
2024-04-02 14:29:14 +00:00
|
|
|
const { inputs } = this.editor
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
if (this.isPen !== inputs.isPen) {
|
|
|
|
// The user made a palm gesture before starting a pen gesture;
|
|
|
|
// ideally we'd start the new shape here but we could also just bail
|
|
|
|
// as the next interaction will work correctly
|
2023-10-02 09:46:59 +00:00
|
|
|
if (this.markId) {
|
|
|
|
this.editor.bailToMark(this.markId)
|
|
|
|
this.startShape()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// If we came in from a menu but have no started dragging...
|
|
|
|
if (!this.canDraw && inputs.isDragging) {
|
|
|
|
this.startShape()
|
|
|
|
this.canDraw = true // bad name
|
|
|
|
}
|
2023-04-25 11:01:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this.canDraw) {
|
|
|
|
if (inputs.isPen) {
|
2024-03-11 13:17:31 +00:00
|
|
|
// Don't update the shape if we haven't moved far enough from the last time we recorded a point
|
2023-06-02 15:21:45 +00:00
|
|
|
if (
|
2024-01-03 12:13:15 +00:00
|
|
|
Vec.Dist(inputs.currentPagePoint, this.lastRecordedPoint) >=
|
2023-11-14 11:57:43 +00:00
|
|
|
1 / this.editor.getZoomLevel()
|
2023-06-02 15:21:45 +00:00
|
|
|
) {
|
2023-04-25 11:01:25 +00:00
|
|
|
this.lastRecordedPoint = inputs.currentPagePoint.clone()
|
|
|
|
this.mergeNextPoint = false
|
|
|
|
} else {
|
|
|
|
this.mergeNextPoint = true
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.mergeNextPoint = false
|
|
|
|
}
|
|
|
|
|
2024-04-21 11:46:35 +00:00
|
|
|
this.updateDrawingShape()
|
2023-04-25 11:01:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
tldraw zero - package shuffle (#1710)
This PR moves code between our packages so that:
- @tldraw/editor is a “core” library with the engine and canvas but no
shapes, tools, or other things
- @tldraw/tldraw contains everything particular to the experience we’ve
built for tldraw
At first look, this might seem like a step away from customization and
configuration, however I believe it greatly increases the configuration
potential of the @tldraw/editor while also providing a more accurate
reflection of what configuration options actually exist for
@tldraw/tldraw.
## Library changes
@tldraw/editor re-exports its dependencies and @tldraw/tldraw re-exports
@tldraw/editor.
- users of @tldraw/editor WITHOUT @tldraw/tldraw should almost always
only import things from @tldraw/editor.
- users of @tldraw/tldraw should almost always only import things from
@tldraw/tldraw.
- @tldraw/polyfills is merged into @tldraw/editor
- @tldraw/indices is merged into @tldraw/editor
- @tldraw/primitives is merged mostly into @tldraw/editor, partially
into @tldraw/tldraw
- @tldraw/file-format is merged into @tldraw/tldraw
- @tldraw/ui is merged into @tldraw/tldraw
Many (many) utils and other code is moved from the editor to tldraw. For
example, embeds now are entirely an feature of @tldraw/tldraw. The only
big chunk of code left in core is related to arrow handling.
## API Changes
The editor can now be used without tldraw's assets. We load them in
@tldraw/tldraw instead, so feel free to use whatever fonts or images or
whatever that you like with the editor.
All tools and shapes (except for the `Group` shape) are moved to
@tldraw/tldraw. This includes the `select` tool.
You should use the editor with at least one tool, however, so you now
also need to send in an `initialState` prop to the Editor /
<TldrawEditor> component indicating which state the editor should begin
in.
The `components` prop now also accepts `SelectionForeground`.
The complex selection component that we use for tldraw is moved to
@tldraw/tldraw. The default component is quite basic but can easily be
replaced via the `components` prop. We pass down our tldraw-flavored
SelectionFg via `components`.
Likewise with the `Scribble` component: the `DefaultScribble` no longer
uses our freehand tech and is a simple path instead. We pass down the
tldraw-flavored scribble via `components`.
The `ExternalContentManager` (`Editor.externalContentManager`) is
removed and replaced with a mapping of types to handlers.
- Register new content handlers with
`Editor.registerExternalContentHandler`.
- Register new asset creation handlers (for files and URLs) with
`Editor.registerExternalAssetHandler`
### Change Type
- [x] `major` — Breaking change
### Test Plan
- [x] Unit Tests
- [x] End to end tests
### Release Notes
- [@tldraw/editor] lots, wip
- [@tldraw/ui] gone, merged to tldraw/tldraw
- [@tldraw/polyfills] gone, merged to tldraw/editor
- [@tldraw/primitives] gone, merged to tldraw/editor / tldraw/tldraw
- [@tldraw/indices] gone, merged to tldraw/editor
- [@tldraw/file-format] gone, merged to tldraw/tldraw
---------
Co-authored-by: alex <alex@dytry.ch>
2023-07-17 21:22:34 +00:00
|
|
|
override onKeyDown: TLEventHandlers['onKeyDown'] = (info) => {
|
2023-04-25 11:01:25 +00:00
|
|
|
if (info.key === 'Shift') {
|
|
|
|
switch (this.segmentMode) {
|
|
|
|
case 'free': {
|
|
|
|
// We've just entered straight mode, go to straight mode
|
|
|
|
this.segmentMode = 'starting_straight'
|
2023-06-02 15:21:45 +00:00
|
|
|
this.pagePointWhereNextSegmentChanged = this.editor.inputs.currentPagePoint.clone()
|
2023-04-25 11:01:25 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
case 'starting_free': {
|
|
|
|
this.segmentMode = 'starting_straight'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-04-21 11:46:35 +00:00
|
|
|
this.updateDrawingShape()
|
2023-04-25 11:01:25 +00:00
|
|
|
}
|
|
|
|
|
tldraw zero - package shuffle (#1710)
This PR moves code between our packages so that:
- @tldraw/editor is a “core” library with the engine and canvas but no
shapes, tools, or other things
- @tldraw/tldraw contains everything particular to the experience we’ve
built for tldraw
At first look, this might seem like a step away from customization and
configuration, however I believe it greatly increases the configuration
potential of the @tldraw/editor while also providing a more accurate
reflection of what configuration options actually exist for
@tldraw/tldraw.
## Library changes
@tldraw/editor re-exports its dependencies and @tldraw/tldraw re-exports
@tldraw/editor.
- users of @tldraw/editor WITHOUT @tldraw/tldraw should almost always
only import things from @tldraw/editor.
- users of @tldraw/tldraw should almost always only import things from
@tldraw/tldraw.
- @tldraw/polyfills is merged into @tldraw/editor
- @tldraw/indices is merged into @tldraw/editor
- @tldraw/primitives is merged mostly into @tldraw/editor, partially
into @tldraw/tldraw
- @tldraw/file-format is merged into @tldraw/tldraw
- @tldraw/ui is merged into @tldraw/tldraw
Many (many) utils and other code is moved from the editor to tldraw. For
example, embeds now are entirely an feature of @tldraw/tldraw. The only
big chunk of code left in core is related to arrow handling.
## API Changes
The editor can now be used without tldraw's assets. We load them in
@tldraw/tldraw instead, so feel free to use whatever fonts or images or
whatever that you like with the editor.
All tools and shapes (except for the `Group` shape) are moved to
@tldraw/tldraw. This includes the `select` tool.
You should use the editor with at least one tool, however, so you now
also need to send in an `initialState` prop to the Editor /
<TldrawEditor> component indicating which state the editor should begin
in.
The `components` prop now also accepts `SelectionForeground`.
The complex selection component that we use for tldraw is moved to
@tldraw/tldraw. The default component is quite basic but can easily be
replaced via the `components` prop. We pass down our tldraw-flavored
SelectionFg via `components`.
Likewise with the `Scribble` component: the `DefaultScribble` no longer
uses our freehand tech and is a simple path instead. We pass down the
tldraw-flavored scribble via `components`.
The `ExternalContentManager` (`Editor.externalContentManager`) is
removed and replaced with a mapping of types to handlers.
- Register new content handlers with
`Editor.registerExternalContentHandler`.
- Register new asset creation handlers (for files and URLs) with
`Editor.registerExternalAssetHandler`
### Change Type
- [x] `major` — Breaking change
### Test Plan
- [x] Unit Tests
- [x] End to end tests
### Release Notes
- [@tldraw/editor] lots, wip
- [@tldraw/ui] gone, merged to tldraw/tldraw
- [@tldraw/polyfills] gone, merged to tldraw/editor
- [@tldraw/primitives] gone, merged to tldraw/editor / tldraw/tldraw
- [@tldraw/indices] gone, merged to tldraw/editor
- [@tldraw/file-format] gone, merged to tldraw/tldraw
---------
Co-authored-by: alex <alex@dytry.ch>
2023-07-17 21:22:34 +00:00
|
|
|
override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
|
2023-04-25 11:01:25 +00:00
|
|
|
if (info.key === 'Shift') {
|
2024-02-07 10:40:01 +00:00
|
|
|
this.editor.snaps.clearIndicators()
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
switch (this.segmentMode) {
|
|
|
|
case 'straight': {
|
|
|
|
// We've just exited straight mode, go back to free mode
|
|
|
|
this.segmentMode = 'starting_free'
|
2023-06-02 15:21:45 +00:00
|
|
|
this.pagePointWhereNextSegmentChanged = this.editor.inputs.currentPagePoint.clone()
|
2023-04-25 11:01:25 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
case 'starting_straight': {
|
|
|
|
this.pagePointWhereNextSegmentChanged = null
|
|
|
|
this.segmentMode = 'free'
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-21 11:46:35 +00:00
|
|
|
this.updateDrawingShape()
|
2023-04-25 11:01:25 +00:00
|
|
|
}
|
|
|
|
|
tldraw zero - package shuffle (#1710)
This PR moves code between our packages so that:
- @tldraw/editor is a “core” library with the engine and canvas but no
shapes, tools, or other things
- @tldraw/tldraw contains everything particular to the experience we’ve
built for tldraw
At first look, this might seem like a step away from customization and
configuration, however I believe it greatly increases the configuration
potential of the @tldraw/editor while also providing a more accurate
reflection of what configuration options actually exist for
@tldraw/tldraw.
## Library changes
@tldraw/editor re-exports its dependencies and @tldraw/tldraw re-exports
@tldraw/editor.
- users of @tldraw/editor WITHOUT @tldraw/tldraw should almost always
only import things from @tldraw/editor.
- users of @tldraw/tldraw should almost always only import things from
@tldraw/tldraw.
- @tldraw/polyfills is merged into @tldraw/editor
- @tldraw/indices is merged into @tldraw/editor
- @tldraw/primitives is merged mostly into @tldraw/editor, partially
into @tldraw/tldraw
- @tldraw/file-format is merged into @tldraw/tldraw
- @tldraw/ui is merged into @tldraw/tldraw
Many (many) utils and other code is moved from the editor to tldraw. For
example, embeds now are entirely an feature of @tldraw/tldraw. The only
big chunk of code left in core is related to arrow handling.
## API Changes
The editor can now be used without tldraw's assets. We load them in
@tldraw/tldraw instead, so feel free to use whatever fonts or images or
whatever that you like with the editor.
All tools and shapes (except for the `Group` shape) are moved to
@tldraw/tldraw. This includes the `select` tool.
You should use the editor with at least one tool, however, so you now
also need to send in an `initialState` prop to the Editor /
<TldrawEditor> component indicating which state the editor should begin
in.
The `components` prop now also accepts `SelectionForeground`.
The complex selection component that we use for tldraw is moved to
@tldraw/tldraw. The default component is quite basic but can easily be
replaced via the `components` prop. We pass down our tldraw-flavored
SelectionFg via `components`.
Likewise with the `Scribble` component: the `DefaultScribble` no longer
uses our freehand tech and is a simple path instead. We pass down the
tldraw-flavored scribble via `components`.
The `ExternalContentManager` (`Editor.externalContentManager`) is
removed and replaced with a mapping of types to handlers.
- Register new content handlers with
`Editor.registerExternalContentHandler`.
- Register new asset creation handlers (for files and URLs) with
`Editor.registerExternalAssetHandler`
### Change Type
- [x] `major` — Breaking change
### Test Plan
- [x] Unit Tests
- [x] End to end tests
### Release Notes
- [@tldraw/editor] lots, wip
- [@tldraw/ui] gone, merged to tldraw/tldraw
- [@tldraw/polyfills] gone, merged to tldraw/editor
- [@tldraw/primitives] gone, merged to tldraw/editor / tldraw/tldraw
- [@tldraw/indices] gone, merged to tldraw/editor
- [@tldraw/file-format] gone, merged to tldraw/tldraw
---------
Co-authored-by: alex <alex@dytry.ch>
2023-07-17 21:22:34 +00:00
|
|
|
override onExit? = () => {
|
2024-02-07 10:40:01 +00:00
|
|
|
this.editor.snaps.clearIndicators()
|
2023-06-02 15:21:45 +00:00
|
|
|
this.pagePointWhereCurrentSegmentChanged = this.editor.inputs.currentPagePoint.clone()
|
2023-04-25 11:01:25 +00:00
|
|
|
}
|
|
|
|
|
2023-06-01 12:46:13 +00:00
|
|
|
canClose() {
|
2023-07-07 13:56:31 +00:00
|
|
|
return this.shapeType !== 'highlight'
|
2023-06-01 12:46:13 +00:00
|
|
|
}
|
|
|
|
|
2023-06-16 10:33:47 +00:00
|
|
|
getIsClosed(segments: TLDrawShapeSegment[], size: TLDefaultSizeStyle) {
|
2023-06-01 12:46:13 +00:00
|
|
|
if (!this.canClose()) return false
|
|
|
|
|
2023-06-16 10:33:47 +00:00
|
|
|
const strokeWidth = STROKE_SIZES[size]
|
2023-04-25 11:01:25 +00:00
|
|
|
const firstPoint = segments[0].points[0]
|
|
|
|
const lastSegment = segments[segments.length - 1]
|
|
|
|
const lastPoint = lastSegment.points[lastSegment.points.length - 1]
|
|
|
|
|
|
|
|
return (
|
2023-05-30 14:28:56 +00:00
|
|
|
firstPoint !== lastPoint &&
|
2023-04-25 11:01:25 +00:00
|
|
|
this.currentLineLength > strokeWidth * 4 &&
|
2024-04-13 13:30:30 +00:00
|
|
|
Vec.DistMin(firstPoint, lastPoint, strokeWidth * 2)
|
2023-04-25 11:01:25 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
private startShape() {
|
|
|
|
const {
|
|
|
|
inputs: { originPagePoint, isPen },
|
2023-06-02 15:21:45 +00:00
|
|
|
} = this.editor
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2023-09-18 10:50:15 +00:00
|
|
|
this.markId = 'draw start ' + uniqueId()
|
|
|
|
this.editor.mark(this.markId)
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
this.isPen = isPen
|
|
|
|
|
|
|
|
const pressure = this.isPen ? this.info.point.z! * 1.25 : 0.5
|
|
|
|
|
2023-06-02 15:21:45 +00:00
|
|
|
this.segmentMode = this.editor.inputs.shiftKey ? 'straight' : 'free'
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
this.didJustShiftClickToExtendPreviousShapeLine = false
|
|
|
|
|
|
|
|
this.lastRecordedPoint = originPagePoint.clone()
|
|
|
|
|
|
|
|
if (this.initialShape) {
|
`ShapeUtil.getGeometry`, selection rewrite (#1751)
This PR is a significant rewrite of our selection / hit testing logic.
It
- replaces our current geometric helpers (`getBounds`, `getOutline`,
`hitTestPoint`, and `hitTestLineSegment`) with a new geometry API
- moves our hit testing entirely to JS using geometry
- improves selection logic, especially around editing shapes, groups and
frames
- fixes many minor selection bugs (e.g. shapes behind frames)
- removes hit-testing DOM elements from ShapeFill etc.
- adds many new tests around selection
- adds new tests around selection
- makes several superficial changes to surface editor APIs
This PR is hard to evaluate. The `selection-omnibus` test suite is
intended to describe all of the selection behavior, however all existing
tests are also either here preserved and passing or (in a few cases
around editing shapes) are modified to reflect the new behavior.
## Geometry
All `ShapeUtils` implement `getGeometry`, which returns a single
geometry primitive (`Geometry2d`). For example:
```ts
class BoxyShapeUtil {
getGeometry(shape: BoxyShape) {
return new Rectangle2d({
width: shape.props.width,
height: shape.props.height,
isFilled: true,
margin: shape.props.strokeWidth
})
}
}
```
This geometric primitive is used for all bounds calculation, hit
testing, intersection with arrows, etc.
There are several geometric primitives that extend `Geometry2d`:
- `Arc2d`
- `Circle2d`
- `CubicBezier2d`
- `CubicSpline2d`
- `Edge2d`
- `Ellipse2d`
- `Group2d`
- `Polygon2d`
- `Rectangle2d`
- `Stadium2d`
For shapes that have more complicated geometric representations, such as
an arrow with a label, the `Group2d` can accept other primitives as its
children.
## Hit testing
Previously, we did all hit testing via events set on shapes and other
elements. In this PR, I've replaced those hit tests with our own
calculation for hit tests in JavaScript. This removed the need for many
DOM elements, such as hit test area borders and fills which only existed
to trigger pointer events.
## Selection
We now support selecting "hollow" shapes by clicking inside of them.
This involves a lot of new logic but it should work intuitively. See
`Editor.getShapeAtPoint` for the (thoroughly commented) implementation.
![Kapture 2023-07-23 at 23 27
27](https://github.com/tldraw/tldraw/assets/23072548/a743275c-acdb-42d9-a3fe-b3e20dce86b6)
every sunset is actually the sun hiding in fear and respect of tldraw's
quality of interactions
This PR also fixes several bugs with scribble selection, in particular
around the shift key modifier.
![Kapture 2023-07-24 at 23 34
07](https://github.com/tldraw/tldraw/assets/23072548/871d67d0-8d06-42ae-a2b2-021effba37c5)
...as well as issues with labels and editing.
There are **over 100 new tests** for selection covering groups, frames,
brushing, scribbling, hovering, and editing. I'll add a few more before
I feel comfortable merging this PR.
## Arrow binding
Using the same "hollow shape" logic as selection, arrow binding is
significantly improved.
![Kapture 2023-07-22 at 07 46
25](https://github.com/tldraw/tldraw/assets/23072548/5aa724b3-b57d-4fb7-92d0-80e34246753c)
a thousand wise men could not improve on this
## Moving focus between editing shapes
Previously, this was handled in the `editing_shapes` state. This is
moved to `useEditableText`, and should generally be considered an
advanced implementation detail on a shape-by-shape basis. This addresses
a bug that I'd never noticed before, but which can be reproduced by
selecting an shape—but not focusing its input—while editing a different
shape. Previously, the new shape became the editing shape but its input
did not focus.
![Kapture 2023-07-23 at 23 19
09](https://github.com/tldraw/tldraw/assets/23072548/a5e157fb-24a8-42bd-a692-04ce769b1a9c)
In this PR, you can select a shape by clicking on its edge or body, or
select its input to transfer editing / focus.
![Kapture 2023-07-23 at 23 22
21](https://github.com/tldraw/tldraw/assets/23072548/7384e7ea-9777-4e1a-8f63-15de2166a53a)
tldraw, glorious tldraw
### Change Type
- [x] `major` — Breaking change
### Test Plan
1. Erase shapes
2. Select shapes
3. Calculate their bounding boxes
- [ ] Unit Tests // todo
- [ ] End to end tests // todo
### Release Notes
- [editor] Remove `ShapeUtil.getBounds`, `ShapeUtil.getOutline`,
`ShapeUtil.hitTestPoint`, `ShapeUtil.hitTestLineSegment`
- [editor] Add `ShapeUtil.getGeometry`
- [editor] Add `Editor.getShapeGeometry`
2023-07-25 16:10:15 +00:00
|
|
|
const shape = this.editor.getShape<DrawableShape>(this.initialShape.id)
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
if (shape && this.segmentMode === 'straight') {
|
|
|
|
// Connect dots
|
|
|
|
|
|
|
|
this.didJustShiftClickToExtendPreviousShapeLine = true
|
|
|
|
|
|
|
|
const prevSegment = last(shape.props.segments)
|
|
|
|
if (!prevSegment) throw Error('Expected a previous segment!')
|
|
|
|
const prevPoint = last(prevSegment.points)
|
|
|
|
if (!prevPoint) throw Error('Expected a previous point!')
|
|
|
|
|
2023-06-02 15:21:45 +00:00
|
|
|
const { x, y } = this.editor.getPointInShapeSpace(shape, originPagePoint).toFixed()
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
const pressure = this.isPen ? this.info.point.z! * 1.25 : 0.5
|
|
|
|
|
|
|
|
const newSegment: TLDrawShapeSegment = {
|
|
|
|
type: this.segmentMode,
|
|
|
|
points: [
|
|
|
|
{
|
|
|
|
x: prevPoint.x,
|
|
|
|
y: prevPoint.y,
|
|
|
|
z: +pressure.toFixed(2),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
x,
|
|
|
|
y,
|
|
|
|
z: +pressure.toFixed(2),
|
|
|
|
},
|
|
|
|
],
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert prevPoint to page space
|
2024-01-03 12:13:15 +00:00
|
|
|
const prevPointPageSpace = Mat.applyToPoint(
|
2023-08-02 18:12:25 +00:00
|
|
|
this.editor.getShapePageTransform(shape.id)!,
|
2023-04-25 11:01:25 +00:00
|
|
|
prevPoint
|
|
|
|
)
|
|
|
|
this.pagePointWhereCurrentSegmentChanged = prevPointPageSpace
|
|
|
|
this.pagePointWhereNextSegmentChanged = null
|
|
|
|
const segments = [...shape.props.segments, newSegment]
|
|
|
|
|
2024-04-13 13:30:30 +00:00
|
|
|
if (this.currentLineLength < STROKE_SIZES[shape.props.size] * 4) {
|
|
|
|
this.currentLineLength = this.getLineLength(segments)
|
|
|
|
}
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2023-06-13 18:02:17 +00:00
|
|
|
const shapePartial: TLShapePartial<DrawableShape> = {
|
|
|
|
id: shape.id,
|
2023-07-07 13:56:31 +00:00
|
|
|
type: this.shapeType,
|
2023-06-13 18:02:17 +00:00
|
|
|
props: {
|
|
|
|
segments,
|
2023-04-25 11:01:25 +00:00
|
|
|
},
|
2023-06-13 18:02:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this.canClose()) {
|
|
|
|
;(shapePartial as TLShapePartial<TLDrawShape>).props!.isClosed = this.getIsClosed(
|
|
|
|
segments,
|
|
|
|
shape.props.size
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
this.editor.updateShapes<TLDrawShape | TLHighlightShape>([shapePartial])
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a new shape
|
|
|
|
|
|
|
|
this.pagePointWhereCurrentSegmentChanged = originPagePoint.clone()
|
|
|
|
const id = createShapeId()
|
2023-06-13 18:02:17 +00:00
|
|
|
|
|
|
|
this.editor.createShapes<DrawableShape>([
|
2023-04-25 11:01:25 +00:00
|
|
|
{
|
|
|
|
id,
|
2023-07-07 13:56:31 +00:00
|
|
|
type: this.shapeType,
|
2023-04-25 11:01:25 +00:00
|
|
|
x: originPagePoint.x,
|
|
|
|
y: originPagePoint.y,
|
|
|
|
props: {
|
|
|
|
isPen: this.isPen,
|
|
|
|
segments: [
|
|
|
|
{
|
|
|
|
type: this.segmentMode,
|
|
|
|
points: [
|
|
|
|
{
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
z: +pressure.toFixed(2),
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
])
|
|
|
|
this.currentLineLength = 0
|
`ShapeUtil.getGeometry`, selection rewrite (#1751)
This PR is a significant rewrite of our selection / hit testing logic.
It
- replaces our current geometric helpers (`getBounds`, `getOutline`,
`hitTestPoint`, and `hitTestLineSegment`) with a new geometry API
- moves our hit testing entirely to JS using geometry
- improves selection logic, especially around editing shapes, groups and
frames
- fixes many minor selection bugs (e.g. shapes behind frames)
- removes hit-testing DOM elements from ShapeFill etc.
- adds many new tests around selection
- adds new tests around selection
- makes several superficial changes to surface editor APIs
This PR is hard to evaluate. The `selection-omnibus` test suite is
intended to describe all of the selection behavior, however all existing
tests are also either here preserved and passing or (in a few cases
around editing shapes) are modified to reflect the new behavior.
## Geometry
All `ShapeUtils` implement `getGeometry`, which returns a single
geometry primitive (`Geometry2d`). For example:
```ts
class BoxyShapeUtil {
getGeometry(shape: BoxyShape) {
return new Rectangle2d({
width: shape.props.width,
height: shape.props.height,
isFilled: true,
margin: shape.props.strokeWidth
})
}
}
```
This geometric primitive is used for all bounds calculation, hit
testing, intersection with arrows, etc.
There are several geometric primitives that extend `Geometry2d`:
- `Arc2d`
- `Circle2d`
- `CubicBezier2d`
- `CubicSpline2d`
- `Edge2d`
- `Ellipse2d`
- `Group2d`
- `Polygon2d`
- `Rectangle2d`
- `Stadium2d`
For shapes that have more complicated geometric representations, such as
an arrow with a label, the `Group2d` can accept other primitives as its
children.
## Hit testing
Previously, we did all hit testing via events set on shapes and other
elements. In this PR, I've replaced those hit tests with our own
calculation for hit tests in JavaScript. This removed the need for many
DOM elements, such as hit test area borders and fills which only existed
to trigger pointer events.
## Selection
We now support selecting "hollow" shapes by clicking inside of them.
This involves a lot of new logic but it should work intuitively. See
`Editor.getShapeAtPoint` for the (thoroughly commented) implementation.
![Kapture 2023-07-23 at 23 27
27](https://github.com/tldraw/tldraw/assets/23072548/a743275c-acdb-42d9-a3fe-b3e20dce86b6)
every sunset is actually the sun hiding in fear and respect of tldraw's
quality of interactions
This PR also fixes several bugs with scribble selection, in particular
around the shift key modifier.
![Kapture 2023-07-24 at 23 34
07](https://github.com/tldraw/tldraw/assets/23072548/871d67d0-8d06-42ae-a2b2-021effba37c5)
...as well as issues with labels and editing.
There are **over 100 new tests** for selection covering groups, frames,
brushing, scribbling, hovering, and editing. I'll add a few more before
I feel comfortable merging this PR.
## Arrow binding
Using the same "hollow shape" logic as selection, arrow binding is
significantly improved.
![Kapture 2023-07-22 at 07 46
25](https://github.com/tldraw/tldraw/assets/23072548/5aa724b3-b57d-4fb7-92d0-80e34246753c)
a thousand wise men could not improve on this
## Moving focus between editing shapes
Previously, this was handled in the `editing_shapes` state. This is
moved to `useEditableText`, and should generally be considered an
advanced implementation detail on a shape-by-shape basis. This addresses
a bug that I'd never noticed before, but which can be reproduced by
selecting an shape—but not focusing its input—while editing a different
shape. Previously, the new shape became the editing shape but its input
did not focus.
![Kapture 2023-07-23 at 23 19
09](https://github.com/tldraw/tldraw/assets/23072548/a5e157fb-24a8-42bd-a692-04ce769b1a9c)
In this PR, you can select a shape by clicking on its edge or body, or
select its input to transfer editing / focus.
![Kapture 2023-07-23 at 23 22
21](https://github.com/tldraw/tldraw/assets/23072548/7384e7ea-9777-4e1a-8f63-15de2166a53a)
tldraw, glorious tldraw
### Change Type
- [x] `major` — Breaking change
### Test Plan
1. Erase shapes
2. Select shapes
3. Calculate their bounding boxes
- [ ] Unit Tests // todo
- [ ] End to end tests // todo
### Release Notes
- [editor] Remove `ShapeUtil.getBounds`, `ShapeUtil.getOutline`,
`ShapeUtil.hitTestPoint`, `ShapeUtil.hitTestLineSegment`
- [editor] Add `ShapeUtil.getGeometry`
- [editor] Add `Editor.getShapeGeometry`
2023-07-25 16:10:15 +00:00
|
|
|
this.initialShape = this.editor.getShape<DrawableShape>(id)
|
2023-04-25 11:01:25 +00:00
|
|
|
}
|
|
|
|
|
2024-04-21 11:46:35 +00:00
|
|
|
private updateDrawingShape() {
|
2023-04-25 11:01:25 +00:00
|
|
|
const { initialShape } = this
|
Input buffering (#3223)
This PR buffs input events.
## The story so far
In the olde days, we throttled events from the canvas events hook so
that a pointer event would only be sent every 1/60th of a second. This
was fine but made drawing on the iPad / 120FPS displays a little sad.
Then we removed this throttle. It seemed fine! Drawing at 120FPS was
great. We improved some rendering speeds and tightened some loops so
that the engine could keep up with 2x the number of points in a line.
Then we started noticing that iPads and other screens could start
choking on events as it received new inputs and tried to process and
render inputs while still recovering from a previous dropped frame. Even
worse, on iPad the work of rendering at 120FPS was causing the browser
to throttle the app after some sustained drawing. Yikes!
### Batching
I did an experimental PR (#3180) to bring back batching but do it in the
editor instead. What we would do is: rather than immediately processing
an event when we get it, we would instead put the event into a buffer.
On the next 60FPS tick, we would flush the buffer and process all of the
events. We'd have them all in the same transaction so that the app would
only render once.
### Render batching?
We then tried batching the renders, so that the app would only ever
render once per (next) frame. This added a bunch of complexity around
events that needed to happen synchronously, such as writing text in a
text field. Some inputs could "lag" in a way familiar to anyone who's
tried to update an input's state asynchronously. So we backed out of
this.
### Coalescing?
Another idea from @ds300 was to "coalesce" the events. This would be
useful because, while some interactions like drawing would require the
in-between frames in order to avoid data loss, most interactions (like
resizing) didn't actually need the in-between frames, they could just
use the last input of a given type.
Coalescing turned out to be trickier than we thought, though. Often a
state node required information from elsewhere in the app when
processing an event (such as camera position or page point, which is
derived from the camera position), and so the coalesced events would
need to also include this information or else the handlers wouldn't work
the way they should when processing the "final" event during a tick.
So we backed out of the coalescing strategy for now. Here's the [PR that
removes](https://github.com/tldraw/tldraw/pull/3223/commits/937469d69d4474fe9d1ff98604acb8f55a49f3fa)
it.
### Let's just buffer the fuckers
So this PR now should only include input buffering.
I think there are ways to achieve the same coalescing-like results
through the state nodes, which could gather information during the
`onPointerMove` handler and then actually make changes during the
`onTick` handler, so that the changes are only done as many time as
necessary. This should help with e.g. resizing lots of shapes at once.
But first let's land the buffering!
---
Mitja's original text:
This PR builds on top of Steve's [experiment
PR](https://github.com/tldraw/tldraw/pull/3180) here. It also adds event
coalescing for [`pointerMove`
events](https://github.com/tldraw/tldraw/blob/mitja/input-buffering/packages/editor/src/lib/editor/Editor.ts#L8364-L8368).
The API is [somewhat similar
](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/getCoalescedEvents)
to `getCoalescedEvent`. In `StateNodes` we register an `onPointerMove`
handler. When the event happens it gets called with the event `info`.
There's now an additional field on `TLMovePointerEvent` called
`coalescedInfo` which includes all the events. It's then on the user to
process all of these.
I decided on this API since it allows us to only expose one event
handler, but it still gives the users access to all events if they need
them.
We would otherwise either need to:
- Expose two events (coalesced and non-coalesced one and complicate the
api) so that state nodes like Resizing would not be triggered for each
pointer move.
- Offer some methods on the editor that would allow use to get the
coalesced information. Then the nodes that need that info could request
it. I [tried
this](https://github.com/tldraw/tldraw/pull/3223/commits/9ad973da3aa287e7974067ac923193530d29c188#diff-32f1de9a5a9ec72aa49a8d18a237fbfff301610f4689a4af6b37f47af435aafcR67),
but it didn't feel good.
This also complicated the editor inputs. The events need to store
information about the event (like the mouse position when the event
happened for `onPointerMove`). But we cannot immediately update inputs
when the event happens. To make this work for `pointerMove` events I've
added `pagePoint`. It's
[calculated](https://github.com/tldraw/tldraw/pull/3223/files#diff-980beb0aa0ee9aa6d1cd386cef3dc05a500c030638ffb58d45fd11b79126103fR71)
when the event triggers and then consumers can get it straight from the
event (like
[Drawing](https://github.com/tldraw/tldraw/pull/3223/files#diff-32f1de9a5a9ec72aa49a8d18a237fbfff301610f4689a4af6b37f47af435aafcR104)).
### Change Type
<!-- ❗ Please select a 'Scope' label ❗️ -->
- [x] `sdk` — Changes the tldraw SDK
- [ ] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff
<!-- ❗ Please select a 'Type' label ❗️ -->
- [ ] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [x] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know
### Test Plan
1. Add a step-by-step description of how to test your PR here.
4.
- [ ] Unit Tests
- [ ] End to end tests
### Release Notes
- Add a brief release note for your PR here.
---------
Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
2024-04-02 14:29:14 +00:00
|
|
|
const { inputs } = this.editor
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
if (!initialShape) return
|
|
|
|
|
|
|
|
const {
|
|
|
|
id,
|
|
|
|
props: { size },
|
|
|
|
} = initialShape
|
|
|
|
|
`ShapeUtil.getGeometry`, selection rewrite (#1751)
This PR is a significant rewrite of our selection / hit testing logic.
It
- replaces our current geometric helpers (`getBounds`, `getOutline`,
`hitTestPoint`, and `hitTestLineSegment`) with a new geometry API
- moves our hit testing entirely to JS using geometry
- improves selection logic, especially around editing shapes, groups and
frames
- fixes many minor selection bugs (e.g. shapes behind frames)
- removes hit-testing DOM elements from ShapeFill etc.
- adds many new tests around selection
- adds new tests around selection
- makes several superficial changes to surface editor APIs
This PR is hard to evaluate. The `selection-omnibus` test suite is
intended to describe all of the selection behavior, however all existing
tests are also either here preserved and passing or (in a few cases
around editing shapes) are modified to reflect the new behavior.
## Geometry
All `ShapeUtils` implement `getGeometry`, which returns a single
geometry primitive (`Geometry2d`). For example:
```ts
class BoxyShapeUtil {
getGeometry(shape: BoxyShape) {
return new Rectangle2d({
width: shape.props.width,
height: shape.props.height,
isFilled: true,
margin: shape.props.strokeWidth
})
}
}
```
This geometric primitive is used for all bounds calculation, hit
testing, intersection with arrows, etc.
There are several geometric primitives that extend `Geometry2d`:
- `Arc2d`
- `Circle2d`
- `CubicBezier2d`
- `CubicSpline2d`
- `Edge2d`
- `Ellipse2d`
- `Group2d`
- `Polygon2d`
- `Rectangle2d`
- `Stadium2d`
For shapes that have more complicated geometric representations, such as
an arrow with a label, the `Group2d` can accept other primitives as its
children.
## Hit testing
Previously, we did all hit testing via events set on shapes and other
elements. In this PR, I've replaced those hit tests with our own
calculation for hit tests in JavaScript. This removed the need for many
DOM elements, such as hit test area borders and fills which only existed
to trigger pointer events.
## Selection
We now support selecting "hollow" shapes by clicking inside of them.
This involves a lot of new logic but it should work intuitively. See
`Editor.getShapeAtPoint` for the (thoroughly commented) implementation.
![Kapture 2023-07-23 at 23 27
27](https://github.com/tldraw/tldraw/assets/23072548/a743275c-acdb-42d9-a3fe-b3e20dce86b6)
every sunset is actually the sun hiding in fear and respect of tldraw's
quality of interactions
This PR also fixes several bugs with scribble selection, in particular
around the shift key modifier.
![Kapture 2023-07-24 at 23 34
07](https://github.com/tldraw/tldraw/assets/23072548/871d67d0-8d06-42ae-a2b2-021effba37c5)
...as well as issues with labels and editing.
There are **over 100 new tests** for selection covering groups, frames,
brushing, scribbling, hovering, and editing. I'll add a few more before
I feel comfortable merging this PR.
## Arrow binding
Using the same "hollow shape" logic as selection, arrow binding is
significantly improved.
![Kapture 2023-07-22 at 07 46
25](https://github.com/tldraw/tldraw/assets/23072548/5aa724b3-b57d-4fb7-92d0-80e34246753c)
a thousand wise men could not improve on this
## Moving focus between editing shapes
Previously, this was handled in the `editing_shapes` state. This is
moved to `useEditableText`, and should generally be considered an
advanced implementation detail on a shape-by-shape basis. This addresses
a bug that I'd never noticed before, but which can be reproduced by
selecting an shape—but not focusing its input—while editing a different
shape. Previously, the new shape became the editing shape but its input
did not focus.
![Kapture 2023-07-23 at 23 19
09](https://github.com/tldraw/tldraw/assets/23072548/a5e157fb-24a8-42bd-a692-04ce769b1a9c)
In this PR, you can select a shape by clicking on its edge or body, or
select its input to transfer editing / focus.
![Kapture 2023-07-23 at 23 22
21](https://github.com/tldraw/tldraw/assets/23072548/7384e7ea-9777-4e1a-8f63-15de2166a53a)
tldraw, glorious tldraw
### Change Type
- [x] `major` — Breaking change
### Test Plan
1. Erase shapes
2. Select shapes
3. Calculate their bounding boxes
- [ ] Unit Tests // todo
- [ ] End to end tests // todo
### Release Notes
- [editor] Remove `ShapeUtil.getBounds`, `ShapeUtil.getOutline`,
`ShapeUtil.hitTestPoint`, `ShapeUtil.hitTestLineSegment`
- [editor] Add `ShapeUtil.getGeometry`
- [editor] Add `Editor.getShapeGeometry`
2023-07-25 16:10:15 +00:00
|
|
|
const shape = this.editor.getShape<DrawableShape>(id)!
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
if (!shape) return
|
|
|
|
|
2024-03-21 10:05:44 +00:00
|
|
|
const { segments } = shape.props
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2023-06-02 15:21:45 +00:00
|
|
|
const { x, y, z } = this.editor.getPointInShapeSpace(shape, inputs.currentPagePoint).toFixed()
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
const newPoint = { x, y, z: this.isPen ? +(z! * 1.25).toFixed(2) : 0.5 }
|
|
|
|
|
|
|
|
switch (this.segmentMode) {
|
|
|
|
case 'starting_straight': {
|
|
|
|
const { pagePointWhereNextSegmentChanged } = this
|
|
|
|
|
|
|
|
if (pagePointWhereNextSegmentChanged === null) {
|
|
|
|
throw Error('We should have a point where the segment changed')
|
|
|
|
}
|
|
|
|
|
|
|
|
const hasMovedFarEnough =
|
2024-04-08 13:31:05 +00:00
|
|
|
Vec.Dist2(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) > DRAG_DISTANCE
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
// Find the distance from where the pointer was when shift was released and
|
|
|
|
// where it is now; if it's far enough away, then update the page point where
|
|
|
|
// the current segment changed (to match the pagepoint where next segment changed)
|
|
|
|
// and set the pagepoint where next segment changed to null.
|
|
|
|
if (hasMovedFarEnough) {
|
|
|
|
this.pagePointWhereCurrentSegmentChanged = this.pagePointWhereNextSegmentChanged!.clone()
|
|
|
|
this.pagePointWhereNextSegmentChanged = null
|
|
|
|
|
|
|
|
// Set the new mode
|
|
|
|
this.segmentMode = 'straight'
|
|
|
|
|
|
|
|
const prevSegment = last(segments)
|
|
|
|
if (!prevSegment) throw Error('Expected a previous segment!')
|
|
|
|
|
|
|
|
const prevLastPoint = last(prevSegment.points)
|
|
|
|
if (!prevLastPoint) throw Error('Expected a previous last point!')
|
|
|
|
|
|
|
|
let newSegment: TLDrawShapeSegment
|
|
|
|
|
2023-06-02 15:21:45 +00:00
|
|
|
const newLastPoint = this.editor
|
2023-04-25 11:01:25 +00:00
|
|
|
.getPointInShapeSpace(shape, this.pagePointWhereCurrentSegmentChanged)
|
|
|
|
.toFixed()
|
|
|
|
.toJson()
|
|
|
|
|
|
|
|
if (prevSegment.type === 'straight') {
|
2024-01-03 12:13:15 +00:00
|
|
|
this.currentLineLength += Vec.Dist(prevLastPoint, newLastPoint)
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
newSegment = {
|
|
|
|
type: 'straight',
|
|
|
|
points: [{ ...prevLastPoint }, newLastPoint],
|
|
|
|
}
|
|
|
|
|
2023-08-02 18:12:25 +00:00
|
|
|
const transform = this.editor.getShapePageTransform(shape)!
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2024-01-03 12:13:15 +00:00
|
|
|
this.pagePointWhereCurrentSegmentChanged = Mat.applyToPoint(transform, prevLastPoint)
|
2023-04-25 11:01:25 +00:00
|
|
|
} else {
|
|
|
|
newSegment = {
|
|
|
|
type: 'straight',
|
|
|
|
points: [newLastPoint, newPoint],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-13 18:02:17 +00:00
|
|
|
const shapePartial: TLShapePartial<DrawableShape> = {
|
|
|
|
id,
|
2023-07-07 13:56:31 +00:00
|
|
|
type: this.shapeType,
|
2023-06-13 18:02:17 +00:00
|
|
|
props: {
|
|
|
|
segments: [...segments, newSegment],
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.canClose()) {
|
|
|
|
;(shapePartial as TLShapePartial<TLDrawShape>).props!.isClosed = this.getIsClosed(
|
|
|
|
segments,
|
|
|
|
size
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-03-21 10:05:44 +00:00
|
|
|
this.editor.updateShapes<TLDrawShape | TLHighlightShape>([shapePartial], {
|
|
|
|
squashing: true,
|
|
|
|
})
|
2023-04-25 11:01:25 +00:00
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
case 'starting_free': {
|
|
|
|
const { pagePointWhereNextSegmentChanged } = this
|
|
|
|
|
|
|
|
if (pagePointWhereNextSegmentChanged === null) {
|
|
|
|
throw Error('We should have a point where the segment changed')
|
|
|
|
}
|
|
|
|
|
|
|
|
const hasMovedFarEnough =
|
2024-04-08 13:31:05 +00:00
|
|
|
Vec.Dist2(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) > DRAG_DISTANCE
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
// Find the distance from where the pointer was when shift was released and
|
|
|
|
// where it is now; if it's far enough away, then update the page point where
|
|
|
|
// the current segment changed (to match the pagepoint where next segment changed)
|
|
|
|
// and set the pagepoint where next segment changed to null.
|
|
|
|
if (hasMovedFarEnough) {
|
|
|
|
this.pagePointWhereCurrentSegmentChanged = this.pagePointWhereNextSegmentChanged!.clone()
|
|
|
|
this.pagePointWhereNextSegmentChanged = null
|
|
|
|
|
|
|
|
// Set the new mode
|
|
|
|
this.segmentMode = 'free'
|
|
|
|
|
|
|
|
const newSegments = segments.slice()
|
|
|
|
const prevStraightSegment = newSegments[newSegments.length - 1]
|
|
|
|
const prevPoint = last(prevStraightSegment.points)
|
|
|
|
|
|
|
|
if (!prevPoint) {
|
|
|
|
throw Error('No previous point!')
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create the new free segment and interpolate the points between where the last line
|
|
|
|
// ended and where the pointer is now
|
|
|
|
const newFreeSegment: TLDrawShapeSegment = {
|
|
|
|
type: 'free',
|
2024-01-03 12:13:15 +00:00
|
|
|
points: [...Vec.PointsBetween(prevPoint, newPoint, 6).map((p) => p.toFixed().toJson())],
|
2023-04-25 11:01:25 +00:00
|
|
|
}
|
|
|
|
|
2023-05-30 14:28:56 +00:00
|
|
|
const finalSegments = [...newSegments, newFreeSegment]
|
2024-04-13 13:30:30 +00:00
|
|
|
|
|
|
|
if (this.currentLineLength < STROKE_SIZES[shape.props.size] * 4) {
|
|
|
|
this.currentLineLength = this.getLineLength(finalSegments)
|
|
|
|
}
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2023-06-13 18:02:17 +00:00
|
|
|
const shapePartial: TLShapePartial<DrawableShape> = {
|
|
|
|
id,
|
2023-07-07 13:56:31 +00:00
|
|
|
type: this.shapeType,
|
2023-06-13 18:02:17 +00:00
|
|
|
props: {
|
|
|
|
segments: finalSegments,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.canClose()) {
|
|
|
|
;(shapePartial as TLShapePartial<TLDrawShape>).props!.isClosed = this.getIsClosed(
|
|
|
|
finalSegments,
|
|
|
|
size
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-03-21 10:05:44 +00:00
|
|
|
this.editor.updateShapes([shapePartial], { squashing: true })
|
2023-04-25 11:01:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
break
|
|
|
|
}
|
|
|
|
case 'straight': {
|
|
|
|
const newSegments = segments.slice()
|
|
|
|
const newSegment = newSegments[newSegments.length - 1]
|
|
|
|
|
|
|
|
const { pagePointWhereCurrentSegmentChanged } = this
|
Input buffering (#3223)
This PR buffs input events.
## The story so far
In the olde days, we throttled events from the canvas events hook so
that a pointer event would only be sent every 1/60th of a second. This
was fine but made drawing on the iPad / 120FPS displays a little sad.
Then we removed this throttle. It seemed fine! Drawing at 120FPS was
great. We improved some rendering speeds and tightened some loops so
that the engine could keep up with 2x the number of points in a line.
Then we started noticing that iPads and other screens could start
choking on events as it received new inputs and tried to process and
render inputs while still recovering from a previous dropped frame. Even
worse, on iPad the work of rendering at 120FPS was causing the browser
to throttle the app after some sustained drawing. Yikes!
### Batching
I did an experimental PR (#3180) to bring back batching but do it in the
editor instead. What we would do is: rather than immediately processing
an event when we get it, we would instead put the event into a buffer.
On the next 60FPS tick, we would flush the buffer and process all of the
events. We'd have them all in the same transaction so that the app would
only render once.
### Render batching?
We then tried batching the renders, so that the app would only ever
render once per (next) frame. This added a bunch of complexity around
events that needed to happen synchronously, such as writing text in a
text field. Some inputs could "lag" in a way familiar to anyone who's
tried to update an input's state asynchronously. So we backed out of
this.
### Coalescing?
Another idea from @ds300 was to "coalesce" the events. This would be
useful because, while some interactions like drawing would require the
in-between frames in order to avoid data loss, most interactions (like
resizing) didn't actually need the in-between frames, they could just
use the last input of a given type.
Coalescing turned out to be trickier than we thought, though. Often a
state node required information from elsewhere in the app when
processing an event (such as camera position or page point, which is
derived from the camera position), and so the coalesced events would
need to also include this information or else the handlers wouldn't work
the way they should when processing the "final" event during a tick.
So we backed out of the coalescing strategy for now. Here's the [PR that
removes](https://github.com/tldraw/tldraw/pull/3223/commits/937469d69d4474fe9d1ff98604acb8f55a49f3fa)
it.
### Let's just buffer the fuckers
So this PR now should only include input buffering.
I think there are ways to achieve the same coalescing-like results
through the state nodes, which could gather information during the
`onPointerMove` handler and then actually make changes during the
`onTick` handler, so that the changes are only done as many time as
necessary. This should help with e.g. resizing lots of shapes at once.
But first let's land the buffering!
---
Mitja's original text:
This PR builds on top of Steve's [experiment
PR](https://github.com/tldraw/tldraw/pull/3180) here. It also adds event
coalescing for [`pointerMove`
events](https://github.com/tldraw/tldraw/blob/mitja/input-buffering/packages/editor/src/lib/editor/Editor.ts#L8364-L8368).
The API is [somewhat similar
](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/getCoalescedEvents)
to `getCoalescedEvent`. In `StateNodes` we register an `onPointerMove`
handler. When the event happens it gets called with the event `info`.
There's now an additional field on `TLMovePointerEvent` called
`coalescedInfo` which includes all the events. It's then on the user to
process all of these.
I decided on this API since it allows us to only expose one event
handler, but it still gives the users access to all events if they need
them.
We would otherwise either need to:
- Expose two events (coalesced and non-coalesced one and complicate the
api) so that state nodes like Resizing would not be triggered for each
pointer move.
- Offer some methods on the editor that would allow use to get the
coalesced information. Then the nodes that need that info could request
it. I [tried
this](https://github.com/tldraw/tldraw/pull/3223/commits/9ad973da3aa287e7974067ac923193530d29c188#diff-32f1de9a5a9ec72aa49a8d18a237fbfff301610f4689a4af6b37f47af435aafcR67),
but it didn't feel good.
This also complicated the editor inputs. The events need to store
information about the event (like the mouse position when the event
happened for `onPointerMove`). But we cannot immediately update inputs
when the event happens. To make this work for `pointerMove` events I've
added `pagePoint`. It's
[calculated](https://github.com/tldraw/tldraw/pull/3223/files#diff-980beb0aa0ee9aa6d1cd386cef3dc05a500c030638ffb58d45fd11b79126103fR71)
when the event triggers and then consumers can get it straight from the
event (like
[Drawing](https://github.com/tldraw/tldraw/pull/3223/files#diff-32f1de9a5a9ec72aa49a8d18a237fbfff301610f4689a4af6b37f47af435aafcR104)).
### Change Type
<!-- ❗ Please select a 'Scope' label ❗️ -->
- [x] `sdk` — Changes the tldraw SDK
- [ ] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff
<!-- ❗ Please select a 'Type' label ❗️ -->
- [ ] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [x] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know
### Test Plan
1. Add a step-by-step description of how to test your PR here.
4.
- [ ] Unit Tests
- [ ] End to end tests
### Release Notes
- Add a brief release note for your PR here.
---------
Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
2024-04-02 14:29:14 +00:00
|
|
|
const { ctrlKey, currentPagePoint } = this.editor.inputs
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
if (!pagePointWhereCurrentSegmentChanged)
|
|
|
|
throw Error('We should have a point where the segment changed')
|
|
|
|
|
2024-01-03 12:13:15 +00:00
|
|
|
let pagePoint: VecModel
|
2023-04-25 11:01:25 +00:00
|
|
|
let shouldSnapToAngle = false
|
|
|
|
|
|
|
|
if (this.didJustShiftClickToExtendPreviousShapeLine) {
|
2023-06-02 15:21:45 +00:00
|
|
|
if (this.editor.inputs.isDragging) {
|
2023-04-25 11:01:25 +00:00
|
|
|
// If we've just shift clicked to extend a line, only snap once we've started dragging
|
|
|
|
shouldSnapToAngle = !ctrlKey
|
|
|
|
this.didJustShiftClickToExtendPreviousShapeLine = false
|
|
|
|
} else {
|
|
|
|
// noop
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// If we're not shift clicking to extend a line, but we're holding shift, then we should snap
|
|
|
|
shouldSnapToAngle = !ctrlKey // don't snap angle while snapping line
|
|
|
|
}
|
|
|
|
|
2023-06-02 15:21:45 +00:00
|
|
|
let newPoint = this.editor.getPointInShapeSpace(shape, currentPagePoint).toFixed().toJson()
|
2023-04-25 11:01:25 +00:00
|
|
|
let didSnap = false
|
|
|
|
let snapSegment: TLDrawShapeSegment | undefined = undefined
|
|
|
|
|
2023-11-14 17:07:35 +00:00
|
|
|
const shouldSnap = this.editor.user.getIsSnapMode() ? !ctrlKey : ctrlKey
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
if (shouldSnap) {
|
|
|
|
if (newSegments.length > 2) {
|
2024-01-03 12:13:15 +00:00
|
|
|
let nearestPoint: VecModel | undefined = undefined
|
2023-11-14 11:57:43 +00:00
|
|
|
let minDistance = 8 / this.editor.getZoomLevel()
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
// Don't try to snap to the last two segments
|
|
|
|
for (let i = 0, n = segments.length - 2; i < n; i++) {
|
|
|
|
const segment = segments[i]
|
|
|
|
if (!segment) break
|
|
|
|
if (segment.type === 'free') continue
|
|
|
|
|
|
|
|
const first = segment.points[0]
|
|
|
|
const lastPoint = last(segment.points)
|
|
|
|
if (!(first && lastPoint)) continue
|
|
|
|
|
|
|
|
// Snap to the nearest point on the segment, if it's closer than the previous snapped point
|
2024-01-03 12:13:15 +00:00
|
|
|
const nearestPointOnSegment = Vec.NearestPointOnLineSegment(
|
2023-04-25 11:01:25 +00:00
|
|
|
first,
|
|
|
|
lastPoint,
|
|
|
|
newPoint
|
|
|
|
)
|
|
|
|
|
2024-04-13 13:30:30 +00:00
|
|
|
if (Vec.DistMin(nearestPointOnSegment, newPoint, minDistance)) {
|
2023-04-25 11:01:25 +00:00
|
|
|
nearestPoint = nearestPointOnSegment.toFixed().toJson()
|
2024-04-13 13:30:30 +00:00
|
|
|
minDistance = Vec.Dist(nearestPointOnSegment, newPoint)
|
2023-04-25 11:01:25 +00:00
|
|
|
snapSegment = segment
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (nearestPoint) {
|
|
|
|
didSnap = true
|
|
|
|
newPoint = nearestPoint
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (didSnap && snapSegment) {
|
2023-08-02 18:12:25 +00:00
|
|
|
const transform = this.editor.getShapePageTransform(shape)!
|
2023-04-25 11:01:25 +00:00
|
|
|
const first = snapSegment.points[0]
|
|
|
|
const lastPoint = last(snapSegment.points)
|
|
|
|
if (!lastPoint) throw Error('Expected a last point!')
|
|
|
|
|
2024-01-03 12:13:15 +00:00
|
|
|
const A = Mat.applyToPoint(transform, first)
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2024-01-03 12:13:15 +00:00
|
|
|
const B = Mat.applyToPoint(transform, lastPoint)
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2024-01-03 12:13:15 +00:00
|
|
|
const snappedPoint = Mat.applyToPoint(transform, newPoint)
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2024-02-07 10:40:01 +00:00
|
|
|
this.editor.snaps.setIndicators([
|
2023-04-25 11:01:25 +00:00
|
|
|
{
|
|
|
|
id: uniqueId(),
|
|
|
|
type: 'points',
|
|
|
|
points: [A, snappedPoint, B],
|
|
|
|
},
|
|
|
|
])
|
|
|
|
} else {
|
2024-02-07 10:40:01 +00:00
|
|
|
this.editor.snaps.clearIndicators()
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
if (shouldSnapToAngle) {
|
|
|
|
// Snap line angle to nearest 15 degrees
|
2024-01-03 12:13:15 +00:00
|
|
|
const currentAngle = Vec.Angle(pagePointWhereCurrentSegmentChanged, currentPagePoint)
|
2023-04-25 11:01:25 +00:00
|
|
|
const snappedAngle = snapAngle(currentAngle, 24)
|
|
|
|
const angleDiff = snappedAngle - currentAngle
|
|
|
|
|
2024-01-03 12:13:15 +00:00
|
|
|
pagePoint = Vec.RotWith(
|
2023-04-25 11:01:25 +00:00
|
|
|
currentPagePoint,
|
|
|
|
pagePointWhereCurrentSegmentChanged,
|
|
|
|
angleDiff
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
pagePoint = currentPagePoint
|
|
|
|
}
|
|
|
|
|
2023-06-02 15:21:45 +00:00
|
|
|
newPoint = this.editor.getPointInShapeSpace(shape, pagePoint).toFixed().toJson()
|
2023-04-25 11:01:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// If the previous segment is a one point free shape and is the first segment of the line,
|
|
|
|
// then the user just did a click-and-immediately-press-shift to create a new straight line
|
|
|
|
// without continuing the previous line. In this case, we want to remove the previous segment.
|
|
|
|
|
2024-01-03 12:13:15 +00:00
|
|
|
this.currentLineLength += Vec.Dist(newSegment.points[0], newPoint)
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
newSegments[newSegments.length - 1] = {
|
|
|
|
...newSegment,
|
|
|
|
type: 'straight',
|
|
|
|
points: [newSegment.points[0], newPoint],
|
|
|
|
}
|
|
|
|
|
2023-06-13 18:02:17 +00:00
|
|
|
const shapePartial: TLShapePartial<DrawableShape> = {
|
|
|
|
id,
|
2023-07-07 13:56:31 +00:00
|
|
|
type: this.shapeType,
|
2023-06-13 18:02:17 +00:00
|
|
|
props: {
|
|
|
|
segments: newSegments,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.canClose()) {
|
|
|
|
;(shapePartial as TLShapePartial<TLDrawShape>).props!.isClosed = this.getIsClosed(
|
|
|
|
segments,
|
|
|
|
size
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-03-21 10:05:44 +00:00
|
|
|
this.editor.updateShapes([shapePartial], { squashing: true })
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
break
|
|
|
|
}
|
|
|
|
case 'free': {
|
|
|
|
const newSegments = segments.slice()
|
|
|
|
const newSegment = newSegments[newSegments.length - 1]
|
|
|
|
const newPoints = [...newSegment.points]
|
|
|
|
|
|
|
|
if (newPoints.length && this.mergeNextPoint) {
|
|
|
|
const { z } = newPoints[newPoints.length - 1]
|
|
|
|
newPoints[newPoints.length - 1] = {
|
|
|
|
x: newPoint.x,
|
|
|
|
y: newPoint.y,
|
|
|
|
z: z ? Math.max(z, newPoint.z) : newPoint.z,
|
|
|
|
}
|
|
|
|
// Note: we could recompute the line length here, but it's not really necessary
|
|
|
|
// this.currentLineLength = this.getLineLength(newSegments)
|
|
|
|
} else {
|
2024-01-03 12:13:15 +00:00
|
|
|
this.currentLineLength += Vec.Dist(newPoints[newPoints.length - 1], newPoint)
|
2023-04-25 11:01:25 +00:00
|
|
|
newPoints.push(newPoint)
|
|
|
|
}
|
|
|
|
|
|
|
|
newSegments[newSegments.length - 1] = {
|
|
|
|
...newSegment,
|
|
|
|
points: newPoints,
|
|
|
|
}
|
|
|
|
|
2024-04-13 13:30:30 +00:00
|
|
|
if (this.currentLineLength < STROKE_SIZES[shape.props.size] * 4) {
|
|
|
|
this.currentLineLength = this.getLineLength(newSegments)
|
|
|
|
}
|
2023-05-30 14:28:56 +00:00
|
|
|
|
2023-06-13 18:02:17 +00:00
|
|
|
const shapePartial: TLShapePartial<DrawableShape> = {
|
|
|
|
id,
|
2023-07-07 13:56:31 +00:00
|
|
|
type: this.shapeType,
|
2023-06-13 18:02:17 +00:00
|
|
|
props: {
|
|
|
|
segments: newSegments,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.canClose()) {
|
|
|
|
;(shapePartial as TLShapePartial<TLDrawShape>).props!.isClosed = this.getIsClosed(
|
|
|
|
newSegments,
|
|
|
|
size
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-03-21 10:05:44 +00:00
|
|
|
this.editor.updateShapes([shapePartial], { squashing: true })
|
|
|
|
|
|
|
|
// Set a maximum length for the lines array; after 200 points, complete the line.
|
2023-04-25 11:01:25 +00:00
|
|
|
if (newPoints.length > 500) {
|
2024-03-21 10:05:44 +00:00
|
|
|
this.editor.updateShapes([{ id, type: this.shapeType, props: { isComplete: true } }])
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2023-06-03 20:46:53 +00:00
|
|
|
const newShapeId = createShapeId()
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2023-06-13 18:02:17 +00:00
|
|
|
this.editor.createShapes<DrawableShape>([
|
2023-04-25 11:01:25 +00:00
|
|
|
{
|
|
|
|
id: newShapeId,
|
2023-07-07 13:56:31 +00:00
|
|
|
type: this.shapeType,
|
Input buffering (#3223)
This PR buffs input events.
## The story so far
In the olde days, we throttled events from the canvas events hook so
that a pointer event would only be sent every 1/60th of a second. This
was fine but made drawing on the iPad / 120FPS displays a little sad.
Then we removed this throttle. It seemed fine! Drawing at 120FPS was
great. We improved some rendering speeds and tightened some loops so
that the engine could keep up with 2x the number of points in a line.
Then we started noticing that iPads and other screens could start
choking on events as it received new inputs and tried to process and
render inputs while still recovering from a previous dropped frame. Even
worse, on iPad the work of rendering at 120FPS was causing the browser
to throttle the app after some sustained drawing. Yikes!
### Batching
I did an experimental PR (#3180) to bring back batching but do it in the
editor instead. What we would do is: rather than immediately processing
an event when we get it, we would instead put the event into a buffer.
On the next 60FPS tick, we would flush the buffer and process all of the
events. We'd have them all in the same transaction so that the app would
only render once.
### Render batching?
We then tried batching the renders, so that the app would only ever
render once per (next) frame. This added a bunch of complexity around
events that needed to happen synchronously, such as writing text in a
text field. Some inputs could "lag" in a way familiar to anyone who's
tried to update an input's state asynchronously. So we backed out of
this.
### Coalescing?
Another idea from @ds300 was to "coalesce" the events. This would be
useful because, while some interactions like drawing would require the
in-between frames in order to avoid data loss, most interactions (like
resizing) didn't actually need the in-between frames, they could just
use the last input of a given type.
Coalescing turned out to be trickier than we thought, though. Often a
state node required information from elsewhere in the app when
processing an event (such as camera position or page point, which is
derived from the camera position), and so the coalesced events would
need to also include this information or else the handlers wouldn't work
the way they should when processing the "final" event during a tick.
So we backed out of the coalescing strategy for now. Here's the [PR that
removes](https://github.com/tldraw/tldraw/pull/3223/commits/937469d69d4474fe9d1ff98604acb8f55a49f3fa)
it.
### Let's just buffer the fuckers
So this PR now should only include input buffering.
I think there are ways to achieve the same coalescing-like results
through the state nodes, which could gather information during the
`onPointerMove` handler and then actually make changes during the
`onTick` handler, so that the changes are only done as many time as
necessary. This should help with e.g. resizing lots of shapes at once.
But first let's land the buffering!
---
Mitja's original text:
This PR builds on top of Steve's [experiment
PR](https://github.com/tldraw/tldraw/pull/3180) here. It also adds event
coalescing for [`pointerMove`
events](https://github.com/tldraw/tldraw/blob/mitja/input-buffering/packages/editor/src/lib/editor/Editor.ts#L8364-L8368).
The API is [somewhat similar
](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/getCoalescedEvents)
to `getCoalescedEvent`. In `StateNodes` we register an `onPointerMove`
handler. When the event happens it gets called with the event `info`.
There's now an additional field on `TLMovePointerEvent` called
`coalescedInfo` which includes all the events. It's then on the user to
process all of these.
I decided on this API since it allows us to only expose one event
handler, but it still gives the users access to all events if they need
them.
We would otherwise either need to:
- Expose two events (coalesced and non-coalesced one and complicate the
api) so that state nodes like Resizing would not be triggered for each
pointer move.
- Offer some methods on the editor that would allow use to get the
coalesced information. Then the nodes that need that info could request
it. I [tried
this](https://github.com/tldraw/tldraw/pull/3223/commits/9ad973da3aa287e7974067ac923193530d29c188#diff-32f1de9a5a9ec72aa49a8d18a237fbfff301610f4689a4af6b37f47af435aafcR67),
but it didn't feel good.
This also complicated the editor inputs. The events need to store
information about the event (like the mouse position when the event
happened for `onPointerMove`). But we cannot immediately update inputs
when the event happens. To make this work for `pointerMove` events I've
added `pagePoint`. It's
[calculated](https://github.com/tldraw/tldraw/pull/3223/files#diff-980beb0aa0ee9aa6d1cd386cef3dc05a500c030638ffb58d45fd11b79126103fR71)
when the event triggers and then consumers can get it straight from the
event (like
[Drawing](https://github.com/tldraw/tldraw/pull/3223/files#diff-32f1de9a5a9ec72aa49a8d18a237fbfff301610f4689a4af6b37f47af435aafcR104)).
### Change Type
<!-- ❗ Please select a 'Scope' label ❗️ -->
- [x] `sdk` — Changes the tldraw SDK
- [ ] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff
<!-- ❗ Please select a 'Type' label ❗️ -->
- [ ] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [x] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know
### Test Plan
1. Add a step-by-step description of how to test your PR here.
4.
- [ ] Unit Tests
- [ ] End to end tests
### Release Notes
- Add a brief release note for your PR here.
---------
Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
2024-04-02 14:29:14 +00:00
|
|
|
x: toFixed(inputs.currentPagePoint.x),
|
|
|
|
y: toFixed(inputs.currentPagePoint.y),
|
2023-04-25 11:01:25 +00:00
|
|
|
props: {
|
|
|
|
isPen: this.isPen,
|
|
|
|
segments: [
|
|
|
|
{
|
|
|
|
type: 'free',
|
|
|
|
points: [{ x: 0, y: 0, z: this.isPen ? +(z! * 1.25).toFixed() : 0.5 }],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
])
|
|
|
|
|
`ShapeUtil.getGeometry`, selection rewrite (#1751)
This PR is a significant rewrite of our selection / hit testing logic.
It
- replaces our current geometric helpers (`getBounds`, `getOutline`,
`hitTestPoint`, and `hitTestLineSegment`) with a new geometry API
- moves our hit testing entirely to JS using geometry
- improves selection logic, especially around editing shapes, groups and
frames
- fixes many minor selection bugs (e.g. shapes behind frames)
- removes hit-testing DOM elements from ShapeFill etc.
- adds many new tests around selection
- adds new tests around selection
- makes several superficial changes to surface editor APIs
This PR is hard to evaluate. The `selection-omnibus` test suite is
intended to describe all of the selection behavior, however all existing
tests are also either here preserved and passing or (in a few cases
around editing shapes) are modified to reflect the new behavior.
## Geometry
All `ShapeUtils` implement `getGeometry`, which returns a single
geometry primitive (`Geometry2d`). For example:
```ts
class BoxyShapeUtil {
getGeometry(shape: BoxyShape) {
return new Rectangle2d({
width: shape.props.width,
height: shape.props.height,
isFilled: true,
margin: shape.props.strokeWidth
})
}
}
```
This geometric primitive is used for all bounds calculation, hit
testing, intersection with arrows, etc.
There are several geometric primitives that extend `Geometry2d`:
- `Arc2d`
- `Circle2d`
- `CubicBezier2d`
- `CubicSpline2d`
- `Edge2d`
- `Ellipse2d`
- `Group2d`
- `Polygon2d`
- `Rectangle2d`
- `Stadium2d`
For shapes that have more complicated geometric representations, such as
an arrow with a label, the `Group2d` can accept other primitives as its
children.
## Hit testing
Previously, we did all hit testing via events set on shapes and other
elements. In this PR, I've replaced those hit tests with our own
calculation for hit tests in JavaScript. This removed the need for many
DOM elements, such as hit test area borders and fills which only existed
to trigger pointer events.
## Selection
We now support selecting "hollow" shapes by clicking inside of them.
This involves a lot of new logic but it should work intuitively. See
`Editor.getShapeAtPoint` for the (thoroughly commented) implementation.
![Kapture 2023-07-23 at 23 27
27](https://github.com/tldraw/tldraw/assets/23072548/a743275c-acdb-42d9-a3fe-b3e20dce86b6)
every sunset is actually the sun hiding in fear and respect of tldraw's
quality of interactions
This PR also fixes several bugs with scribble selection, in particular
around the shift key modifier.
![Kapture 2023-07-24 at 23 34
07](https://github.com/tldraw/tldraw/assets/23072548/871d67d0-8d06-42ae-a2b2-021effba37c5)
...as well as issues with labels and editing.
There are **over 100 new tests** for selection covering groups, frames,
brushing, scribbling, hovering, and editing. I'll add a few more before
I feel comfortable merging this PR.
## Arrow binding
Using the same "hollow shape" logic as selection, arrow binding is
significantly improved.
![Kapture 2023-07-22 at 07 46
25](https://github.com/tldraw/tldraw/assets/23072548/5aa724b3-b57d-4fb7-92d0-80e34246753c)
a thousand wise men could not improve on this
## Moving focus between editing shapes
Previously, this was handled in the `editing_shapes` state. This is
moved to `useEditableText`, and should generally be considered an
advanced implementation detail on a shape-by-shape basis. This addresses
a bug that I'd never noticed before, but which can be reproduced by
selecting an shape—but not focusing its input—while editing a different
shape. Previously, the new shape became the editing shape but its input
did not focus.
![Kapture 2023-07-23 at 23 19
09](https://github.com/tldraw/tldraw/assets/23072548/a5e157fb-24a8-42bd-a692-04ce769b1a9c)
In this PR, you can select a shape by clicking on its edge or body, or
select its input to transfer editing / focus.
![Kapture 2023-07-23 at 23 22
21](https://github.com/tldraw/tldraw/assets/23072548/7384e7ea-9777-4e1a-8f63-15de2166a53a)
tldraw, glorious tldraw
### Change Type
- [x] `major` — Breaking change
### Test Plan
1. Erase shapes
2. Select shapes
3. Calculate their bounding boxes
- [ ] Unit Tests // todo
- [ ] End to end tests // todo
### Release Notes
- [editor] Remove `ShapeUtil.getBounds`, `ShapeUtil.getOutline`,
`ShapeUtil.hitTestPoint`, `ShapeUtil.hitTestLineSegment`
- [editor] Add `ShapeUtil.getGeometry`
- [editor] Add `Editor.getShapeGeometry`
2023-07-25 16:10:15 +00:00
|
|
|
this.initialShape = structuredClone(this.editor.getShape<DrawableShape>(newShapeId)!)
|
2023-04-25 11:01:25 +00:00
|
|
|
this.mergeNextPoint = false
|
Input buffering (#3223)
This PR buffs input events.
## The story so far
In the olde days, we throttled events from the canvas events hook so
that a pointer event would only be sent every 1/60th of a second. This
was fine but made drawing on the iPad / 120FPS displays a little sad.
Then we removed this throttle. It seemed fine! Drawing at 120FPS was
great. We improved some rendering speeds and tightened some loops so
that the engine could keep up with 2x the number of points in a line.
Then we started noticing that iPads and other screens could start
choking on events as it received new inputs and tried to process and
render inputs while still recovering from a previous dropped frame. Even
worse, on iPad the work of rendering at 120FPS was causing the browser
to throttle the app after some sustained drawing. Yikes!
### Batching
I did an experimental PR (#3180) to bring back batching but do it in the
editor instead. What we would do is: rather than immediately processing
an event when we get it, we would instead put the event into a buffer.
On the next 60FPS tick, we would flush the buffer and process all of the
events. We'd have them all in the same transaction so that the app would
only render once.
### Render batching?
We then tried batching the renders, so that the app would only ever
render once per (next) frame. This added a bunch of complexity around
events that needed to happen synchronously, such as writing text in a
text field. Some inputs could "lag" in a way familiar to anyone who's
tried to update an input's state asynchronously. So we backed out of
this.
### Coalescing?
Another idea from @ds300 was to "coalesce" the events. This would be
useful because, while some interactions like drawing would require the
in-between frames in order to avoid data loss, most interactions (like
resizing) didn't actually need the in-between frames, they could just
use the last input of a given type.
Coalescing turned out to be trickier than we thought, though. Often a
state node required information from elsewhere in the app when
processing an event (such as camera position or page point, which is
derived from the camera position), and so the coalesced events would
need to also include this information or else the handlers wouldn't work
the way they should when processing the "final" event during a tick.
So we backed out of the coalescing strategy for now. Here's the [PR that
removes](https://github.com/tldraw/tldraw/pull/3223/commits/937469d69d4474fe9d1ff98604acb8f55a49f3fa)
it.
### Let's just buffer the fuckers
So this PR now should only include input buffering.
I think there are ways to achieve the same coalescing-like results
through the state nodes, which could gather information during the
`onPointerMove` handler and then actually make changes during the
`onTick` handler, so that the changes are only done as many time as
necessary. This should help with e.g. resizing lots of shapes at once.
But first let's land the buffering!
---
Mitja's original text:
This PR builds on top of Steve's [experiment
PR](https://github.com/tldraw/tldraw/pull/3180) here. It also adds event
coalescing for [`pointerMove`
events](https://github.com/tldraw/tldraw/blob/mitja/input-buffering/packages/editor/src/lib/editor/Editor.ts#L8364-L8368).
The API is [somewhat similar
](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/getCoalescedEvents)
to `getCoalescedEvent`. In `StateNodes` we register an `onPointerMove`
handler. When the event happens it gets called with the event `info`.
There's now an additional field on `TLMovePointerEvent` called
`coalescedInfo` which includes all the events. It's then on the user to
process all of these.
I decided on this API since it allows us to only expose one event
handler, but it still gives the users access to all events if they need
them.
We would otherwise either need to:
- Expose two events (coalesced and non-coalesced one and complicate the
api) so that state nodes like Resizing would not be triggered for each
pointer move.
- Offer some methods on the editor that would allow use to get the
coalesced information. Then the nodes that need that info could request
it. I [tried
this](https://github.com/tldraw/tldraw/pull/3223/commits/9ad973da3aa287e7974067ac923193530d29c188#diff-32f1de9a5a9ec72aa49a8d18a237fbfff301610f4689a4af6b37f47af435aafcR67),
but it didn't feel good.
This also complicated the editor inputs. The events need to store
information about the event (like the mouse position when the event
happened for `onPointerMove`). But we cannot immediately update inputs
when the event happens. To make this work for `pointerMove` events I've
added `pagePoint`. It's
[calculated](https://github.com/tldraw/tldraw/pull/3223/files#diff-980beb0aa0ee9aa6d1cd386cef3dc05a500c030638ffb58d45fd11b79126103fR71)
when the event triggers and then consumers can get it straight from the
event (like
[Drawing](https://github.com/tldraw/tldraw/pull/3223/files#diff-32f1de9a5a9ec72aa49a8d18a237fbfff301610f4689a4af6b37f47af435aafcR104)).
### Change Type
<!-- ❗ Please select a 'Scope' label ❗️ -->
- [x] `sdk` — Changes the tldraw SDK
- [ ] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff
<!-- ❗ Please select a 'Type' label ❗️ -->
- [ ] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [x] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know
### Test Plan
1. Add a step-by-step description of how to test your PR here.
4.
- [ ] Unit Tests
- [ ] End to end tests
### Release Notes
- Add a brief release note for your PR here.
---------
Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
2024-04-02 14:29:14 +00:00
|
|
|
this.lastRecordedPoint = inputs.currentPagePoint.clone()
|
2023-04-25 11:01:25 +00:00
|
|
|
this.currentLineLength = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private getLineLength(segments: TLDrawShapeSegment[]) {
|
|
|
|
let length = 0
|
|
|
|
|
|
|
|
for (const segment of segments) {
|
|
|
|
for (let i = 0; i < segment.points.length - 1; i++) {
|
|
|
|
const A = segment.points[i]
|
|
|
|
const B = segment.points[i + 1]
|
2024-04-13 13:30:30 +00:00
|
|
|
length += Vec.Dist2(B, A)
|
2023-04-25 11:01:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return Math.sqrt(length)
|
|
|
|
}
|
|
|
|
|
|
|
|
override onPointerUp: TLEventHandlers['onPointerUp'] = () => {
|
|
|
|
this.complete()
|
|
|
|
}
|
|
|
|
|
|
|
|
override onCancel: TLEventHandlers['onCancel'] = () => {
|
|
|
|
this.cancel()
|
|
|
|
}
|
|
|
|
|
|
|
|
override onComplete: TLEventHandlers['onComplete'] = () => {
|
|
|
|
this.complete()
|
|
|
|
}
|
|
|
|
|
|
|
|
override onInterrupt: TLEventHandlers['onInterrupt'] = () => {
|
2023-06-02 15:21:45 +00:00
|
|
|
if (this.editor.inputs.isDragging) {
|
2023-04-25 11:01:25 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-09-18 10:50:15 +00:00
|
|
|
if (this.markId) {
|
|
|
|
this.editor.bailToMark(this.markId)
|
|
|
|
}
|
2023-04-25 11:01:25 +00:00
|
|
|
this.cancel()
|
|
|
|
}
|
|
|
|
|
|
|
|
complete() {
|
|
|
|
// If we weren't focused when the drawing shape started, and if
|
|
|
|
// we haven't dragged far enough to start dragging, then don't do
|
|
|
|
// anything here. Most likely we clicked back into the canvas from
|
|
|
|
// a menu or other UI element.
|
|
|
|
if (!this.canDraw) {
|
|
|
|
this.cancel()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const { initialShape } = this
|
|
|
|
if (!initialShape) return
|
2023-06-02 15:21:45 +00:00
|
|
|
this.editor.updateShapes([
|
2023-04-25 11:01:25 +00:00
|
|
|
{ id: initialShape.id, type: initialShape.type, props: { isComplete: true } },
|
|
|
|
])
|
|
|
|
|
2023-11-14 13:02:50 +00:00
|
|
|
this.parent.transition('idle')
|
2023-04-25 11:01:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
cancel() {
|
|
|
|
this.parent.transition('idle', this.info)
|
|
|
|
}
|
|
|
|
}
|