snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
import {
|
||||
Box,
|
||||
createShapeId,
|
||||
Editor,
|
||||
isPageId,
|
||||
reverseRecordsDiff,
|
||||
TLArrowBinding,
|
||||
TLArrowShape,
|
||||
TLDrawShape,
|
||||
TLGeoShape,
|
||||
TLGeoShapeGeoStyle,
|
||||
TLLineShape,
|
||||
TLNoteShape,
|
||||
TLShape,
|
||||
TLShapeId,
|
||||
TLTextShape,
|
||||
Vec,
|
||||
} from 'tldraw'
|
||||
import { SimpleShapeId } from '../types/ids-schema'
|
||||
import { convertTldrawFillToFocusedFill } from './FocusedFill'
|
||||
import { convertTldrawFontSizeAndScaleToFocusedFontSize } from './FocusedFontSize'
|
||||
import { FocusedGeoShapeType } from './FocusedGeoShapeType'
|
||||
import {
|
||||
FocusedArrowShape,
|
||||
FocusedDrawShape,
|
||||
FocusedGeoShape,
|
||||
FocusedLineShape,
|
||||
FocusedNoteShape,
|
||||
FocusedShape,
|
||||
FocusedTextAnchor,
|
||||
FocusedTextShape,
|
||||
FocusedUnknownShape,
|
||||
} from './FocusedShape'
|
||||
|
||||
/**
|
||||
* Convert a tldraw shape to a focused shape
|
||||
*/
|
||||
export function convertTldrawShapeToFocusedShape(editor: Editor, shape: TLShape): FocusedShape {
|
||||
switch (shape.type) {
|
||||
case 'text':
|
||||
return convertTextShapeToFocused(editor, shape as TLTextShape)
|
||||
case 'geo':
|
||||
return convertGeoShapeToFocused(editor, shape as TLGeoShape)
|
||||
case 'line':
|
||||
return convertLineShapeToFocused(editor, shape as TLLineShape)
|
||||
case 'arrow':
|
||||
return convertArrowShapeToFocused(editor, shape as TLArrowShape)
|
||||
case 'note':
|
||||
return convertNoteShapeToFocused(editor, shape as TLNoteShape)
|
||||
case 'draw':
|
||||
return convertDrawShapeToFocused(editor, shape as TLDrawShape)
|
||||
default:
|
||||
return convertUnknownShapeToFocused(editor, shape)
|
||||
}
|
||||
}
|
||||
|
||||
export function convertTldrawShapeToFocusedType(shape: TLShape): FocusedShape['_type'] {
|
||||
switch (shape.type) {
|
||||
case 'geo': {
|
||||
const geoShape = shape as TLGeoShape
|
||||
return GEO_TO_FOCUSED_TYPES[geoShape.props.geo]
|
||||
}
|
||||
case 'text':
|
||||
case 'line':
|
||||
case 'arrow':
|
||||
case 'note':
|
||||
case 'draw':
|
||||
return shape.type
|
||||
default:
|
||||
return 'unknown'
|
||||
}
|
||||
}
|
||||
|
||||
const GEO_TO_FOCUSED_TYPES: Record<TLGeoShapeGeoStyle, FocusedGeoShapeType> = {
|
||||
rectangle: 'rectangle',
|
||||
ellipse: 'ellipse',
|
||||
triangle: 'triangle',
|
||||
diamond: 'diamond',
|
||||
hexagon: 'hexagon',
|
||||
oval: 'pill',
|
||||
cloud: 'cloud',
|
||||
'x-box': 'x-box',
|
||||
'check-box': 'check-box',
|
||||
heart: 'heart',
|
||||
pentagon: 'pentagon',
|
||||
octagon: 'octagon',
|
||||
star: 'star',
|
||||
rhombus: 'parallelogram-right',
|
||||
'rhombus-2': 'parallelogram-left',
|
||||
trapezoid: 'trapezoid',
|
||||
'arrow-right': 'fat-arrow-right',
|
||||
'arrow-left': 'fat-arrow-left',
|
||||
'arrow-up': 'fat-arrow-up',
|
||||
'arrow-down': 'fat-arrow-down',
|
||||
} as const
|
||||
|
||||
export function convertTldrawIdToSimpleId(id: TLShapeId): SimpleShapeId {
|
||||
return id.slice(6) as SimpleShapeId
|
||||
}
|
||||
|
||||
function convertDrawShapeToFocused(editor: Editor, shape: TLDrawShape): FocusedDrawShape {
|
||||
return {
|
||||
_type: 'draw',
|
||||
color: shape.props.color,
|
||||
fill: convertTldrawFillToFocusedFill(shape.props.fill),
|
||||
note: (shape.meta.note as string) ?? '',
|
||||
shapeId: convertTldrawIdToSimpleId(shape.id),
|
||||
}
|
||||
}
|
||||
|
||||
function convertTextShapeToFocused(editor: Editor, shape: TLTextShape): FocusedTextShape {
|
||||
const util = editor.getShapeUtil(shape)
|
||||
const text = util.getText(shape) ?? ''
|
||||
const bounds = getSimpleBounds(editor, shape)
|
||||
|
||||
const position = new Vec()
|
||||
let anchor: FocusedTextAnchor = 'top-left'
|
||||
switch (shape.props.textAlign) {
|
||||
case 'middle': {
|
||||
anchor = 'top-center'
|
||||
position.x = bounds.center.x
|
||||
position.y = bounds.top
|
||||
break
|
||||
}
|
||||
case 'end': {
|
||||
anchor = 'top-right'
|
||||
position.x = bounds.right
|
||||
position.y = bounds.top
|
||||
break
|
||||
}
|
||||
case 'start': {
|
||||
anchor = 'top-left'
|
||||
position.x = bounds.left
|
||||
position.y = bounds.top
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
_type: 'text',
|
||||
anchor,
|
||||
color: shape.props.color,
|
||||
fontSize: convertTldrawFontSizeAndScaleToFocusedFontSize(editor, shape),
|
||||
maxWidth: shape.props.autoSize ? null : shape.props.w,
|
||||
note: (shape.meta.note as string) ?? '',
|
||||
shapeId: convertTldrawIdToSimpleId(shape.id),
|
||||
text: text,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
}
|
||||
}
|
||||
|
||||
function convertGeoShapeToFocused(editor: Editor, shape: TLGeoShape): FocusedGeoShape {
|
||||
const util = editor.getShapeUtil(shape)
|
||||
const text = util.getText(shape)
|
||||
const bounds = getSimpleBounds(editor, shape)
|
||||
const shapeTextAlign = shape.props.align
|
||||
|
||||
let newTextAlign: FocusedGeoShape['textAlign']
|
||||
switch (shapeTextAlign) {
|
||||
case 'start-legacy':
|
||||
newTextAlign = 'start'
|
||||
break
|
||||
case 'middle-legacy':
|
||||
newTextAlign = 'middle'
|
||||
break
|
||||
case 'end-legacy':
|
||||
newTextAlign = 'end'
|
||||
break
|
||||
default:
|
||||
newTextAlign = shapeTextAlign
|
||||
break
|
||||
}
|
||||
|
||||
return {
|
||||
_type: GEO_TO_FOCUSED_TYPES[shape.props.geo],
|
||||
color: shape.props.color,
|
||||
fill: convertTldrawFillToFocusedFill(shape.props.fill),
|
||||
h: shape.props.h,
|
||||
note: (shape.meta.note as string) ?? '',
|
||||
shapeId: convertTldrawIdToSimpleId(shape.id),
|
||||
text: text ?? '',
|
||||
textAlign: newTextAlign,
|
||||
w: shape.props.w,
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
}
|
||||
}
|
||||
|
||||
function convertLineShapeToFocused(editor: Editor, shape: TLLineShape): FocusedLineShape {
|
||||
const bounds = getSimpleBounds(editor, shape)
|
||||
const points = Object.values(shape.props.points).sort((a, b) => a.index.localeCompare(b.index))
|
||||
return {
|
||||
_type: 'line',
|
||||
color: shape.props.color,
|
||||
note: (shape.meta.note as string) ?? '',
|
||||
shapeId: convertTldrawIdToSimpleId(shape.id),
|
||||
x1: points[0].x + bounds.x,
|
||||
x2: points[1].x + bounds.x,
|
||||
y1: points[0].y + bounds.y,
|
||||
y2: points[1].y + bounds.y,
|
||||
}
|
||||
}
|
||||
|
||||
function convertArrowShapeToFocused(editor: Editor, shape: TLArrowShape): FocusedArrowShape {
|
||||
const bounds = getSimpleBounds(editor, shape)
|
||||
const bindings = editor.store.query.records('binding').get()
|
||||
const arrowBindings = bindings.filter(
|
||||
(b) => b.type === 'arrow' && b.fromId === shape.id
|
||||
) as TLArrowBinding[]
|
||||
const startBinding = arrowBindings.find((b) => b.props.terminal === 'start')
|
||||
const endBinding = arrowBindings.find((b) => b.props.terminal === 'end')
|
||||
|
||||
return {
|
||||
_type: 'arrow',
|
||||
bend: shape.props.bend * -1,
|
||||
color: shape.props.color,
|
||||
fromId: startBinding ? convertTldrawIdToSimpleId(startBinding.toId) : null,
|
||||
note: (shape.meta.note as string) ?? '',
|
||||
shapeId: convertTldrawIdToSimpleId(shape.id),
|
||||
text: (shape.meta.text as string) ?? '',
|
||||
toId: endBinding ? convertTldrawIdToSimpleId(endBinding.toId) : null,
|
||||
x1: shape.props.start.x + bounds.x,
|
||||
x2: shape.props.end.x + bounds.x,
|
||||
y1: shape.props.start.y + bounds.y,
|
||||
y2: shape.props.end.y + bounds.y,
|
||||
}
|
||||
}
|
||||
|
||||
function convertNoteShapeToFocused(editor: Editor, shape: TLNoteShape): FocusedNoteShape {
|
||||
const util = editor.getShapeUtil(shape)
|
||||
const text = util.getText(shape)
|
||||
const bounds = getSimpleBounds(editor, shape)
|
||||
return {
|
||||
_type: 'note',
|
||||
color: shape.props.color,
|
||||
note: (shape.meta.note as string) ?? '',
|
||||
shapeId: convertTldrawIdToSimpleId(shape.id),
|
||||
text: text ?? '',
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
}
|
||||
}
|
||||
|
||||
function convertUnknownShapeToFocused(editor: Editor, shape: TLShape): FocusedUnknownShape {
|
||||
const bounds = getSimpleBounds(editor, shape)
|
||||
return {
|
||||
_type: 'unknown',
|
||||
note: (shape.meta.note as string) ?? '',
|
||||
shapeId: convertTldrawIdToSimpleId(shape.id),
|
||||
subType: shape.type,
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
}
|
||||
}
|
||||
|
||||
function getSimpleBounds(editor: Editor, shape: TLShape): Box {
|
||||
// Compute page position from the shape record's own x/y, not the editor's cached bounds.
|
||||
// This is critical for diffing historical shape records where the editor's
|
||||
// current state differs from the shape record being converted.
|
||||
const pagePoint = getShapePagePoint(editor, shape)
|
||||
|
||||
// Try to get dimensions from shape props first
|
||||
const props = shape.props as { w?: number; h?: number }
|
||||
if (props.w !== undefined && props.h !== undefined) {
|
||||
return new Box(pagePoint.x, pagePoint.y, props.w, props.h)
|
||||
}
|
||||
|
||||
// Fall back to editor bounds for dimensions only (position comes from shape record)
|
||||
const bounds = editor.getShapePageBounds(shape)
|
||||
if (bounds) {
|
||||
return new Box(pagePoint.x, pagePoint.y, bounds.w, bounds.h)
|
||||
}
|
||||
|
||||
// Create a mock shape and get the bounds, then reverse the creation of the mock shape
|
||||
let mockBounds: Box | undefined
|
||||
const diff = editor.store.extractingChanges(() => {
|
||||
editor.run(
|
||||
() => {
|
||||
const mockId = createShapeId()
|
||||
editor.createShape({
|
||||
...shape,
|
||||
id: mockId,
|
||||
})
|
||||
mockBounds = editor.getShapePageBounds(mockId)
|
||||
},
|
||||
{ ignoreShapeLock: false, history: 'ignore' }
|
||||
)
|
||||
})
|
||||
const reverseDiff = reverseRecordsDiff(diff)
|
||||
editor.store.applyDiff(reverseDiff)
|
||||
|
||||
if (!mockBounds) {
|
||||
throw new Error('Failed to get bounds for shape')
|
||||
}
|
||||
return new Box(pagePoint.x, pagePoint.y, mockBounds.w, mockBounds.h)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the page-space position of a shape from its record's x/y values.
|
||||
* For shapes at the root level, this is just shape.x/y.
|
||||
* For shapes inside frames/groups, we transform through the parent's page transform.
|
||||
*/
|
||||
function getShapePagePoint(editor: Editor, shape: TLShape): Vec {
|
||||
// If the shape is at the root level (parent is a page), use x/y directly
|
||||
if (isPageId(shape.parentId)) {
|
||||
return new Vec(shape.x, shape.y)
|
||||
}
|
||||
|
||||
// For shapes inside parents, get the parent's page transform and apply it
|
||||
// Note: We use the editor's current parent transform, which is correct as long
|
||||
// as the parent itself hasn't moved in the same diff (uncommon case)
|
||||
const parentTransform = editor.getShapePageTransform(shape.parentId)
|
||||
return parentTransform.applyToPoint(new Vec(shape.x, shape.y))
|
||||
}
|
||||
Reference in New Issue
Block a user