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

This commit is contained in:
upstream-mirror
2026-09-23 16:12:52 +05:00
commit a774ac673a
4541 changed files with 770499 additions and 0 deletions

View File

@@ -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))
}