Layout constraints example (#3996)
Adds an example of implementing layout contraints using bindings. ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [ ] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [x] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [ ] `bugfix` — Bug fix - [x] `feature` — New feature - [ ] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know ### Test Plan 1. Add a step-by-step description of how to test your PR here. 2. - [ ] Unit Tests - [ ] End to end tests ### Release Notes - Adds an example of how to use bindings to create layout constraints --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
parent
8250112061
commit
978de17a12
3 changed files with 595 additions and 0 deletions
373
apps/examples/src/examples/layout-bindings/LayoutExample.tsx
Normal file
373
apps/examples/src/examples/layout-bindings/LayoutExample.tsx
Normal file
|
@ -0,0 +1,373 @@
|
||||||
|
import {
|
||||||
|
BindingOnChangeOptions,
|
||||||
|
BindingOnCreateOptions,
|
||||||
|
BindingOnDeleteOptions,
|
||||||
|
BindingOnShapeChangeOptions,
|
||||||
|
BindingUtil,
|
||||||
|
HTMLContainer,
|
||||||
|
IndexKey,
|
||||||
|
RecordProps,
|
||||||
|
Rectangle2d,
|
||||||
|
ShapeUtil,
|
||||||
|
T,
|
||||||
|
TLBaseBinding,
|
||||||
|
TLBaseShape,
|
||||||
|
TLOnTranslateHandler,
|
||||||
|
Tldraw,
|
||||||
|
Vec,
|
||||||
|
clamp,
|
||||||
|
createBindingId,
|
||||||
|
getIndexBetween,
|
||||||
|
} from 'tldraw'
|
||||||
|
import snapShot from './snapshot.json'
|
||||||
|
|
||||||
|
// The container shapes that can contain element shapes
|
||||||
|
|
||||||
|
const CONTAINER_PADDING = 24
|
||||||
|
|
||||||
|
type ContainerShape = TLBaseShape<'element', { height: number; width: number }>
|
||||||
|
|
||||||
|
class ContainerShapeUtil extends ShapeUtil<ContainerShape> {
|
||||||
|
static override type = 'container' as const
|
||||||
|
static override props: RecordProps<ContainerShape> = { height: T.number, width: T.number }
|
||||||
|
|
||||||
|
override getDefaultProps() {
|
||||||
|
return {
|
||||||
|
width: 100 + CONTAINER_PADDING * 2,
|
||||||
|
height: 100 + CONTAINER_PADDING * 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override canBind({
|
||||||
|
fromShapeType,
|
||||||
|
toShapeType,
|
||||||
|
bindingType,
|
||||||
|
}: {
|
||||||
|
fromShapeType: string
|
||||||
|
toShapeType: string
|
||||||
|
bindingType: string
|
||||||
|
}) {
|
||||||
|
return fromShapeType === 'container' && toShapeType === 'element' && bindingType === 'layout'
|
||||||
|
}
|
||||||
|
override canEdit = () => false
|
||||||
|
override canResize = () => false
|
||||||
|
override hideRotateHandle = () => true
|
||||||
|
override isAspectRatioLocked = () => true
|
||||||
|
|
||||||
|
override getGeometry(shape: ContainerShape) {
|
||||||
|
return new Rectangle2d({
|
||||||
|
width: shape.props.width,
|
||||||
|
height: shape.props.height,
|
||||||
|
isFilled: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override component(shape: ContainerShape) {
|
||||||
|
return (
|
||||||
|
<HTMLContainer
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#efefef',
|
||||||
|
width: shape.props.width,
|
||||||
|
height: shape.props.height,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override indicator(shape: ContainerShape) {
|
||||||
|
return <rect width={shape.props.width} height={shape.props.height} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The element shapes that can be placed inside the container shapes
|
||||||
|
|
||||||
|
type ElementShape = TLBaseShape<'element', { color: string }>
|
||||||
|
|
||||||
|
class ElementShapeUtil extends ShapeUtil<ElementShape> {
|
||||||
|
static override type = 'element' as const
|
||||||
|
static override props: RecordProps<ElementShape> = {
|
||||||
|
color: T.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
override getDefaultProps() {
|
||||||
|
return {
|
||||||
|
color: '#AEC6CF',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override canBind({
|
||||||
|
fromShapeType,
|
||||||
|
toShapeType,
|
||||||
|
bindingType,
|
||||||
|
}: {
|
||||||
|
fromShapeType: string
|
||||||
|
toShapeType: string
|
||||||
|
bindingType: string
|
||||||
|
}) {
|
||||||
|
return fromShapeType === 'container' && toShapeType === 'element' && bindingType === 'layout'
|
||||||
|
}
|
||||||
|
override canEdit = () => false
|
||||||
|
override canResize = () => false
|
||||||
|
override hideRotateHandle = () => true
|
||||||
|
override isAspectRatioLocked = () => true
|
||||||
|
|
||||||
|
override getGeometry() {
|
||||||
|
return new Rectangle2d({
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
isFilled: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override component(shape: ElementShape) {
|
||||||
|
return <HTMLContainer style={{ backgroundColor: shape.props.color }}></HTMLContainer>
|
||||||
|
}
|
||||||
|
|
||||||
|
override indicator() {
|
||||||
|
return <rect width={100} height={100} />
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTargetContainer = (shape: ElementShape, pageAnchor: Vec) => {
|
||||||
|
// Find the container shape that the element is being dropped on
|
||||||
|
return this.editor.getShapeAtPoint(pageAnchor, {
|
||||||
|
hitInside: true,
|
||||||
|
filter: (otherShape) =>
|
||||||
|
this.editor.canBindShapes({ fromShape: otherShape, toShape: shape, binding: 'layout' }),
|
||||||
|
}) as ContainerShape | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBindingIndexForPosition = (
|
||||||
|
shape: ElementShape,
|
||||||
|
container: ContainerShape,
|
||||||
|
pageAnchor: Vec
|
||||||
|
) => {
|
||||||
|
// All the layout bindings from the container
|
||||||
|
const allBindings = this.editor
|
||||||
|
.getBindingsFromShape<LayoutBinding>(container, 'layout')
|
||||||
|
.sort((a, b) => (a.props.index > b.props.index ? 1 : -1))
|
||||||
|
|
||||||
|
// Those bindings that don't involve the element
|
||||||
|
const siblings = allBindings.filter((b) => b.toId !== shape.id)
|
||||||
|
|
||||||
|
// Get the relative x position of the element center in the container
|
||||||
|
// Where should the element be placed? min index at left, max index + 1
|
||||||
|
const order = clamp(
|
||||||
|
Math.round((pageAnchor.x - container.x - CONTAINER_PADDING) / (100 + CONTAINER_PADDING)),
|
||||||
|
0,
|
||||||
|
siblings.length + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get a fractional index between the two siblings
|
||||||
|
const belowSib = allBindings[order - 1]
|
||||||
|
const aboveSib = allBindings[order]
|
||||||
|
let index: IndexKey
|
||||||
|
|
||||||
|
if (belowSib?.toId === shape.id) {
|
||||||
|
index = belowSib.props.index
|
||||||
|
} else if (aboveSib?.toId === shape.id) {
|
||||||
|
index = aboveSib.props.index
|
||||||
|
} else {
|
||||||
|
index = getIndexBetween(belowSib?.props.index, aboveSib?.props.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
override onTranslateStart = (shape: ElementShape) => {
|
||||||
|
// Update all the layout bindings for this shape to be placeholders
|
||||||
|
this.editor.updateBindings(
|
||||||
|
this.editor.getBindingsToShape<LayoutBinding>(shape, 'layout').map((binding) => ({
|
||||||
|
...binding,
|
||||||
|
props: { ...binding.props, placeholder: true },
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override onTranslate: TLOnTranslateHandler<ElementShape> | undefined = (
|
||||||
|
_,
|
||||||
|
shape: ElementShape
|
||||||
|
) => {
|
||||||
|
// Find the center of the element shape
|
||||||
|
const pageAnchor = this.editor.getShapePageTransform(shape).applyToPoint({ x: 50, y: 50 })
|
||||||
|
|
||||||
|
// Find the container shape that the element is being dropped on
|
||||||
|
const targetContainer = this.getTargetContainer(shape, pageAnchor)
|
||||||
|
|
||||||
|
if (!targetContainer) {
|
||||||
|
// Delete all the bindings to the element
|
||||||
|
const bindings = this.editor.getBindingsToShape<LayoutBinding>(shape, 'layout')
|
||||||
|
this.editor.deleteBindings(bindings)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the index for the new binding
|
||||||
|
const index = this.getBindingIndexForPosition(shape, targetContainer, pageAnchor)
|
||||||
|
|
||||||
|
// Is there an existing binding already between the container and the shape?
|
||||||
|
const existingBinding = this.editor
|
||||||
|
.getBindingsFromShape<LayoutBinding>(targetContainer, 'layout')
|
||||||
|
.find((b) => b.toId === shape.id)
|
||||||
|
|
||||||
|
if (existingBinding) {
|
||||||
|
// If a binding already exists, update it
|
||||||
|
if (existingBinding.props.index === index) return
|
||||||
|
this.editor.updateBinding<LayoutBinding>({
|
||||||
|
...existingBinding,
|
||||||
|
props: {
|
||||||
|
...existingBinding.props,
|
||||||
|
placeholder: true,
|
||||||
|
index,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// ...otherwise, create a new one
|
||||||
|
this.editor.createBinding<LayoutBinding>({
|
||||||
|
id: createBindingId(),
|
||||||
|
type: 'layout',
|
||||||
|
fromId: targetContainer.id,
|
||||||
|
toId: shape.id,
|
||||||
|
props: {
|
||||||
|
index,
|
||||||
|
placeholder: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override onTranslateEnd = (_: ElementShape, shape: ElementShape) => {
|
||||||
|
// Find the center of the element shape
|
||||||
|
const pageAnchor = this.editor.getShapePageTransform(shape).applyToPoint({ x: 50, y: 50 })
|
||||||
|
|
||||||
|
// Find the container shape that the element is being dropped on
|
||||||
|
const targetContainer = this.getTargetContainer(shape, pageAnchor)
|
||||||
|
|
||||||
|
// No target container? no problem
|
||||||
|
if (!targetContainer) return
|
||||||
|
|
||||||
|
// get the index for the new binding
|
||||||
|
const index = this.getBindingIndexForPosition(shape, targetContainer, pageAnchor)
|
||||||
|
|
||||||
|
// delete all the previous bindings for this shape
|
||||||
|
this.editor.deleteBindings(this.editor.getBindingsToShape<LayoutBinding>(shape, 'layout'))
|
||||||
|
|
||||||
|
// ...and then create a new one
|
||||||
|
this.editor.createBinding<LayoutBinding>({
|
||||||
|
id: createBindingId(),
|
||||||
|
type: 'layout',
|
||||||
|
fromId: targetContainer.id,
|
||||||
|
toId: shape.id,
|
||||||
|
props: {
|
||||||
|
index,
|
||||||
|
placeholder: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The binding between the element shapes and the container shapes
|
||||||
|
|
||||||
|
type LayoutBinding = TLBaseBinding<
|
||||||
|
'layout',
|
||||||
|
{
|
||||||
|
index: IndexKey
|
||||||
|
placeholder: boolean
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
class LayoutBindingUtil extends BindingUtil<LayoutBinding> {
|
||||||
|
static override type = 'layout' as const
|
||||||
|
|
||||||
|
override getDefaultProps() {
|
||||||
|
return {
|
||||||
|
index: 'a1' as IndexKey,
|
||||||
|
placeholder: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override onAfterCreate({ binding }: BindingOnCreateOptions<LayoutBinding>): void {
|
||||||
|
this.updateElementsForContainer(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override onAfterChange({ bindingAfter }: BindingOnChangeOptions<LayoutBinding>): void {
|
||||||
|
this.updateElementsForContainer(bindingAfter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override onAfterChangeFromShape({ binding }: BindingOnShapeChangeOptions<LayoutBinding>): void {
|
||||||
|
this.updateElementsForContainer(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override onAfterDelete({ binding }: BindingOnDeleteOptions<LayoutBinding>): void {
|
||||||
|
this.updateElementsForContainer(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateElementsForContainer({
|
||||||
|
props: { placeholder },
|
||||||
|
fromId: containerId,
|
||||||
|
toId,
|
||||||
|
}: LayoutBinding) {
|
||||||
|
// Get all of the bindings from the layout container
|
||||||
|
const container = this.editor.getShape<ContainerShape>(containerId)
|
||||||
|
if (!container) return
|
||||||
|
|
||||||
|
const bindings = this.editor
|
||||||
|
.getBindingsFromShape<LayoutBinding>(container, 'layout')
|
||||||
|
.sort((a, b) => (a.props.index > b.props.index ? 1 : -1))
|
||||||
|
if (bindings.length === 0) return
|
||||||
|
|
||||||
|
for (let i = 0; i < bindings.length; i++) {
|
||||||
|
const binding = bindings[i]
|
||||||
|
|
||||||
|
if (toId === binding.toId && placeholder) continue
|
||||||
|
|
||||||
|
const offset = new Vec(CONTAINER_PADDING + i * (100 + CONTAINER_PADDING), CONTAINER_PADDING)
|
||||||
|
|
||||||
|
const shape = this.editor.getShape<ElementShape>(binding.toId)
|
||||||
|
if (!shape) continue
|
||||||
|
|
||||||
|
const point = this.editor.getPointInParentSpace(
|
||||||
|
shape,
|
||||||
|
this.editor.getShapePageTransform(container)!.applyToPoint(offset)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (shape.x !== point.x || shape.y !== point.y) {
|
||||||
|
this.editor.updateShape({
|
||||||
|
id: binding.toId,
|
||||||
|
type: 'element',
|
||||||
|
x: point.x,
|
||||||
|
y: point.y,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const width =
|
||||||
|
CONTAINER_PADDING +
|
||||||
|
(bindings.length * 100 + (bindings.length - 1) * CONTAINER_PADDING) +
|
||||||
|
CONTAINER_PADDING
|
||||||
|
|
||||||
|
const height = CONTAINER_PADDING + 100 + CONTAINER_PADDING
|
||||||
|
|
||||||
|
if (width !== container.props.width || height !== container.props.height) {
|
||||||
|
this.editor.updateShape({
|
||||||
|
id: container.id,
|
||||||
|
type: 'container',
|
||||||
|
props: { width, height },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LayoutExample() {
|
||||||
|
return (
|
||||||
|
<div className="tldraw__editor">
|
||||||
|
<Tldraw
|
||||||
|
// @ts-ignore
|
||||||
|
snapshot={snapShot}
|
||||||
|
onMount={(editor) => {
|
||||||
|
;(window as any).editor = editor
|
||||||
|
}}
|
||||||
|
shapeUtils={[ContainerShapeUtil, ElementShapeUtil]}
|
||||||
|
bindingUtils={[LayoutBindingUtil]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
12
apps/examples/src/examples/layout-bindings/README.md
Normal file
12
apps/examples/src/examples/layout-bindings/README.md
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
title: Layout constraints (bindings)
|
||||||
|
component: ./LayoutExample.tsx
|
||||||
|
category: shapes/tools
|
||||||
|
keywords: [constraints, group, shape, custom, bindings, drag, drop, position]
|
||||||
|
---
|
||||||
|
|
||||||
|
How to constrain shapes to a layout using bindings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
You can use bindings to make shapes respond to changes to other shapes. This is useful for enforcing layout constraints
|
210
apps/examples/src/examples/layout-bindings/snapshot.json
Normal file
210
apps/examples/src/examples/layout-bindings/snapshot.json
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
{
|
||||||
|
"store": {
|
||||||
|
"document:document": {
|
||||||
|
"gridSize": 10,
|
||||||
|
"name": "",
|
||||||
|
"meta": {},
|
||||||
|
"id": "document:document",
|
||||||
|
"typeName": "document"
|
||||||
|
},
|
||||||
|
"page:page": {
|
||||||
|
"meta": {},
|
||||||
|
"id": "page:page",
|
||||||
|
"name": "Page 1",
|
||||||
|
"index": "a1",
|
||||||
|
"typeName": "page"
|
||||||
|
},
|
||||||
|
"shape:f4LKGB_8M2qsyWGpHR5Dq": {
|
||||||
|
"x": 30.9375,
|
||||||
|
"y": 69.48828125,
|
||||||
|
"rotation": 0,
|
||||||
|
"isLocked": false,
|
||||||
|
"opacity": 1,
|
||||||
|
"meta": {},
|
||||||
|
"id": "shape:f4LKGB_8M2qsyWGpHR5Dq",
|
||||||
|
"type": "container",
|
||||||
|
"parentId": "page:page",
|
||||||
|
"index": "a1",
|
||||||
|
"props": {
|
||||||
|
"width": 644,
|
||||||
|
"height": 148
|
||||||
|
},
|
||||||
|
"typeName": "shape"
|
||||||
|
},
|
||||||
|
"shape:2oThF4kJ4v31xqKN5lvq2": {
|
||||||
|
"x": 550.9375,
|
||||||
|
"y": 93.48828125,
|
||||||
|
"rotation": 0,
|
||||||
|
"isLocked": false,
|
||||||
|
"opacity": 1,
|
||||||
|
"meta": {},
|
||||||
|
"id": "shape:2oThF4kJ4v31xqKN5lvq2",
|
||||||
|
"type": "element",
|
||||||
|
"props": {
|
||||||
|
"color": "#5BCEFA"
|
||||||
|
},
|
||||||
|
"parentId": "page:page",
|
||||||
|
"index": "a2",
|
||||||
|
"typeName": "shape"
|
||||||
|
},
|
||||||
|
"shape:K2vk_VTaNh-ANaRNOAvgY": {
|
||||||
|
"x": 426.9375,
|
||||||
|
"y": 93.48828125,
|
||||||
|
"rotation": 0,
|
||||||
|
"isLocked": false,
|
||||||
|
"opacity": 1,
|
||||||
|
"meta": {},
|
||||||
|
"id": "shape:K2vk_VTaNh-ANaRNOAvgY",
|
||||||
|
"type": "element",
|
||||||
|
"props": {
|
||||||
|
"color": "#F5A9B8"
|
||||||
|
},
|
||||||
|
"parentId": "page:page",
|
||||||
|
"index": "a3",
|
||||||
|
"typeName": "shape"
|
||||||
|
},
|
||||||
|
"shape:6uouhIK7PvyIRNQHACf-d": {
|
||||||
|
"x": 302.9375,
|
||||||
|
"y": 93.48828125,
|
||||||
|
"rotation": 0,
|
||||||
|
"isLocked": false,
|
||||||
|
"opacity": 1,
|
||||||
|
"meta": {},
|
||||||
|
"id": "shape:6uouhIK7PvyIRNQHACf-d",
|
||||||
|
"type": "element",
|
||||||
|
"props": {
|
||||||
|
"color": "#FFFFFF"
|
||||||
|
},
|
||||||
|
"parentId": "page:page",
|
||||||
|
"index": "a4",
|
||||||
|
"typeName": "shape"
|
||||||
|
},
|
||||||
|
"shape:GTQq2qxkWPHEK7KMIRtsh": {
|
||||||
|
"x": 54.9375,
|
||||||
|
"y": 93.48828125,
|
||||||
|
"rotation": 0,
|
||||||
|
"isLocked": false,
|
||||||
|
"opacity": 1,
|
||||||
|
"meta": {},
|
||||||
|
"id": "shape:GTQq2qxkWPHEK7KMIRtsh",
|
||||||
|
"type": "element",
|
||||||
|
"props": {
|
||||||
|
"color": "#5BCEFA"
|
||||||
|
},
|
||||||
|
"parentId": "page:page",
|
||||||
|
"index": "a5",
|
||||||
|
"typeName": "shape"
|
||||||
|
},
|
||||||
|
"shape:05jMujN6A0sIp6zzHMpbV": {
|
||||||
|
"x": 178.9375,
|
||||||
|
"y": 93.48828125,
|
||||||
|
"rotation": 0,
|
||||||
|
"isLocked": false,
|
||||||
|
"opacity": 1,
|
||||||
|
"meta": {},
|
||||||
|
"id": "shape:05jMujN6A0sIp6zzHMpbV",
|
||||||
|
"type": "element",
|
||||||
|
"props": {
|
||||||
|
"color": "#F5A9B8"
|
||||||
|
},
|
||||||
|
"parentId": "page:page",
|
||||||
|
"index": "a6",
|
||||||
|
"typeName": "shape"
|
||||||
|
},
|
||||||
|
"binding:iOBENBUHvzD8N7mBdIM5l": {
|
||||||
|
"meta": {},
|
||||||
|
"id": "binding:iOBENBUHvzD8N7mBdIM5l",
|
||||||
|
"type": "layout",
|
||||||
|
"fromId": "shape:f4LKGB_8M2qsyWGpHR5Dq",
|
||||||
|
"toId": "shape:05jMujN6A0sIp6zzHMpbV",
|
||||||
|
"props": {
|
||||||
|
"index": "a2",
|
||||||
|
"placeholder": false
|
||||||
|
},
|
||||||
|
"typeName": "binding"
|
||||||
|
},
|
||||||
|
"binding:YTIeOALEmHJk6dczRpQmE": {
|
||||||
|
"meta": {},
|
||||||
|
"id": "binding:YTIeOALEmHJk6dczRpQmE",
|
||||||
|
"type": "layout",
|
||||||
|
"fromId": "shape:f4LKGB_8M2qsyWGpHR5Dq",
|
||||||
|
"toId": "shape:GTQq2qxkWPHEK7KMIRtsh",
|
||||||
|
"props": {
|
||||||
|
"index": "a1",
|
||||||
|
"placeholder": false
|
||||||
|
},
|
||||||
|
"typeName": "binding"
|
||||||
|
},
|
||||||
|
"binding:n4LY_pVuLfjV1qpOTZX-U": {
|
||||||
|
"meta": {},
|
||||||
|
"id": "binding:n4LY_pVuLfjV1qpOTZX-U",
|
||||||
|
"type": "layout",
|
||||||
|
"fromId": "shape:f4LKGB_8M2qsyWGpHR5Dq",
|
||||||
|
"toId": "shape:6uouhIK7PvyIRNQHACf-d",
|
||||||
|
"props": {
|
||||||
|
"index": "a3",
|
||||||
|
"placeholder": false
|
||||||
|
},
|
||||||
|
"typeName": "binding"
|
||||||
|
},
|
||||||
|
"binding:8XayRsWB_nxAH2833SYg1": {
|
||||||
|
"meta": {},
|
||||||
|
"id": "binding:8XayRsWB_nxAH2833SYg1",
|
||||||
|
"type": "layout",
|
||||||
|
"fromId": "shape:f4LKGB_8M2qsyWGpHR5Dq",
|
||||||
|
"toId": "shape:2oThF4kJ4v31xqKN5lvq2",
|
||||||
|
"props": {
|
||||||
|
"index": "a5",
|
||||||
|
"placeholder": false
|
||||||
|
},
|
||||||
|
"typeName": "binding"
|
||||||
|
},
|
||||||
|
"binding:MTYuIRiEVTn2DyVChthry": {
|
||||||
|
"meta": {},
|
||||||
|
"id": "binding:MTYuIRiEVTn2DyVChthry",
|
||||||
|
"type": "layout",
|
||||||
|
"fromId": "shape:f4LKGB_8M2qsyWGpHR5Dq",
|
||||||
|
"toId": "shape:K2vk_VTaNh-ANaRNOAvgY",
|
||||||
|
"props": {
|
||||||
|
"index": "a4",
|
||||||
|
"placeholder": false
|
||||||
|
},
|
||||||
|
"typeName": "binding"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"sequences": {
|
||||||
|
"com.tldraw.store": 4,
|
||||||
|
"com.tldraw.asset": 1,
|
||||||
|
"com.tldraw.camera": 1,
|
||||||
|
"com.tldraw.document": 2,
|
||||||
|
"com.tldraw.instance": 25,
|
||||||
|
"com.tldraw.instance_page_state": 5,
|
||||||
|
"com.tldraw.page": 1,
|
||||||
|
"com.tldraw.instance_presence": 5,
|
||||||
|
"com.tldraw.pointer": 1,
|
||||||
|
"com.tldraw.shape": 4,
|
||||||
|
"com.tldraw.asset.bookmark": 2,
|
||||||
|
"com.tldraw.asset.image": 4,
|
||||||
|
"com.tldraw.asset.video": 4,
|
||||||
|
"com.tldraw.shape.group": 0,
|
||||||
|
"com.tldraw.shape.text": 2,
|
||||||
|
"com.tldraw.shape.bookmark": 2,
|
||||||
|
"com.tldraw.shape.draw": 2,
|
||||||
|
"com.tldraw.shape.geo": 9,
|
||||||
|
"com.tldraw.shape.note": 7,
|
||||||
|
"com.tldraw.shape.line": 5,
|
||||||
|
"com.tldraw.shape.frame": 0,
|
||||||
|
"com.tldraw.shape.arrow": 5,
|
||||||
|
"com.tldraw.shape.highlight": 1,
|
||||||
|
"com.tldraw.shape.embed": 4,
|
||||||
|
"com.tldraw.shape.image": 3,
|
||||||
|
"com.tldraw.shape.video": 2,
|
||||||
|
"com.tldraw.shape.container": 0,
|
||||||
|
"com.tldraw.shape.element": 0,
|
||||||
|
"com.tldraw.binding.arrow": 0,
|
||||||
|
"com.tldraw.binding.layout": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue