Tldraw/packages/tldraw/src/lib/utils/tldr/buildFromV1Document.ts

1173 wiersze
29 KiB
TypeScript
Czysty Zwykły widok Historia

2023-04-25 11:01:25 +00:00
import {
AssetRecordType,
Editor,
PageRecordType,
2023-04-25 11:01:25 +00:00
TLArrowShape,
TLArrowShapeArrowheadStyle,
TLArrowShapeTerminal,
2023-04-25 11:01:25 +00:00
TLAsset,
TLAssetId,
TLDefaultColorStyle,
TLDefaultDashStyle,
TLDefaultFontStyle,
TLDefaultHorizontalAlignStyle,
TLDefaultSizeStyle,
2023-04-25 11:01:25 +00:00
TLDrawShape,
TLGeoShape,
TLImageShape,
TLNoteShape,
TLPageId,
TLShapeId,
TLTextShape,
TLVideoShape,
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
clamp,
createShapeId,
} from '@tldraw/editor'
2024-04-27 18:07:01 +00:00
import { tldrawConstants } from '../../tldraw-constants'
const { MAX_SHAPES_PER_PAGE } = tldrawConstants
2023-04-25 11:01:25 +00:00
const TLDRAW_V1_VERSION = 15.5
/** @internal */
export function buildFromV1Document(editor: Editor, document: LegacyTldrawDocument) {
editor.batch(() => {
2023-04-25 11:01:25 +00:00
document = migrate(document, TLDRAW_V1_VERSION)
// Cancel any interactions / states
editor.cancel().cancel().cancel().cancel()
2023-04-25 11:01:25 +00:00
const firstPageId = editor.getPages()[0].id
2023-04-25 11:01:25 +00:00
// Set the current page to the first page
`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
editor.setCurrentPage(firstPageId)
2023-04-25 11:01:25 +00:00
// Delete all pages except first page
for (const page of editor.getPages().slice(1)) {
editor.deletePage(page.id)
2023-04-25 11:01:25 +00:00
}
// Delete all of the shapes on the current page
editor.selectAll()
editor.deleteShapes(editor.getSelectedShapeIds())
2023-04-25 11:01:25 +00:00
// Create assets
const v1AssetIdsToV2AssetIds = new Map<string, TLAssetId>()
Object.values(document.assets ?? {}).forEach((v1Asset) => {
switch (v1Asset.type) {
case TDAssetType.Image: {
const assetId: TLAssetId = AssetRecordType.createId()
2023-04-25 11:01:25 +00:00
v1AssetIdsToV2AssetIds.set(v1Asset.id, assetId)
const placeholderAsset: TLAsset = {
id: assetId,
typeName: 'asset',
type: 'image',
props: {
w: coerceDimension(v1Asset.size[0]),
h: coerceDimension(v1Asset.size[1]),
name: v1Asset.fileName ?? 'Untitled',
isAnimated: false,
mimeType: null,
src: v1Asset.src,
},
meta: {},
2023-04-25 11:01:25 +00:00
}
editor.createAssets([placeholderAsset])
tryMigrateAsset(editor, placeholderAsset)
2023-04-25 11:01:25 +00:00
break
}
case TDAssetType.Video:
{
const assetId: TLAssetId = AssetRecordType.createId()
2023-04-25 11:01:25 +00:00
v1AssetIdsToV2AssetIds.set(v1Asset.id, assetId)
editor.createAssets([
2023-04-25 11:01:25 +00:00
{
id: assetId,
typeName: 'asset',
type: 'video',
props: {
w: coerceDimension(v1Asset.size[0]),
h: coerceDimension(v1Asset.size[1]),
name: v1Asset.fileName ?? 'Untitled',
isAnimated: true,
mimeType: null,
src: v1Asset.src,
},
meta: {},
2023-04-25 11:01:25 +00:00
},
])
}
break
}
})
// Create pages
const v1PageIdsToV2PageIds = new Map<string, TLPageId>()
Object.values(document.pages ?? {})
.sort((a, b) => ((a.childIndex ?? 1) < (b.childIndex ?? 1) ? -1 : 1))
.forEach((v1Page, i) => {
if (i === 0) {
v1PageIdsToV2PageIds.set(v1Page.id, editor.getCurrentPageId())
2023-04-25 11:01:25 +00:00
} else {
const pageId = PageRecordType.createId()
2023-04-25 11:01:25 +00:00
v1PageIdsToV2PageIds.set(v1Page.id, pageId)
editor.createPage({ name: v1Page.name ?? 'Page', id: pageId })
2023-04-25 11:01:25 +00:00
}
})
Object.values(document.pages ?? {})
.sort((a, b) => ((a.childIndex ?? 1) < (b.childIndex ?? 1) ? -1 : 1))
.forEach((v1Page) => {
// Set the current page id to the current page
`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
editor.setCurrentPage(v1PageIdsToV2PageIds.get(v1Page.id)!)
2023-04-25 11:01:25 +00:00
const v1ShapeIdsToV2ShapeIds = new Map<string, TLShapeId>()
const v1GroupShapeIdsToV1ChildIds = new Map<string, string[]>()
const v1Shapes = Object.values(v1Page.shapes ?? {})
.sort((a, b) => (a.childIndex < b.childIndex ? -1 : 1))
.slice(0, MAX_SHAPES_PER_PAGE)
// Groups only
v1Shapes.forEach((v1Shape) => {
if (v1Shape.type !== TDShapeType.Group) return
const shapeId = createShapeId()
2023-04-25 11:01:25 +00:00
v1ShapeIdsToV2ShapeIds.set(v1Shape.id, shapeId)
v1GroupShapeIdsToV1ChildIds.set(v1Shape.id, [])
})
function decideNotToCreateShape(v1Shape: TDShape) {
v1ShapeIdsToV2ShapeIds.delete(v1Shape.id)
const v1GroupParent = v1GroupShapeIdsToV1ChildIds.has(v1Shape.parentId)
if (v1GroupParent) {
const ids = v1GroupShapeIdsToV1ChildIds
.get(v1Shape.parentId)!
.filter((id) => id !== v1Shape.id)
v1GroupShapeIdsToV1ChildIds.set(v1Shape.parentId, ids)
}
}
// Non-groups only
v1Shapes.forEach((v1Shape) => {
// Skip groups for now, we'll create groups via the app's API
if (v1Shape.type === TDShapeType.Group) {
return
}
const shapeId = createShapeId()
2023-04-25 11:01:25 +00:00
v1ShapeIdsToV2ShapeIds.set(v1Shape.id, shapeId)
if (v1Shape.parentId !== v1Page.id) {
// If the parent is a group, then add the shape to the group's children
if (v1GroupShapeIdsToV1ChildIds.has(v1Shape.parentId)) {
v1GroupShapeIdsToV1ChildIds.get(v1Shape.parentId)!.push(v1Shape.id)
} else {
console.warn('parent does not exist', v1Shape)
}
}
// First, try to find the shape's parent among the existing groups
const parentId = v1PageIdsToV2PageIds.get(v1Page.id)!
const inCommon = {
id: shapeId,
parentId,
x: coerceNumber(v1Shape.point[0]),
y: coerceNumber(v1Shape.point[1]),
rotation: 0,
isLocked: !!v1Shape.isLocked,
}
switch (v1Shape.type) {
case TDShapeType.Sticky: {
editor.createShapes<TLNoteShape>([
{
...inCommon,
type: 'note',
props: {
text: v1Shape.text ?? '',
color: getV2Color(v1Shape.style.color),
size: getV2Size(v1Shape.style.size),
font: getV2Font(v1Shape.style.font),
align: getV2Align(v1Shape.style.textAlign),
},
2023-04-25 11:01:25 +00:00
},
])
2023-04-25 11:01:25 +00:00
break
}
case TDShapeType.Rectangle: {
editor.createShapes<TLGeoShape>([
{
...inCommon,
type: 'geo',
props: {
geo: 'rectangle',
w: coerceDimension(v1Shape.size[0]),
h: coerceDimension(v1Shape.size[1]),
text: v1Shape.label ?? '',
fill: getV2Fill(v1Shape.style.isFilled, v1Shape.style.color),
labelColor: getV2Color(v1Shape.style.color),
color: getV2Color(v1Shape.style.color),
size: getV2Size(v1Shape.style.size),
font: getV2Font(v1Shape.style.font),
dash: getV2Dash(v1Shape.style.dash),
align: 'middle',
},
2023-04-25 11:01:25 +00:00
},
])
2023-04-25 11:01:25 +00:00
const pageBoundsBeforeLabel = editor.getShapePageBounds(inCommon.id)!
2023-04-25 11:01:25 +00:00
editor.updateShapes([
2023-04-25 11:01:25 +00:00
{
id: inCommon.id,
type: 'geo',
props: {
text: v1Shape.label ?? '',
},
},
])
if (pageBoundsBeforeLabel.width === pageBoundsBeforeLabel.height) {
`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 = editor.getShape<TLGeoShape>(inCommon.id)!
2023-04-25 11:01:25 +00:00
const { growY } = shape.props
const w = coerceDimension(shape.props.w)
const h = coerceDimension(shape.props.h)
const newW = w + growY / 2
const newH = h + growY / 2
editor.updateShapes([
2023-04-25 11:01:25 +00:00
{
id: inCommon.id,
type: 'geo',
x: coerceNumber(shape.x) - (newW - w) / 2,
y: coerceNumber(shape.y) - (newH - h) / 2,
props: {
w: newW,
h: newH,
},
},
])
}
break
}
case TDShapeType.Triangle: {
editor.createShapes<TLGeoShape>([
{
...inCommon,
type: 'geo',
props: {
geo: 'triangle',
w: coerceDimension(v1Shape.size[0]),
h: coerceDimension(v1Shape.size[1]),
fill: getV2Fill(v1Shape.style.isFilled, v1Shape.style.color),
labelColor: getV2Color(v1Shape.style.color),
color: getV2Color(v1Shape.style.color),
size: getV2Size(v1Shape.style.size),
font: getV2Font(v1Shape.style.font),
dash: getV2Dash(v1Shape.style.dash),
align: 'middle',
},
2023-04-25 11:01:25 +00:00
},
])
2023-04-25 11:01:25 +00:00
const pageBoundsBeforeLabel = editor.getShapePageBounds(inCommon.id)!
2023-04-25 11:01:25 +00:00
editor.updateShapes([
2023-04-25 11:01:25 +00:00
{
id: inCommon.id,
type: 'geo',
props: {
text: v1Shape.label ?? '',
},
},
])
if (pageBoundsBeforeLabel.width === pageBoundsBeforeLabel.height) {
`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 = editor.getShape<TLGeoShape>(inCommon.id)!
2023-04-25 11:01:25 +00:00
const { growY } = shape.props
const w = coerceDimension(shape.props.w)
const h = coerceDimension(shape.props.h)
const newW = w + growY / 2
const newH = h + growY / 2
editor.updateShapes([
2023-04-25 11:01:25 +00:00
{
id: inCommon.id,
type: 'geo',
x: coerceNumber(shape.x) - (newW - w) / 2,
y: coerceNumber(shape.y) - (newH - h) / 2,
props: {
w: newW,
h: newH,
},
},
])
}
break
}
case TDShapeType.Ellipse: {
editor.createShapes<TLGeoShape>([
{
...inCommon,
type: 'geo',
props: {
geo: 'ellipse',
w: coerceDimension(v1Shape.radius[0]) * 2,
h: coerceDimension(v1Shape.radius[1]) * 2,
fill: getV2Fill(v1Shape.style.isFilled, v1Shape.style.color),
labelColor: getV2Color(v1Shape.style.color),
color: getV2Color(v1Shape.style.color),
size: getV2Size(v1Shape.style.size),
font: getV2Font(v1Shape.style.font),
dash: getV2Dash(v1Shape.style.dash),
align: 'middle',
},
2023-04-25 11:01:25 +00:00
},
])
2023-04-25 11:01:25 +00:00
const pageBoundsBeforeLabel = editor.getShapePageBounds(inCommon.id)!
2023-04-25 11:01:25 +00:00
editor.updateShapes([
2023-04-25 11:01:25 +00:00
{
id: inCommon.id,
type: 'geo',
props: {
text: v1Shape.label ?? '',
},
},
])
if (pageBoundsBeforeLabel.width === pageBoundsBeforeLabel.height) {
`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 = editor.getShape<TLGeoShape>(inCommon.id)!
2023-04-25 11:01:25 +00:00
const { growY } = shape.props
const w = coerceDimension(shape.props.w)
const h = coerceDimension(shape.props.h)
const newW = w + growY / 2
const newH = h + growY / 2
editor.updateShapes([
2023-04-25 11:01:25 +00:00
{
id: inCommon.id,
type: 'geo',
x: coerceNumber(shape.x) - (newW - w) / 2,
y: coerceNumber(shape.y) - (newH - h) / 2,
props: {
w: newW,
h: newH,
},
},
])
}
break
}
case TDShapeType.Draw: {
if (v1Shape.points.length === 0) {
decideNotToCreateShape(v1Shape)
break
}
editor.createShapes<TLDrawShape>([
{
...inCommon,
type: 'draw',
props: {
fill: getV2Fill(v1Shape.style.isFilled, v1Shape.style.color),
color: getV2Color(v1Shape.style.color),
size: getV2Size(v1Shape.style.size),
dash: getV2Dash(v1Shape.style.dash),
isPen: false,
isComplete: v1Shape.isComplete,
segments: [{ type: 'free', points: v1Shape.points.map(getV2Point) }],
},
2023-04-25 11:01:25 +00:00
},
])
2023-04-25 11:01:25 +00:00
break
}
case TDShapeType.Arrow: {
const v1Bend = coerceNumber(v1Shape.bend)
const v1Start = getV2Point(v1Shape.handles.start.point)
const v1End = getV2Point(v1Shape.handles.end.point)
const dist = Vec.Dist(v1Start, v1End)
2023-04-25 11:01:25 +00:00
const v2Bend = (dist * -v1Bend) / 2
// Could also be a line... but we'll use it as an arrow anyway
editor.createShapes<TLArrowShape>([
{
...inCommon,
type: 'arrow',
props: {
text: v1Shape.label ?? '',
color: getV2Color(v1Shape.style.color),
labelColor: getV2Color(v1Shape.style.color),
size: getV2Size(v1Shape.style.size),
font: getV2Font(v1Shape.style.font),
dash: getV2Dash(v1Shape.style.dash),
arrowheadStart: getV2Arrowhead(v1Shape.decorations?.start),
arrowheadEnd: getV2Arrowhead(v1Shape.decorations?.end),
start: {
type: 'point',
x: coerceNumber(v1Shape.handles.start.point[0]),
y: coerceNumber(v1Shape.handles.start.point[1]),
},
end: {
type: 'point',
x: coerceNumber(v1Shape.handles.end.point[0]),
y: coerceNumber(v1Shape.handles.end.point[1]),
},
bend: v2Bend,
2023-04-25 11:01:25 +00:00
},
},
])
2023-04-25 11:01:25 +00:00
break
}
case TDShapeType.Text: {
editor.createShapes<TLTextShape>([
{
...inCommon,
type: 'text',
props: {
text: v1Shape.text ?? ' ',
color: getV2Color(v1Shape.style.color),
size: getV2TextSize(v1Shape.style.size),
font: getV2Font(v1Shape.style.font),
align: getV2Align(v1Shape.style.textAlign),
scale: v1Shape.style.scale ?? 1,
},
2023-04-25 11:01:25 +00:00
},
])
2023-04-25 11:01:25 +00:00
break
}
case TDShapeType.Image: {
const assetId = v1AssetIdsToV2AssetIds.get(v1Shape.assetId)
if (!assetId) {
console.warn('Could not find asset id', v1Shape.assetId)
return
}
editor.createShapes<TLImageShape>([
{
...inCommon,
type: 'image',
props: {
w: coerceDimension(v1Shape.size[0]),
h: coerceDimension(v1Shape.size[1]),
assetId,
},
2023-04-25 11:01:25 +00:00
},
])
2023-04-25 11:01:25 +00:00
break
}
case TDShapeType.Video: {
const assetId = v1AssetIdsToV2AssetIds.get(v1Shape.assetId)
if (!assetId) {
console.warn('Could not find asset id', v1Shape.assetId)
return
}
editor.createShapes<TLVideoShape>([
{
...inCommon,
type: 'video',
props: {
w: coerceDimension(v1Shape.size[0]),
h: coerceDimension(v1Shape.size[1]),
assetId,
},
2023-04-25 11:01:25 +00:00
},
])
2023-04-25 11:01:25 +00:00
break
}
}
const rotation = coerceNumber(v1Shape.rotation)
if (rotation !== 0) {
editor.select(shapeId)
editor.rotateShapesBy([shapeId], rotation)
2023-04-25 11:01:25 +00:00
}
})
// Create groups
v1GroupShapeIdsToV1ChildIds.forEach((v1ChildIds, v1GroupId) => {
const v2ChildShapeIds = v1ChildIds.map((id) => v1ShapeIdsToV2ShapeIds.get(id)!)
const v2GroupId = v1ShapeIdsToV2ShapeIds.get(v1GroupId)!
editor.groupShapes(v2ChildShapeIds, v2GroupId)
2023-04-25 11:01:25 +00:00
const v1Group = v1Page.shapes[v1GroupId]
const rotation = coerceNumber(v1Group.rotation)
if (rotation !== 0) {
editor.select(v2GroupId)
editor.rotateShapesBy([v2GroupId], rotation)
2023-04-25 11:01:25 +00:00
}
})
// Bind arrows to shapes
v1Shapes.forEach((v1Shape) => {
if (v1Shape.type !== TDShapeType.Arrow) {
return
}
const v2ShapeId = v1ShapeIdsToV2ShapeIds.get(v1Shape.id)!
[refactor] reduce dependencies on shape utils in editor (#1693) We'd like to make the @tldraw/editor layer more independent of specific shapes. Unfortunately there are many places where shape types and certain shape behavior is deeply embedded in the Editor. This PR begins to refactor out dependencies between the editor library and shape utils. It does this in two ways: - removing shape utils from the arguments of `isShapeOfType`, replacing with a generic - removing shape utils from the arguments of `getShapeUtil`, replacing with a generic - moving custom arrow info cache out of the util and into the editor class - changing the a tool's `shapeType` to be a string instead of a shape util We're here trading type safety based on inferred types—"hey editor, give me your instance of this shape util class"—for knowledge at the point of call—"hey editor, give me a shape util class of this type; and trust me it'll be an instance this shape util class". Likewise for shapes. ### A note on style We haven't really established our conventions or style when it comes to types, but I'm increasingly of the opinion that we should defer to the point of call to narrow a type based on generics (keeping the types in typescript land) rather than using arguments, which blur into JavaScript land. ### Change Type - [x] `major` — Breaking change ### Test Plan - [x] Unit Tests ### Release Notes - removes shape utils from the arguments of `isShapeOfType`, replacing with a generic - removes shape utils from the arguments of `getShapeUtil`, replacing with a generic - moves custom arrow info cache out of the util and into the editor class - changes the a tool's `shapeType` to be a string instead of a shape util
2023-07-07 13:56:31 +00:00
const util = editor.getShapeUtil<TLArrowShape>('arrow')
2023-04-25 11:01:25 +00:00
// dumb but necessary
editor.inputs.ctrlKey = false
2023-04-25 11:01:25 +00:00
for (const handleId of ['start', 'end'] as const) {
const bindingId = v1Shape.handles[handleId].bindingId
if (bindingId) {
const binding = v1Page.bindings[bindingId]
if (!binding) {
// arrow has a reference to a binding that no longer exists
continue
}
const targetId = v1ShapeIdsToV2ShapeIds.get(binding.toId)!
`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 targetShape = editor.getShape(targetId)!
2023-04-25 11:01:25 +00:00
// (unexpected) We didn't create the target shape
if (!targetShape) continue
if (targetId) {
const bounds = editor.getShapePageBounds(targetId)!
2023-04-25 11:01:25 +00:00
`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 v2ShapeFresh = editor.getShape<TLArrowShape>(v2ShapeId)!
2023-04-25 11:01:25 +00:00
const nx = clamp((coerceNumber(binding.point[0]) + 0.5) / 2, 0.2, 0.8)
const ny = clamp((coerceNumber(binding.point[1]) + 0.5) / 2, 0.2, 0.8)
const point = editor.getPointInShapeSpace(v2ShapeFresh, {
2023-04-25 11:01:25 +00:00
x: bounds.minX + bounds.width * nx,
y: bounds.minY + bounds.height * ny,
})
const handles = editor.getShapeHandles(v2ShapeFresh)!
arrows: add ability to change label placement (#2557) This adds the ability to drag the label on an arrow to a different location within the line segment/arc. https://github.com/tldraw/tldraw/assets/469604/dbd2ee35-bebc-48d6-b8ee-fcf12ce91fa5 - A lot of the complexity lay in ensuring a fixed distance from the ends of the arrowheads. - I added a new type of handle `text-adjust` that makes the text box the very handle itself. - I added a `ARROW_HANDLES` enum - we should use more enums! - The bulk of the changes are in ArrowShapeUtil — check that out in particular obviously :) Along the way, I tried to improve a couple spots as I touched them: - added some more documentation to Vec.ts because some of the functions in there were obscure/new to me. (at least the naming, hah) - added `getPointOnCircle` which was being done in a couple places independently and refactored those places. ### Questions - the `getPointOnCircle` API changed. Is this considered breaking and/or should I leave the signature the same? Wasn't sure if it was a big deal or not. - I made `labelPosition` in the schema always but I guess it could have been optional? Lemme know if there's a preference. - Any feedback on tests? Happy to expand those if necessary. ### Change Type - [ ] `patch` — Bug fix - [x] `minor` — New feature - [ ] `major` — Breaking change - [ ] `dependencies` — Changes to package dependencies[^1] - [ ] `documentation` — Changes to the documentation only[^2] - [ ] `tests` — Changes to any test code only[^2] - [ ] `internal` — Any other changes that don't affect the published package[^2] - [ ] I don't know [^1]: publishes a `patch` release, for devDependencies use `internal` [^2]: will not publish a new version ### Test Plan 1. For arrow in [straightArrow, curvedArrow] test the following: a. Label in the middle b. Label at both ends of the arrow c. Test arrows in different directions d. Rotating the endpoints and seeing that the label stays at the end of the arrow at a fixed width. e. Test different stroke widths. f. Test with different arrowheads. 2. Also, test arcs that are more circle like than arc-like. - [x] Unit Tests - [ ] End to end tests ### Release Notes - Adds ability to change label position on arrows. --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com> Co-authored-by: alex <alex@dytry.ch>
2024-01-24 10:19:20 +00:00
const change = util.onHandleDrag!(v2ShapeFresh, {
2023-04-25 11:01:25 +00:00
handle: {
...handles.find((h) => h.id === handleId)!,
x: point.x,
y: point.y,
},
isPrecise: point.x !== 0.5 || point.y !== 0.5,
})
if (change) {
if (change.props?.[handleId]) {
const terminal = change.props?.[handleId] as TLArrowShapeTerminal
2023-04-25 11:01:25 +00:00
if (terminal.type === 'binding') {
terminal.isExact = binding.distance === 0
if (terminal.boundShapeId !== targetId) {
console.warn('Hit the wrong shape!')
terminal.boundShapeId = targetId
terminal.normalizedAnchor = { x: 0.5, y: 0.5 }
}
}
}
editor.updateShapes([change])
2023-04-25 11:01:25 +00:00
}
}
}
}
})
})
// Set the current page to the first page again
`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
editor.setCurrentPage(firstPageId)
2023-04-25 11:01:25 +00:00
editor.history.clear()
editor.selectNone()
2023-04-25 11:01:25 +00:00
const bounds = editor.getCurrentPageBounds()
2023-04-25 11:01:25 +00:00
if (bounds) {
Add component for viewing an image of a snapshot (#2804) This PR adds the `TldrawImage` component that displays a tldraw snapshot as an SVG image. ![2024-02-15 at 12 29 52 - Coral Cod](https://github.com/tldraw/tldraw/assets/15892272/14140e9e-7d6d-4dd3-88a3-86a6786325c5) ## Why We've seen requests for this kind of thing from users. eg: GitBook, and on discord: <img width="710" alt="image" src="https://github.com/tldraw/tldraw/assets/15892272/3d3a3e9d-66b9-42e7-81de-a70aa7165bdc"> The component provides a way to do that. This PR also untangles various bits of editor state from image exporting, which makes it easier for library users to export images more agnostically. (ie: they can now export any shapes on any page in any theme. previously, they had to change the user's state to do that). ## What else - This PR also adds an **Image snapshot** example to demonstrate the new component. - We now pass an `isDarkMode` property to the `toSvg` method (inside the `ctx` argument). This means that `toSvg` doesn't have to rely on editor state anymore. I updated all our `toSvg` methods to use it. - See code comments for more info. ## Any issues? When you toggle to editing mode in the new example, text measurements are initially wrong (until you edit the size of a text shape). Click on the text shape to see how its indicator is wrong. Not sure why this is, or if it's even related. Does it ring a bell with anyone? If not, I'll take a closer look. (fixed, see comments --steve) ## Future work Now that we've untangled image exporting from editor state, we could expose some more helpful helpers for making this easier. Fixes tld-2122 ### Change Type - [x] `minor` — New feature [^1]: publishes a `patch` release, for devDependencies use `internal` [^2]: will not publish a new version ### Test Plan 1. Open the **Image snapshot** example. 2. Try editing the image, saving the image, and making sure the image updates. - [ ] Unit Tests - [ ] End to end tests ### Release Notes - Dev: Added the `TldrawImage` component. --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
2024-02-16 13:54:48 +00:00
editor.zoomToBounds(bounds, { targetZoom: 1 })
2023-04-25 11:01:25 +00:00
}
})
}
function coerceNumber(n: unknown): number {
if (typeof n !== 'number') return 0
if (Number.isNaN(n)) return 0
if (!Number.isFinite(n)) return 0
return n
}
function coerceDimension(d: unknown): number {
const n = coerceNumber(d)
if (n <= 0) return 1
return n
}
/**
* We want to move assets over to our new S3 bucket & extract any relevant metadata. That process is
* async though, where the rest of our migration is synchronous.
*
* We'll write placeholder assets to the app using the old asset URLs, then kick off a process async
* to try and download the real assets, extract the metadata, and upload them to our new bucket.
* It's not a big deal if this fails though.
*/
async function tryMigrateAsset(editor: Editor, placeholderAsset: TLAsset) {
2023-04-25 11:01:25 +00:00
try {
if (placeholderAsset.type === 'bookmark' || !placeholderAsset.props.src) return
const response = await fetch(placeholderAsset.props.src)
if (!response.ok) return
const file = new File([await response.blob()], placeholderAsset.props.name, {
type: response.headers.get('content-type') ?? placeholderAsset.props.mimeType ?? undefined,
})
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
const newAsset = await editor.getAssetForExternalContent({ type: 'file', file })
if (!newAsset) throw new Error('Could not get asset for external content')
2023-04-25 11:01:25 +00:00
if (newAsset.type === 'bookmark') return
editor.updateAssets([
2023-04-25 11:01:25 +00:00
{
id: placeholderAsset.id,
type: placeholderAsset.type,
props: {
...newAsset.props,
name: placeholderAsset.props.name,
},
},
])
} catch (err) {
// not a big deal, we'll just keep the placeholder asset
}
}
function migrate(document: LegacyTldrawDocument, newVersion: number): LegacyTldrawDocument {
const { version = 0 } = document
if (!document.assets) {
document.assets = {}
}
// Remove unused assets when loading a document
const assetIdsInUse = new Set<string>()
Object.values(document.pages).forEach((page) =>
Object.values(page.shapes).forEach((shape) => {
const { parentId, children, assetId } = shape
if (assetId) {
assetIdsInUse.add(assetId)
}
// Fix missing parent bug
if (parentId !== page.id && !page.shapes[parentId]) {
console.warn('Encountered a shape with a missing parent!')
shape.parentId = page.id
}
if (shape.type === TDShapeType.Group && children) {
children.forEach((childId) => {
if (!page.shapes[childId]) {
console.warn('Encountered a parent with a missing child!', shape.id, childId)
children?.splice(children.indexOf(childId), 1)
}
})
// TODO: Remove the shape if it has no children
}
})
)
Object.keys(document.assets).forEach((assetId) => {
if (!assetIdsInUse.has(assetId)) {
delete document.assets[assetId]
}
})
if (version !== newVersion) {
if (version < 14) {
Object.values(document.pages).forEach((page) => {
Object.values(page.shapes)
.filter((shape) => shape.type === TDShapeType.Text)
.forEach((shape) => {
if ((shape as TextShape).style.font === undefined) {
;(shape as TextShape).style.font === FontStyle.Script
}
})
})
}
// Lowercase styles, move binding meta to binding
if (version <= 13) {
Object.values(document.pages).forEach((page) => {
Object.values(page.bindings).forEach((binding) => {
Object.assign(binding, (binding as any).meta)
})
Object.values(page.shapes).forEach((shape) => {
Object.entries(shape.style).forEach(([id, style]) => {
if (typeof style === 'string') {
// @ts-ignore
shape.style[id] = style.toLowerCase()
}
})
if (shape.type === TDShapeType.Arrow) {
if (shape.decorations) {
Object.entries(shape.decorations).forEach(([id, decoration]) => {
if ((decoration as unknown) === 'Arrow') {
shape.decorations = {
...shape.decorations,
[id]: Decoration.Arrow,
}
}
})
}
}
})
})
}
// Add document name and file system handle
if (version <= 13.1 && document.name == null) {
document.name = 'New Document'
}
if (version < 15 && document.assets == null) {
document.assets = {}
}
Object.values(document.pages).forEach((page) => {
Object.values(page.shapes).forEach((shape) => {
if (version < 15.2) {
if (
(shape.type === TDShapeType.Image || shape.type === TDShapeType.Video) &&
shape.style.isFilled == null
) {
shape.style.isFilled = true
}
}
if (version < 15.3) {
if (
shape.type === TDShapeType.Rectangle ||
shape.type === TDShapeType.Triangle ||
shape.type === TDShapeType.Ellipse ||
shape.type === TDShapeType.Arrow
) {
if ('text' in shape && typeof shape.text === 'string') {
shape.label = shape.text
}
if (!shape.label) {
shape.label = ''
}
if (!shape.labelPoint) {
shape.labelPoint = [0.5, 0.5]
}
}
}
})
})
}
// Cleanup
Object.values(document.pageStates).forEach((pageState) => {
pageState.selectedIds = pageState.selectedIds.filter((id) => {
2023-04-25 11:01:25 +00:00
return document.pages[pageState.id].shapes[id] !== undefined
})
pageState.bindingId = undefined
pageState.editingId = undefined
pageState.hoveredId = undefined
2023-04-25 11:01:25 +00:00
pageState.pointedId = undefined
})
document.version = newVersion
return document
}
/* -------------------- TLV1 Types -------------------- */
interface TLV1Handle {
id: string
index: number
point: number[]
}
interface TLV1Binding {
id: string
toId: string
fromId: string
}
interface TLV1Shape {
id: string
type: string
parentId: string
childIndex: number
name: string
point: number[]
assetId?: string
rotation?: number
children?: string[]
handles?: Record<string, TLV1Handle>
isGhost?: boolean
isHidden?: boolean
isLocked?: boolean
isGenerated?: boolean
isAspectRatioLocked?: boolean
}
enum TDShapeType {
Sticky = 'sticky',
Ellipse = 'ellipse',
Rectangle = 'rectangle',
Triangle = 'triangle',
Draw = 'draw',
Arrow = 'arrow',
Text = 'text',
Group = 'group',
Image = 'image',
Video = 'video',
}
enum ColorStyle {
White = 'white',
LightGray = 'lightGray',
Gray = 'gray',
Black = 'black',
Green = 'green',
Cyan = 'cyan',
Blue = 'blue',
Indigo = 'indigo',
Violet = 'violet',
Red = 'red',
Orange = 'orange',
Yellow = 'yellow',
}
enum SizeStyle {
Small = 'small',
Medium = 'medium',
Large = 'large',
}
enum DashStyle {
Draw = 'draw',
Solid = 'solid',
Dashed = 'dashed',
Dotted = 'dotted',
}
enum AlignStyle {
Start = 'start',
Middle = 'middle',
End = 'end',
Justify = 'justify',
}
enum FontStyle {
Script = 'script',
Sans = 'sans',
Serif = 'serif',
Mono = 'mono',
}
type ShapeStyles = {
color: ColorStyle
size: SizeStyle
dash: DashStyle
font?: FontStyle
textAlign?: AlignStyle
isFilled?: boolean
scale?: number
}
interface TDBaseShape extends TLV1Shape {
style: ShapeStyles
type: TDShapeType
label?: string
handles?: Record<string, TDHandle>
}
interface DrawShape extends TDBaseShape {
type: TDShapeType.Draw
points: number[][]
isComplete: boolean
}
// The extended handle (used for arrows)
interface TDHandle extends TLV1Handle {
canBind?: boolean
bindingId?: string
}
interface RectangleShape extends TDBaseShape {
type: TDShapeType.Rectangle
size: number[]
label?: string
labelPoint?: number[]
}
interface EllipseShape extends TDBaseShape {
type: TDShapeType.Ellipse
radius: number[]
label?: string
labelPoint?: number[]
}
interface TriangleShape extends TDBaseShape {
type: TDShapeType.Triangle
size: number[]
label?: string
labelPoint?: number[]
}
enum Decoration {
Arrow = 'arrow',
}
// The shape created with the arrow tool
interface ArrowShape extends TDBaseShape {
type: TDShapeType.Arrow
bend: number
handles: {
start: TDHandle
bend: TDHandle
end: TDHandle
}
decorations?: {
start?: Decoration
end?: Decoration
middle?: Decoration
}
label?: string
labelPoint?: number[]
}
interface ArrowBinding extends TLV1Binding {
handleId: keyof ArrowShape['handles']
distance: number
point: number[]
}
type TDBinding = ArrowBinding
interface ImageShape extends TDBaseShape {
type: TDShapeType.Image
size: number[]
assetId: string
}
interface VideoShape extends TDBaseShape {
type: TDShapeType.Video
size: number[]
assetId: string
isPlaying: boolean
currentTime: number
}
// The shape created by the text tool
interface TextShape extends TDBaseShape {
type: TDShapeType.Text
text: string
}
// The shape created by the sticky tool
interface StickyShape extends TDBaseShape {
type: TDShapeType.Sticky
size: number[]
text: string
}
// The shape created when multiple shapes are grouped
interface GroupShape extends TDBaseShape {
type: TDShapeType.Group
size: number[]
children: string[]
}
type TDShape =
| RectangleShape
| EllipseShape
| TriangleShape
| DrawShape
| ArrowShape
| TextShape
| GroupShape
| StickyShape
| ImageShape
| VideoShape
type TDPage = {
id: string
name?: string
childIndex?: number
shapes: Record<string, TDShape>
bindings: Record<string, TDBinding>
}
interface TLV1Bounds {
minX: number
minY: number
maxX: number
maxY: number
width: number
height: number
rotation?: number
}
interface TLV1PageState {
id: string
selectedIds: string[]
2023-04-25 11:01:25 +00:00
camera: {
point: number[]
zoom: number
}
brush?: TLV1Bounds | null
pointedId?: string | null
hoveredId?: string | null
editingId?: string | null
2023-04-25 11:01:25 +00:00
bindingId?: string | null
}
enum TDAssetType {
Image = 'image',
Video = 'video',
}
interface TDImageAsset extends TLV1Asset {
type: TDAssetType.Image
fileName: string
src: string
size: number[]
}
interface TDVideoAsset extends TLV1Asset {
type: TDAssetType.Video
fileName: string
src: string
size: number[]
}
interface TLV1Asset {
id: string
type: string
}
type TDAsset = TDImageAsset | TDVideoAsset
type TDAssets = Record<string, TDAsset>
/** @internal */
export interface LegacyTldrawDocument {
id: string
name: string
version: number
pages: Record<string, TDPage>
pageStates: Record<string, TLV1PageState>
assets: TDAssets
}
/* ------------------ Translations ------------------ */
const v1ColorsToV2Colors: Record<ColorStyle, TLDefaultColorStyle> = {
2023-04-25 11:01:25 +00:00
[ColorStyle.White]: 'black',
[ColorStyle.Black]: 'black',
[ColorStyle.LightGray]: 'grey',
[ColorStyle.Gray]: 'grey',
[ColorStyle.Green]: 'light-green',
[ColorStyle.Cyan]: 'green',
[ColorStyle.Blue]: 'light-blue',
[ColorStyle.Indigo]: 'blue',
[ColorStyle.Orange]: 'orange',
[ColorStyle.Yellow]: 'yellow',
[ColorStyle.Red]: 'red',
[ColorStyle.Violet]: 'light-violet',
}
const v1FontsToV2Fonts: Record<FontStyle, TLDefaultFontStyle> = {
2023-04-25 11:01:25 +00:00
[FontStyle.Mono]: 'mono',
[FontStyle.Sans]: 'sans',
[FontStyle.Script]: 'draw',
[FontStyle.Serif]: 'serif',
}
const v1AlignsToV2Aligns: Record<AlignStyle, TLDefaultHorizontalAlignStyle> = {
2023-04-25 11:01:25 +00:00
[AlignStyle.Start]: 'start',
[AlignStyle.Middle]: 'middle',
[AlignStyle.End]: 'end',
[AlignStyle.Justify]: 'start',
}
const v1TextSizesToV2TextSizes: Record<SizeStyle, TLDefaultSizeStyle> = {
2023-04-25 11:01:25 +00:00
[SizeStyle.Small]: 's',
[SizeStyle.Medium]: 'l',
[SizeStyle.Large]: 'xl',
}
const v1SizesToV2Sizes: Record<SizeStyle, TLDefaultSizeStyle> = {
2023-04-25 11:01:25 +00:00
[SizeStyle.Small]: 'm',
[SizeStyle.Medium]: 'l',
[SizeStyle.Large]: 'xl',
}
const v1DashesToV2Dashes: Record<DashStyle, TLDefaultDashStyle> = {
2023-04-25 11:01:25 +00:00
[DashStyle.Solid]: 'solid',
[DashStyle.Dashed]: 'dashed',
[DashStyle.Dotted]: 'dotted',
[DashStyle.Draw]: 'draw',
}
function getV2Color(color: ColorStyle | undefined): TLDefaultColorStyle {
2023-04-25 11:01:25 +00:00
return color ? v1ColorsToV2Colors[color] ?? 'black' : 'black'
}
function getV2Font(font: FontStyle | undefined): TLDefaultFontStyle {
2023-04-25 11:01:25 +00:00
return font ? v1FontsToV2Fonts[font] ?? 'draw' : 'draw'
}
function getV2Align(align: AlignStyle | undefined): TLDefaultHorizontalAlignStyle {
2023-04-25 11:01:25 +00:00
return align ? v1AlignsToV2Aligns[align] ?? 'middle' : 'middle'
}
function getV2TextSize(size: SizeStyle | undefined): TLDefaultSizeStyle {
2023-04-25 11:01:25 +00:00
return size ? v1TextSizesToV2TextSizes[size] ?? 'm' : 'm'
}
function getV2Size(size: SizeStyle | undefined): TLDefaultSizeStyle {
2023-04-25 11:01:25 +00:00
return size ? v1SizesToV2Sizes[size] ?? 'l' : 'l'
}
function getV2Dash(dash: DashStyle | undefined): TLDefaultDashStyle {
2023-04-25 11:01:25 +00:00
return dash ? v1DashesToV2Dashes[dash] ?? 'draw' : 'draw'
}
function getV2Point(point: number[]): VecModel {
2023-04-25 11:01:25 +00:00
return {
x: coerceNumber(point[0]),
y: coerceNumber(point[1]),
z: point[2] == null ? 0.5 : coerceNumber(point[2]),
}
}
function getV2Arrowhead(decoration: Decoration | undefined): TLArrowShapeArrowheadStyle {
2023-04-25 11:01:25 +00:00
return decoration === Decoration.Arrow ? 'arrow' : 'none'
}
function getV2Fill(isFilled: boolean | undefined, color: ColorStyle) {
return isFilled
? color === ColorStyle.Black || color === ColorStyle.White
? 'semi'
: 'solid'
: 'none'
}