@dxos/plugin-tldraw 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +105 -0
- package/PLUGIN.mdl +412 -0
- package/README.md +3 -0
- package/dist/assets/embed-icons/codepen.png +0 -0
- package/dist/assets/embed-icons/codesandbox.png +0 -0
- package/dist/assets/embed-icons/desmos.png +0 -0
- package/dist/assets/embed-icons/excalidraw.png +0 -0
- package/dist/assets/embed-icons/felt.png +0 -0
- package/dist/assets/embed-icons/figma.png +0 -0
- package/dist/assets/embed-icons/github_gist.png +0 -0
- package/dist/assets/embed-icons/google_calendar.png +0 -0
- package/dist/assets/embed-icons/google_maps.png +0 -0
- package/dist/assets/embed-icons/google_slides.png +0 -0
- package/dist/assets/embed-icons/observable.png +0 -0
- package/dist/assets/embed-icons/replit.png +0 -0
- package/dist/assets/embed-icons/scratch.png +0 -0
- package/dist/assets/embed-icons/spotify.png +0 -0
- package/dist/assets/embed-icons/tldraw.png +0 -0
- package/dist/assets/embed-icons/val_town.png +0 -0
- package/dist/assets/embed-icons/vimeo.png +0 -0
- package/dist/assets/embed-icons/youtube.png +0 -0
- package/dist/assets/fonts/IBMPlexMono-Medium.woff2 +0 -0
- package/dist/assets/fonts/IBMPlexSans-Medium.woff2 +0 -0
- package/dist/assets/fonts/IBMPlexSerif-Medium.woff2 +0 -0
- package/dist/assets/fonts/Montserrat-Regular.woff2 +0 -0
- package/dist/assets/fonts/Shantell_Sans-Tldrawish.woff2 +0 -0
- package/dist/assets/icons/icon/0_merged.svg +1 -0
- package/dist/assets/icons/icon/align-bottom.svg +1 -0
- package/dist/assets/icons/icon/align-center-horizontal.svg +1 -0
- package/dist/assets/icons/icon/align-center-vertical.svg +1 -0
- package/dist/assets/icons/icon/align-left.svg +1 -0
- package/dist/assets/icons/icon/align-right.svg +1 -0
- package/dist/assets/icons/icon/align-top.svg +1 -0
- package/dist/assets/icons/icon/arrow-left.svg +1 -0
- package/dist/assets/icons/icon/arrowhead-arrow.svg +1 -0
- package/dist/assets/icons/icon/arrowhead-bar.svg +1 -0
- package/dist/assets/icons/icon/arrowhead-diamond.svg +1 -0
- package/dist/assets/icons/icon/arrowhead-dot.svg +1 -0
- package/dist/assets/icons/icon/arrowhead-none.svg +1 -0
- package/dist/assets/icons/icon/arrowhead-square.svg +1 -0
- package/dist/assets/icons/icon/arrowhead-triangle-inverted.svg +1 -0
- package/dist/assets/icons/icon/arrowhead-triangle.svg +1 -0
- package/dist/assets/icons/icon/blob.svg +1 -0
- package/dist/assets/icons/icon/bring-forward.svg +1 -0
- package/dist/assets/icons/icon/bring-to-front.svg +1 -0
- package/dist/assets/icons/icon/broken.svg +1 -0
- package/dist/assets/icons/icon/chat-teardrop-text.svg +1 -0
- package/dist/assets/icons/icon/check-circle.svg +1 -0
- package/dist/assets/icons/icon/check.svg +1 -0
- package/dist/assets/icons/icon/chevron-down.svg +1 -0
- package/dist/assets/icons/icon/chevron-left.svg +1 -0
- package/dist/assets/icons/icon/chevron-right.svg +1 -0
- package/dist/assets/icons/icon/chevron-up.svg +1 -0
- package/dist/assets/icons/icon/chevrons-ne.svg +1 -0
- package/dist/assets/icons/icon/chevrons-sw.svg +1 -0
- package/dist/assets/icons/icon/clipboard-copied.svg +1 -0
- package/dist/assets/icons/icon/clipboard-copy.svg +1 -0
- package/dist/assets/icons/icon/color.svg +1 -0
- package/dist/assets/icons/icon/cross-2.svg +1 -0
- package/dist/assets/icons/icon/cross-circle.svg +1 -0
- package/dist/assets/icons/icon/dash-dashed.svg +1 -0
- package/dist/assets/icons/icon/dash-dotted.svg +1 -0
- package/dist/assets/icons/icon/dash-draw.svg +1 -0
- package/dist/assets/icons/icon/dash-solid.svg +1 -0
- package/dist/assets/icons/icon/disconnected.svg +1 -0
- package/dist/assets/icons/icon/discord.svg +1 -0
- package/dist/assets/icons/icon/distribute-horizontal.svg +1 -0
- package/dist/assets/icons/icon/distribute-vertical.svg +1 -0
- package/dist/assets/icons/icon/dot.svg +1 -0
- package/dist/assets/icons/icon/dots-horizontal.svg +1 -0
- package/dist/assets/icons/icon/dots-vertical.svg +1 -0
- package/dist/assets/icons/icon/drag-handle-dots.svg +1 -0
- package/dist/assets/icons/icon/duplicate.svg +1 -0
- package/dist/assets/icons/icon/edit.svg +1 -0
- package/dist/assets/icons/icon/external-link.svg +1 -0
- package/dist/assets/icons/icon/fill-fill.svg +1 -0
- package/dist/assets/icons/icon/fill-none.svg +1 -0
- package/dist/assets/icons/icon/fill-pattern.svg +1 -0
- package/dist/assets/icons/icon/fill-semi.svg +1 -0
- package/dist/assets/icons/icon/fill-solid.svg +1 -0
- package/dist/assets/icons/icon/follow.svg +1 -0
- package/dist/assets/icons/icon/following.svg +1 -0
- package/dist/assets/icons/icon/font-draw.svg +8 -0
- package/dist/assets/icons/icon/font-mono.svg +1 -0
- package/dist/assets/icons/icon/font-sans.svg +1 -0
- package/dist/assets/icons/icon/font-serif.svg +1 -0
- package/dist/assets/icons/icon/geo-arrow-down.svg +1 -0
- package/dist/assets/icons/icon/geo-arrow-left.svg +1 -0
- package/dist/assets/icons/icon/geo-arrow-right.svg +1 -0
- package/dist/assets/icons/icon/geo-arrow-up.svg +1 -0
- package/dist/assets/icons/icon/geo-check-box.svg +1 -0
- package/dist/assets/icons/icon/geo-cloud.svg +1 -0
- package/dist/assets/icons/icon/geo-diamond.svg +1 -0
- package/dist/assets/icons/icon/geo-ellipse.svg +1 -0
- package/dist/assets/icons/icon/geo-heart.svg +1 -0
- package/dist/assets/icons/icon/geo-hexagon.svg +1 -0
- package/dist/assets/icons/icon/geo-octagon.svg +1 -0
- package/dist/assets/icons/icon/geo-oval.svg +1 -0
- package/dist/assets/icons/icon/geo-pentagon.svg +1 -0
- package/dist/assets/icons/icon/geo-rectangle.svg +1 -0
- package/dist/assets/icons/icon/geo-rhombus-2.svg +1 -0
- package/dist/assets/icons/icon/geo-rhombus.svg +1 -0
- package/dist/assets/icons/icon/geo-star.svg +1 -0
- package/dist/assets/icons/icon/geo-trapezoid.svg +1 -0
- package/dist/assets/icons/icon/geo-triangle.svg +1 -0
- package/dist/assets/icons/icon/geo-x-box.svg +1 -0
- package/dist/assets/icons/icon/github.svg +1 -0
- package/dist/assets/icons/icon/group.svg +1 -0
- package/dist/assets/icons/icon/horizontal-align-end.svg +1 -0
- package/dist/assets/icons/icon/horizontal-align-middle.svg +1 -0
- package/dist/assets/icons/icon/horizontal-align-start.svg +1 -0
- package/dist/assets/icons/icon/icon-names.json +143 -0
- package/dist/assets/icons/icon/info-circle.svg +1 -0
- package/dist/assets/icons/icon/leading.svg +1 -0
- package/dist/assets/icons/icon/link.svg +1 -0
- package/dist/assets/icons/icon/lock.svg +1 -0
- package/dist/assets/icons/icon/menu.svg +1 -0
- package/dist/assets/icons/icon/minus.svg +1 -0
- package/dist/assets/icons/icon/mixed.svg +1 -0
- package/dist/assets/icons/icon/pack.svg +1 -0
- package/dist/assets/icons/icon/plus.svg +1 -0
- package/dist/assets/icons/icon/question-mark-circle.svg +1 -0
- package/dist/assets/icons/icon/question-mark.svg +1 -0
- package/dist/assets/icons/icon/redo.svg +1 -0
- package/dist/assets/icons/icon/reset-zoom.svg +1 -0
- package/dist/assets/icons/icon/rotate-ccw.svg +1 -0
- package/dist/assets/icons/icon/rotate-cw.svg +1 -0
- package/dist/assets/icons/icon/send-backward.svg +1 -0
- package/dist/assets/icons/icon/send-to-back.svg +1 -0
- package/dist/assets/icons/icon/share-1.svg +1 -0
- package/dist/assets/icons/icon/size-extra-large.svg +1 -0
- package/dist/assets/icons/icon/size-large.svg +1 -0
- package/dist/assets/icons/icon/size-medium.svg +1 -0
- package/dist/assets/icons/icon/size-small.svg +1 -0
- package/dist/assets/icons/icon/spline-cubic.svg +1 -0
- package/dist/assets/icons/icon/spline-line.svg +1 -0
- package/dist/assets/icons/icon/stack-horizontal.svg +1 -0
- package/dist/assets/icons/icon/stack-vertical.svg +1 -0
- package/dist/assets/icons/icon/status-offline.svg +1 -0
- package/dist/assets/icons/icon/stretch-horizontal.svg +1 -0
- package/dist/assets/icons/icon/stretch-vertical.svg +1 -0
- package/dist/assets/icons/icon/text-align-center.svg +1 -0
- package/dist/assets/icons/icon/text-align-left.svg +1 -0
- package/dist/assets/icons/icon/text-align-right.svg +1 -0
- package/dist/assets/icons/icon/toggle-off.svg +1 -0
- package/dist/assets/icons/icon/toggle-on.svg +1 -0
- package/dist/assets/icons/icon/tool-arrow.svg +1 -0
- package/dist/assets/icons/icon/tool-eraser.svg +1 -0
- package/dist/assets/icons/icon/tool-frame.svg +1 -0
- package/dist/assets/icons/icon/tool-hand.svg +1 -0
- package/dist/assets/icons/icon/tool-highlight.svg +1 -0
- package/dist/assets/icons/icon/tool-laser.svg +1 -0
- package/dist/assets/icons/icon/tool-line.svg +1 -0
- package/dist/assets/icons/icon/tool-media.svg +1 -0
- package/dist/assets/icons/icon/tool-note.svg +1 -0
- package/dist/assets/icons/icon/tool-pencil.svg +1 -0
- package/dist/assets/icons/icon/tool-pointer.svg +1 -0
- package/dist/assets/icons/icon/tool-screenshot.svg +1 -0
- package/dist/assets/icons/icon/tool-text.svg +1 -0
- package/dist/assets/icons/icon/trash.svg +1 -0
- package/dist/assets/icons/icon/twitter.svg +1 -0
- package/dist/assets/icons/icon/undo.svg +1 -0
- package/dist/assets/icons/icon/ungroup.svg +1 -0
- package/dist/assets/icons/icon/unlock.svg +1 -0
- package/dist/assets/icons/icon/vertical-align-end.svg +1 -0
- package/dist/assets/icons/icon/vertical-align-middle.svg +1 -0
- package/dist/assets/icons/icon/vertical-align-start.svg +1 -0
- package/dist/assets/icons/icon/warning-triangle.svg +1 -0
- package/dist/assets/icons/icon/zoom-in.svg +1 -0
- package/dist/assets/icons/icon/zoom-out.svg +1 -0
- package/dist/assets/translations/ar.json +295 -0
- package/dist/assets/translations/ca.json +295 -0
- package/dist/assets/translations/cs.json +352 -0
- package/dist/assets/translations/da.json +160 -0
- package/dist/assets/translations/de.json +310 -0
- package/dist/assets/translations/en.json +1 -0
- package/dist/assets/translations/es.json +299 -0
- package/dist/assets/translations/fa.json +384 -0
- package/dist/assets/translations/fi.json +312 -0
- package/dist/assets/translations/fr.json +372 -0
- package/dist/assets/translations/gl.json +349 -0
- package/dist/assets/translations/he.json +86 -0
- package/dist/assets/translations/hi-in.json +290 -0
- package/dist/assets/translations/hr.json +358 -0
- package/dist/assets/translations/hu.json +365 -0
- package/dist/assets/translations/id.json +373 -0
- package/dist/assets/translations/it.json +330 -0
- package/dist/assets/translations/ja.json +351 -0
- package/dist/assets/translations/ko-kr.json +396 -0
- package/dist/assets/translations/ku.json +93 -0
- package/dist/assets/translations/languages.json +150 -0
- package/dist/assets/translations/main.json +396 -0
- package/dist/assets/translations/my.json +112 -0
- package/dist/assets/translations/ne.json +295 -0
- package/dist/assets/translations/no.json +62 -0
- package/dist/assets/translations/pl.json +113 -0
- package/dist/assets/translations/pt-br.json +333 -0
- package/dist/assets/translations/pt-pt.json +86 -0
- package/dist/assets/translations/ro.json +371 -0
- package/dist/assets/translations/ru.json +375 -0
- package/dist/assets/translations/sl.json +373 -0
- package/dist/assets/translations/sv.json +96 -0
- package/dist/assets/translations/te.json +112 -0
- package/dist/assets/translations/th.json +295 -0
- package/dist/assets/translations/tr.json +355 -0
- package/dist/assets/translations/uk.json +375 -0
- package/dist/assets/translations/vi.json +295 -0
- package/dist/assets/translations/zh-cn.json +386 -0
- package/dist/assets/translations/zh-tw.json +340 -0
- package/dist/lib/TldrawPlugin.mjs +22 -0
- package/dist/lib/TldrawPlugin.mjs.map +1 -0
- package/dist/lib/assets/plugin-tldraw-BDrn_mrV.css +24 -0
- package/dist/lib/capabilities.mjs +8 -0
- package/dist/lib/capabilities.mjs.map +1 -0
- package/dist/lib/chunk-TldrawArticle.mjs +60 -0
- package/dist/lib/chunk-TldrawArticle.mjs.map +1 -0
- package/dist/lib/chunk-TldrawCard.mjs +35 -0
- package/dist/lib/chunk-TldrawCard.mjs.map +1 -0
- package/dist/lib/chunk-meta.mjs +40 -0
- package/dist/lib/chunk-meta.mjs.map +1 -0
- package/dist/lib/chunk-settings.mjs +23 -0
- package/dist/lib/chunk-settings.mjs.map +1 -0
- package/dist/lib/chunk-sketch-variant.mjs +23 -0
- package/dist/lib/chunk-sketch-variant.mjs.map +1 -0
- package/dist/lib/chunk-types.mjs +73 -0
- package/dist/lib/chunk-types.mjs.map +1 -0
- package/dist/lib/components.mjs +443 -0
- package/dist/lib/components.mjs.map +1 -0
- package/dist/lib/containers.mjs +8 -0
- package/dist/lib/containers.mjs.map +1 -0
- package/dist/lib/hooks.mjs +116 -0
- package/dist/lib/hooks.mjs.map +1 -0
- package/dist/lib/index.mjs +4 -0
- package/dist/lib/meta.mjs +2 -0
- package/dist/lib/model.mjs +905 -0
- package/dist/lib/model.mjs.map +1 -0
- package/dist/lib/plugin.mjs +8 -0
- package/dist/lib/plugin.mjs.map +1 -0
- package/dist/lib/testing.mjs +246 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/lib/translations.mjs +10 -0
- package/dist/lib/translations.mjs.map +1 -0
- package/dist/lib/types.mjs +2 -0
- package/dist/types/dx.config.d.ts +28 -0
- package/dist/types/dx.config.d.ts.map +1 -0
- package/dist/types/src/TldrawPlugin.d.ts +4 -0
- package/dist/types/src/TldrawPlugin.d.ts.map +1 -0
- package/dist/types/src/capabilities/index.d.ts +4 -0
- package/dist/types/src/capabilities/index.d.ts.map +1 -0
- package/dist/types/src/capabilities/settings.d.ts +5 -0
- package/dist/types/src/capabilities/settings.d.ts.map +1 -0
- package/dist/types/src/capabilities/sketch-variant.d.ts +5 -0
- package/dist/types/src/capabilities/sketch-variant.d.ts.map +1 -0
- package/dist/types/src/components/Canvas/Canvas.d.ts +17 -0
- package/dist/types/src/components/Canvas/Canvas.d.ts.map +1 -0
- package/dist/types/src/components/Canvas/Canvas.stories.d.ts +15 -0
- package/dist/types/src/components/Canvas/Canvas.stories.d.ts.map +1 -0
- package/dist/types/src/components/Canvas/index.d.ts +4 -0
- package/dist/types/src/components/Canvas/index.d.ts.map +1 -0
- package/dist/types/src/components/actions.d.ts +6 -0
- package/dist/types/src/components/actions.d.ts.map +1 -0
- package/dist/types/src/components/custom/CustomGrid.d.ts +12 -0
- package/dist/types/src/components/custom/CustomGrid.d.ts.map +1 -0
- package/dist/types/src/components/custom/CustomMenu.d.ts +4 -0
- package/dist/types/src/components/custom/CustomMenu.d.ts.map +1 -0
- package/dist/types/src/components/custom/CustomStylePanel.d.ts +4 -0
- package/dist/types/src/components/custom/CustomStylePanel.d.ts.map +1 -0
- package/dist/types/src/components/custom/CustomToolbar.d.ts +10 -0
- package/dist/types/src/components/custom/CustomToolbar.d.ts.map +1 -0
- package/dist/types/src/components/custom/DottedGrid.d.ts +4 -0
- package/dist/types/src/components/custom/DottedGrid.d.ts.map +1 -0
- package/dist/types/src/components/custom/MeshGrid.d.ts +4 -0
- package/dist/types/src/components/custom/MeshGrid.d.ts.map +1 -0
- package/dist/types/src/components/custom/index.d.ts +6 -0
- package/dist/types/src/components/custom/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +2 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/containers/TldrawArticle/TldrawArticle.d.ts +8 -0
- package/dist/types/src/containers/TldrawArticle/TldrawArticle.d.ts.map +1 -0
- package/dist/types/src/containers/TldrawArticle/index.d.ts +2 -0
- package/dist/types/src/containers/TldrawArticle/index.d.ts.map +1 -0
- package/dist/types/src/containers/TldrawCard/TldrawCard.d.ts +9 -0
- package/dist/types/src/containers/TldrawCard/TldrawCard.d.ts.map +1 -0
- package/dist/types/src/containers/TldrawCard/TldrawCard.stories.d.ts +15 -0
- package/dist/types/src/containers/TldrawCard/TldrawCard.stories.d.ts.map +1 -0
- package/dist/types/src/containers/TldrawCard/index.d.ts +2 -0
- package/dist/types/src/containers/TldrawCard/index.d.ts.map +1 -0
- package/dist/types/src/containers/index.d.ts +4 -0
- package/dist/types/src/containers/index.d.ts.map +1 -0
- package/dist/types/src/hooks/adapter.d.ts +15 -0
- package/dist/types/src/hooks/adapter.d.ts.map +1 -0
- package/dist/types/src/hooks/adapter.test.d.ts +2 -0
- package/dist/types/src/hooks/adapter.test.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +3 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useStoreAdapter.d.ts +11 -0
- package/dist/types/src/hooks/useStoreAdapter.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/meta.d.ts +33 -0
- package/dist/types/src/meta.d.ts.map +1 -0
- package/dist/types/src/migrations/index.d.ts +3 -0
- package/dist/types/src/migrations/index.d.ts.map +1 -0
- package/dist/types/src/migrations/migrate.d.ts +9 -0
- package/dist/types/src/migrations/migrate.d.ts.map +1 -0
- package/dist/types/src/migrations/schema.d.ts +7 -0
- package/dist/types/src/migrations/schema.d.ts.map +1 -0
- package/dist/types/src/model/TldrawBuilder.d.ts +110 -0
- package/dist/types/src/model/TldrawBuilder.d.ts.map +1 -0
- package/dist/types/src/model/TldrawBuilder.test.d.ts +2 -0
- package/dist/types/src/model/TldrawBuilder.test.d.ts.map +1 -0
- package/dist/types/src/model/apply.d.ts +5 -0
- package/dist/types/src/model/apply.d.ts.map +1 -0
- package/dist/types/src/model/builder.d.ts +3 -0
- package/dist/types/src/model/builder.d.ts.map +1 -0
- package/dist/types/src/model/index.d.ts +6 -0
- package/dist/types/src/model/index.d.ts.map +1 -0
- package/dist/types/src/model/read.d.ts +8 -0
- package/dist/types/src/model/read.d.ts.map +1 -0
- package/dist/types/src/model/render.d.ts +27 -0
- package/dist/types/src/model/render.d.ts.map +1 -0
- package/dist/types/src/model/scene.test.d.ts +2 -0
- package/dist/types/src/model/scene.test.d.ts.map +1 -0
- package/dist/types/src/plugin.d.ts +3 -0
- package/dist/types/src/plugin.d.ts.map +1 -0
- package/dist/types/src/testing/data.d.ts +242 -0
- package/dist/types/src/testing/data.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +2 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/schema.d.ts +7 -0
- package/dist/types/src/testing/schema.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +9 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/src/types/Settings.d.ts +10 -0
- package/dist/types/src/types/Settings.d.ts.map +1 -0
- package/dist/types/src/types/Tldraw.d.ts +25 -0
- package/dist/types/src/types/Tldraw.d.ts.map +1 -0
- package/dist/types/src/types/TldrawCapabilities.d.ts +4 -0
- package/dist/types/src/types/TldrawCapabilities.d.ts.map +1 -0
- package/dist/types/src/types/index.d.ts +4 -0
- package/dist/types/src/types/index.d.ts.map +1 -0
- package/dist/types/src/types/types.d.ts +6 -0
- package/dist/types/src/types/types.d.ts.map +1 -0
- package/dist/types/src/variant.test.d.ts +2 -0
- package/dist/types/src/variant.test.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/dx.config.ts +35 -0
- package/package.json +157 -0
- package/src/TldrawPlugin.tsx +31 -0
- package/src/capabilities/index.ts +8 -0
- package/src/capabilities/settings.ts +31 -0
- package/src/capabilities/sketch-variant.ts +29 -0
- package/src/components/Canvas/Canvas.stories.tsx +101 -0
- package/src/components/Canvas/Canvas.tsx +284 -0
- package/src/components/Canvas/index.ts +9 -0
- package/src/components/Canvas/theme.css +23 -0
- package/src/components/actions.ts +42 -0
- package/src/components/custom/CustomGrid.tsx +61 -0
- package/src/components/custom/CustomMenu.tsx +31 -0
- package/src/components/custom/CustomStylePanel.tsx +38 -0
- package/src/components/custom/CustomToolbar.tsx +88 -0
- package/src/components/custom/DottedGrid.tsx +53 -0
- package/src/components/custom/MeshGrid.tsx +47 -0
- package/src/components/custom/index.ts +9 -0
- package/src/components/index.ts +5 -0
- package/src/containers/TldrawArticle/TldrawArticle.tsx +78 -0
- package/src/containers/TldrawArticle/index.ts +5 -0
- package/src/containers/TldrawCard/TldrawCard.stories.tsx +53 -0
- package/src/containers/TldrawCard/TldrawCard.tsx +32 -0
- package/src/containers/TldrawCard/index.ts +5 -0
- package/src/containers/index.ts +8 -0
- package/src/hooks/adapter.test.ts +20 -0
- package/src/hooks/adapter.ts +83 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useStoreAdapter.ts +55 -0
- package/src/index.ts +7 -0
- package/src/meta.ts +9 -0
- package/src/migrations/index.ts +6 -0
- package/src/migrations/migrate.ts +49 -0
- package/src/migrations/schema.ts +98 -0
- package/src/model/TldrawBuilder.test.ts +72 -0
- package/src/model/TldrawBuilder.ts +443 -0
- package/src/model/apply.ts +132 -0
- package/src/model/builder.ts +31 -0
- package/src/model/index.ts +9 -0
- package/src/model/read.ts +203 -0
- package/src/model/render.ts +191 -0
- package/src/model/scene.test.ts +222 -0
- package/src/plugin.ts +9 -0
- package/src/testing/data.ts +238 -0
- package/src/testing/index.ts +5 -0
- package/src/testing/schema.ts +98 -0
- package/src/translations.ts +18 -0
- package/src/types/Settings.ts +32 -0
- package/src/types/Tldraw.ts +38 -0
- package/src/types/TldrawCapabilities.ts +17 -0
- package/src/types/index.ts +8 -0
- package/src/types/types.ts +11 -0
- package/src/typings.d.ts +8 -0
- package/src/variant.test.ts +142 -0
- package/src/vite-env.d.ts +10 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
//
|
|
6
|
+
// Derives the neutral scene from tldraw records. Geometry is read from the live records —
|
|
7
|
+
// not from a stored copy — so the agent's mental model stays coherent after users drag or
|
|
8
|
+
// resize shapes in the UI. An object's `origin` is the top-left of its bounding box.
|
|
9
|
+
//
|
|
10
|
+
|
|
11
|
+
import { type ReadResult, type ReadWorldObject, type Scene } from '@dxos/plugin-illustrator/model';
|
|
12
|
+
|
|
13
|
+
import { type ExternalBox } from './render';
|
|
14
|
+
import { type CanvasContent } from './TldrawBuilder';
|
|
15
|
+
|
|
16
|
+
type ShapeRecord = Record<string, any>;
|
|
17
|
+
|
|
18
|
+
/** Read the derived scene from canvas content. */
|
|
19
|
+
export const readScene = (content: CanvasContent): ReadResult => {
|
|
20
|
+
const groups = new Map<string, ShapeRecord[]>();
|
|
21
|
+
let unmanaged = 0;
|
|
22
|
+
for (const record of Object.values(content ?? {})) {
|
|
23
|
+
if (record?.typeName !== 'shape') {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const objectId = record.meta?.object;
|
|
27
|
+
if (typeof objectId !== 'string' || typeof record.meta?.element !== 'string') {
|
|
28
|
+
unmanaged++;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
groups.set(objectId, [...(groups.get(objectId) ?? []), record]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const objects = [...groups.entries()].map(([objectId, records]) => readObject(objectId, records, content));
|
|
35
|
+
return { scene: { objects }, unmanaged };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Canvas-px bounding boxes of all managed elements, keyed by `objectId/elementId`. */
|
|
39
|
+
export const elementBoxes = (content: CanvasContent): Record<string, ExternalBox> => {
|
|
40
|
+
const boxes: Record<string, ExternalBox> = {};
|
|
41
|
+
for (const record of Object.values(content ?? {})) {
|
|
42
|
+
if (record?.typeName === 'shape' && record.meta?.object && record.meta?.element) {
|
|
43
|
+
boxes[`${record.meta.object}/${record.meta.element}`] = bounds(record);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return boxes;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const readObject = (objectId: string, records: ShapeRecord[], content: CanvasContent): ReadWorldObject => {
|
|
50
|
+
const scale = records.find((record) => typeof record.meta?.scale === 'number')?.meta.scale ?? 1;
|
|
51
|
+
const origin = {
|
|
52
|
+
x: Math.min(...records.map((record) => bounds(record).x)),
|
|
53
|
+
y: Math.min(...records.map((record) => bounds(record).y)),
|
|
54
|
+
};
|
|
55
|
+
const local = (abs: { x: number; y: number }): Scene.Point => ({
|
|
56
|
+
x: round((abs.x - origin.x) / scale),
|
|
57
|
+
y: round((abs.y - origin.y) / scale),
|
|
58
|
+
});
|
|
59
|
+
const length = (px: number) => round(px / scale);
|
|
60
|
+
|
|
61
|
+
const elements = records
|
|
62
|
+
.map((record) => readElement(record, { local, length, objectId, content }))
|
|
63
|
+
.filter((element): element is Scene.Element => element !== undefined);
|
|
64
|
+
|
|
65
|
+
return { id: objectId, origin, scale, elements };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type ReadContext = {
|
|
69
|
+
local: (abs: { x: number; y: number }) => Scene.Point;
|
|
70
|
+
length: (px: number) => number;
|
|
71
|
+
objectId: string;
|
|
72
|
+
content: CanvasContent;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const readElement = (record: ShapeRecord, ctx: ReadContext): Scene.Element | undefined => {
|
|
76
|
+
const id = record.meta.element;
|
|
77
|
+
const kind = record.meta.kind;
|
|
78
|
+
switch (record.type) {
|
|
79
|
+
case 'geo': {
|
|
80
|
+
const boxKind = kind === 'rect' || kind === 'diamond' || kind === 'triangle' ? kind : 'ellipse';
|
|
81
|
+
return {
|
|
82
|
+
kind: boxKind,
|
|
83
|
+
id,
|
|
84
|
+
...ctx.local({ x: record.x, y: record.y }),
|
|
85
|
+
w: ctx.length(record.props.w),
|
|
86
|
+
h: ctx.length(record.props.h),
|
|
87
|
+
...(record.rotation ? { rotation: round((record.rotation * 180) / Math.PI) } : {}),
|
|
88
|
+
...(record.props.text ? { text: record.props.text } : {}),
|
|
89
|
+
...readStyle(record),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
case 'text': {
|
|
93
|
+
return {
|
|
94
|
+
kind: 'text',
|
|
95
|
+
id,
|
|
96
|
+
...ctx.local({ x: record.x, y: record.y }),
|
|
97
|
+
...(record.props.autoSize ? {} : { w: ctx.length(record.props.w) }),
|
|
98
|
+
text: record.props.text ?? '',
|
|
99
|
+
...readStyle(record),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
case 'line': {
|
|
103
|
+
const points = linePoints(record).map((point) => ctx.local({ x: record.x + point.x, y: record.y + point.y }));
|
|
104
|
+
if (kind === 'curve') {
|
|
105
|
+
return { kind: 'curve', id, points, ...readStyle(record) };
|
|
106
|
+
}
|
|
107
|
+
const closed =
|
|
108
|
+
points.length > 2 && points[0].x === points[points.length - 1].x && points[0].y === points[points.length - 1].y;
|
|
109
|
+
return {
|
|
110
|
+
kind: 'line',
|
|
111
|
+
id,
|
|
112
|
+
points: closed ? points.slice(0, -1) : points,
|
|
113
|
+
...(closed ? { closed: true } : {}),
|
|
114
|
+
...readStyle(record),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
case 'arrow': {
|
|
118
|
+
const { from, to } = arrowRefs(record, ctx);
|
|
119
|
+
return {
|
|
120
|
+
kind: 'arrow',
|
|
121
|
+
id,
|
|
122
|
+
...(from
|
|
123
|
+
? { from }
|
|
124
|
+
: { start: ctx.local({ x: record.x + record.props.start.x, y: record.y + record.props.start.y }) }),
|
|
125
|
+
...(to ? { to } : { end: ctx.local({ x: record.x + record.props.end.x, y: record.y + record.props.end.y }) }),
|
|
126
|
+
...(record.props.text ? { text: record.props.text } : {}),
|
|
127
|
+
...readStyle(record),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
default:
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** Resolve arrow terminal bindings back to element refs (same-object refs are shortened). */
|
|
136
|
+
const arrowRefs = (record: ShapeRecord, ctx: ReadContext): { from?: string; to?: string } => {
|
|
137
|
+
const refs: { from?: string; to?: string } = {};
|
|
138
|
+
for (const binding of Object.values(ctx.content ?? {})) {
|
|
139
|
+
if (binding?.typeName !== 'binding' || binding.fromId !== record.id) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const target = ctx.content[binding.toId];
|
|
143
|
+
if (!target?.meta?.object || !target?.meta?.element) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const ref =
|
|
147
|
+
target.meta.object === ctx.objectId ? target.meta.element : `${target.meta.object}/${target.meta.element}`;
|
|
148
|
+
if (binding.props?.terminal === 'start') {
|
|
149
|
+
refs.from = ref;
|
|
150
|
+
} else {
|
|
151
|
+
refs.to = ref;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return refs;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** Report only non-default styles to keep the read output compact. */
|
|
158
|
+
const readStyle = (record: ShapeRecord): Partial<Pick<Scene.Box, 'color' | 'fill' | 'stroke' | 'weight'>> => {
|
|
159
|
+
const { color, fill, dash, size } = record.props;
|
|
160
|
+
return {
|
|
161
|
+
...(color && color !== 'black' ? { color } : {}),
|
|
162
|
+
...(fill && fill !== 'none' ? { fill: fill === 'pattern' ? ('pattern' as const) : ('solid' as const) } : {}),
|
|
163
|
+
...(dash && dash !== 'draw' ? { stroke: dash as Scene.Stroke } : {}),
|
|
164
|
+
...(size && size !== 'm' ? { weight: size as Scene.Weight } : {}),
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const linePoints = (record: ShapeRecord): { x: number; y: number }[] =>
|
|
169
|
+
Object.values(record.props.points ?? {})
|
|
170
|
+
// Indexes are `a<n>` fractional keys; numeric compare so `a10` sorts after `a2`.
|
|
171
|
+
.sort((a: any, b: any) => Number(String(a.index).slice(1)) - Number(String(b.index).slice(1)))
|
|
172
|
+
.map((point: any) => ({ x: point.x, y: point.y }));
|
|
173
|
+
|
|
174
|
+
/** Bounding box of a shape record in canvas px. */
|
|
175
|
+
const bounds = (record: ShapeRecord): ExternalBox => {
|
|
176
|
+
switch (record.type) {
|
|
177
|
+
case 'geo':
|
|
178
|
+
return { x: record.x, y: record.y, w: record.props.w, h: record.props.h };
|
|
179
|
+
case 'text':
|
|
180
|
+
return { x: record.x, y: record.y, w: record.props.w ?? 0, h: 0 };
|
|
181
|
+
case 'line': {
|
|
182
|
+
const points = linePoints(record);
|
|
183
|
+
const xs = points.map((point) => record.x + point.x);
|
|
184
|
+
const ys = points.map((point) => record.y + point.y);
|
|
185
|
+
return boxFrom(xs, ys);
|
|
186
|
+
}
|
|
187
|
+
case 'arrow': {
|
|
188
|
+
const xs = [record.x + record.props.start.x, record.x + record.props.end.x];
|
|
189
|
+
const ys = [record.y + record.props.start.y, record.y + record.props.end.y];
|
|
190
|
+
return boxFrom(xs, ys);
|
|
191
|
+
}
|
|
192
|
+
default:
|
|
193
|
+
return { x: record.x ?? 0, y: record.y ?? 0, w: record.props?.w ?? 0, h: record.props?.h ?? 0 };
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const boxFrom = (xs: number[], ys: number[]): ExternalBox => {
|
|
198
|
+
const x = Math.min(...xs);
|
|
199
|
+
const y = Math.min(...ys);
|
|
200
|
+
return { x, y, w: Math.max(...xs) - x, h: Math.max(...ys) - y };
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const round = (value: number) => Math.round(value * 100) / 100;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
//
|
|
6
|
+
// tldraw backend for the scene DSL: compiles a `WorldObject` into tldraw records.
|
|
7
|
+
// Element identity is stamped into each record's `meta` (`{ object, element, kind, scale }`)
|
|
8
|
+
// so `read.ts` can reconstruct the scene and edits can address records by id.
|
|
9
|
+
//
|
|
10
|
+
|
|
11
|
+
import { Scene } from '@dxos/plugin-illustrator/model';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
type CanvasContent,
|
|
15
|
+
type ConnectorProps,
|
|
16
|
+
type Dash,
|
|
17
|
+
type Fill,
|
|
18
|
+
type PathProps,
|
|
19
|
+
type Point,
|
|
20
|
+
type ShapeProps,
|
|
21
|
+
TldrawBuilder,
|
|
22
|
+
} from './TldrawBuilder';
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_SCALE = 1;
|
|
25
|
+
|
|
26
|
+
/** Canvas-px bounding box of an existing element another object's arrow may bind to. */
|
|
27
|
+
export type ExternalBox = { x: number; y: number; w: number; h: number };
|
|
28
|
+
|
|
29
|
+
export type RenderOptions = {
|
|
30
|
+
origin: Scene.Point;
|
|
31
|
+
scale: number;
|
|
32
|
+
/** Fractional-index seed past the existing canvas content. */
|
|
33
|
+
indexStart: number;
|
|
34
|
+
/** Bounding boxes of existing elements, keyed by `objectId/elementId` (canvas px). */
|
|
35
|
+
external?: Record<string, ExternalBox>;
|
|
36
|
+
dialect?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Stable record id for an element: `shape:<objectId>/<elementId>`. */
|
|
40
|
+
export const shapeId = (objectId: string, elementId: string) => `shape:${objectId}/${elementId}`;
|
|
41
|
+
|
|
42
|
+
const strokeToDash: Record<Scene.Stroke, Dash> = {
|
|
43
|
+
sketchy: 'draw',
|
|
44
|
+
solid: 'solid',
|
|
45
|
+
dashed: 'dashed',
|
|
46
|
+
dotted: 'dotted',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const fillToFill: Record<Scene.Fill, Fill> = {
|
|
50
|
+
none: 'none',
|
|
51
|
+
solid: 'solid',
|
|
52
|
+
pattern: 'pattern',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const boxKindToGeo = {
|
|
56
|
+
rect: 'rectangle',
|
|
57
|
+
ellipse: 'ellipse',
|
|
58
|
+
diamond: 'diamond',
|
|
59
|
+
triangle: 'triangle',
|
|
60
|
+
} as const;
|
|
61
|
+
|
|
62
|
+
const style = (element: Scene.Element): Pick<ShapeProps, 'color' | 'fill' | 'dash' | 'size'> => ({
|
|
63
|
+
color: element.color,
|
|
64
|
+
fill: 'fill' in element && element.fill ? fillToFill[element.fill] : undefined,
|
|
65
|
+
dash: element.stroke ? strokeToDash[element.stroke] : undefined,
|
|
66
|
+
size: element.weight,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Compile one world object into tldraw records (canvas-px coordinates).
|
|
71
|
+
* Local coordinates are mapped via `canvas = origin + local * scale`.
|
|
72
|
+
*/
|
|
73
|
+
export const renderObject = (object: Scene.WorldObject, options: RenderOptions): CanvasContent => {
|
|
74
|
+
const { origin, scale, indexStart, external = {}, dialect = 'scene' } = options;
|
|
75
|
+
const point = (local: Scene.Point | { x: number; y: number }): Point => ({
|
|
76
|
+
x: origin.x + local.x * scale,
|
|
77
|
+
y: origin.y + local.y * scale,
|
|
78
|
+
});
|
|
79
|
+
const length = (units: number) => units * scale;
|
|
80
|
+
|
|
81
|
+
const kinds = new Map<string, string>();
|
|
82
|
+
const builder = new TldrawBuilder({
|
|
83
|
+
indexStart,
|
|
84
|
+
metaFor: (handle) => {
|
|
85
|
+
const [objectId, elementId] = splitHandle(handle);
|
|
86
|
+
return { object: objectId, element: elementId, kind: kinds.get(handle), scale, dialect };
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Register existing canvas elements so arrows can bind across objects.
|
|
91
|
+
for (const [handle, box] of Object.entries(external)) {
|
|
92
|
+
builder.external(handle, box);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Refs are element ids within this object, or `objectId/elementId` across objects.
|
|
96
|
+
const ref = (value: string) => (value.includes('/') ? value : `${object.id}/${value}`);
|
|
97
|
+
|
|
98
|
+
for (const element of object.elements) {
|
|
99
|
+
const handle = `${object.id}/${element.id}`;
|
|
100
|
+
kinds.set(handle, element.kind);
|
|
101
|
+
switch (element.kind) {
|
|
102
|
+
case 'rect':
|
|
103
|
+
case 'ellipse':
|
|
104
|
+
case 'diamond':
|
|
105
|
+
case 'triangle': {
|
|
106
|
+
builder.geo(boxKindToGeo[element.kind], {
|
|
107
|
+
id: handle,
|
|
108
|
+
...point(element),
|
|
109
|
+
w: length(element.w),
|
|
110
|
+
h: length(element.h),
|
|
111
|
+
rotation: element.rotation !== undefined ? (element.rotation * Math.PI) / 180 : undefined,
|
|
112
|
+
text: element.text,
|
|
113
|
+
...style(element),
|
|
114
|
+
});
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case 'circle': {
|
|
118
|
+
kinds.set(handle, 'ellipse');
|
|
119
|
+
builder.circle({
|
|
120
|
+
id: handle,
|
|
121
|
+
...point({ x: element.cx - element.r, y: element.cy - element.r }),
|
|
122
|
+
w: length(element.r * 2),
|
|
123
|
+
text: element.text,
|
|
124
|
+
...style(element),
|
|
125
|
+
});
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case 'line': {
|
|
129
|
+
const props: PathProps = {
|
|
130
|
+
id: handle,
|
|
131
|
+
points: element.points.map(point),
|
|
132
|
+
closed: element.closed,
|
|
133
|
+
...style(element),
|
|
134
|
+
};
|
|
135
|
+
builder.path(props);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
case 'curve': {
|
|
139
|
+
builder.path({ id: handle, points: element.points.map(point), smooth: true, ...style(element) });
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
case 'arc': {
|
|
143
|
+
kinds.set(handle, 'curve');
|
|
144
|
+
builder.path({ id: handle, points: sampleArc(element).map(point), smooth: true, ...style(element) });
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case 'text': {
|
|
148
|
+
builder.text({
|
|
149
|
+
id: handle,
|
|
150
|
+
...point(element),
|
|
151
|
+
w: element.w !== undefined ? length(element.w) : undefined,
|
|
152
|
+
text: element.text,
|
|
153
|
+
color: element.color,
|
|
154
|
+
size: element.weight,
|
|
155
|
+
});
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case 'arrow': {
|
|
159
|
+
const props: ConnectorProps = {
|
|
160
|
+
id: handle,
|
|
161
|
+
from: element.from ? ref(element.from) : undefined,
|
|
162
|
+
to: element.to ? ref(element.to) : undefined,
|
|
163
|
+
start: element.start ? point(element.start) : undefined,
|
|
164
|
+
end: element.end ? point(element.end) : undefined,
|
|
165
|
+
text: element.text,
|
|
166
|
+
...style(element),
|
|
167
|
+
};
|
|
168
|
+
builder.arrow(props);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return builder.records();
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/** `objectId/elementId` — element ids may themselves not contain `/`. */
|
|
178
|
+
const splitHandle = (handle: string): [string, string] => {
|
|
179
|
+
const index = handle.indexOf('/');
|
|
180
|
+
return [handle.slice(0, index), handle.slice(index + 1)];
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** Sample an arc (degrees, clockwise from +x, y-down screen coords) into spline points. */
|
|
184
|
+
const sampleArc = ({ cx, cy, r, startAngle, endAngle }: Scene.Arc): Scene.Point[] => {
|
|
185
|
+
const sweep = endAngle - startAngle;
|
|
186
|
+
const steps = Math.max(4, Math.ceil(Math.abs(sweep) / 22.5));
|
|
187
|
+
return Array.from({ length: steps + 1 }, (_, i) => {
|
|
188
|
+
const angle = ((startAngle + (sweep * i) / steps) * Math.PI) / 180;
|
|
189
|
+
return { x: cx + r * Math.cos(angle), y: cy + r * Math.sin(angle) };
|
|
190
|
+
});
|
|
191
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { createTLSchema } from '@tldraw/tlschema';
|
|
6
|
+
import { describe, test } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import { type Scene } from '@dxos/plugin-illustrator/model';
|
|
9
|
+
|
|
10
|
+
import { applyCommands } from './apply';
|
|
11
|
+
import { readScene } from './read';
|
|
12
|
+
|
|
13
|
+
/** A face made of every element category: boxes, circles, arc sugar, polyline, text. */
|
|
14
|
+
const face: Scene.WorldObject = {
|
|
15
|
+
id: 'face',
|
|
16
|
+
origin: { x: 100, y: 100 },
|
|
17
|
+
scale: 2,
|
|
18
|
+
elements: [
|
|
19
|
+
{ kind: 'circle', id: 'head', cx: 50, cy: 50, r: 50, color: 'yellow', fill: 'solid' },
|
|
20
|
+
{ kind: 'circle', id: 'left-eye', cx: 30, cy: 35, r: 6, fill: 'solid' },
|
|
21
|
+
{ kind: 'circle', id: 'right-eye', cx: 70, cy: 35, r: 6, fill: 'solid' },
|
|
22
|
+
{
|
|
23
|
+
kind: 'line',
|
|
24
|
+
id: 'nose',
|
|
25
|
+
points: [
|
|
26
|
+
{ x: 50, y: 45 },
|
|
27
|
+
{ x: 50, y: 60 },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{ kind: 'arc', id: 'smile', cx: 50, cy: 55, r: 25, startAngle: 30, endAngle: 150, color: 'red' },
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const hat: Scene.WorldObject = {
|
|
35
|
+
id: 'hat',
|
|
36
|
+
origin: { x: 120, y: 40 },
|
|
37
|
+
scale: 2,
|
|
38
|
+
elements: [
|
|
39
|
+
{ kind: 'rect', id: 'brim', x: 0, y: 25, w: 80, h: 6, color: 'blue', fill: 'solid' },
|
|
40
|
+
{ kind: 'rect', id: 'crown', x: 20, y: 0, w: 40, h: 25, color: 'blue', fill: 'solid' },
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const upsert = (object: Scene.WorldObject): Scene.Command => ({ op: 'upsert-object', object });
|
|
45
|
+
|
|
46
|
+
describe('scene DSL', () => {
|
|
47
|
+
test('renders objects and reads back a coherent scene', ({ expect }) => {
|
|
48
|
+
const content = {};
|
|
49
|
+
const result = applyCommands(content, [upsert(face)]);
|
|
50
|
+
expect(result.upserted).to.deep.eq(['face']);
|
|
51
|
+
|
|
52
|
+
const { scene, unmanaged } = readScene(content);
|
|
53
|
+
expect(unmanaged).to.eq(0);
|
|
54
|
+
expect(scene.objects).to.have.length(1);
|
|
55
|
+
|
|
56
|
+
const read = scene.objects[0];
|
|
57
|
+
expect(read.id).to.eq('face');
|
|
58
|
+
// Origin is derived from the bounding box: head top-left = origin + (0,0) * scale.
|
|
59
|
+
expect(read.origin).to.deep.eq({ x: 100, y: 100 });
|
|
60
|
+
expect(read.scale).to.eq(2);
|
|
61
|
+
expect(read.elements.map((element) => element.id)).to.deep.eq(['head', 'left-eye', 'right-eye', 'nose', 'smile']);
|
|
62
|
+
|
|
63
|
+
// Circle sugar reads back as an ellipse in local units.
|
|
64
|
+
const head = read.elements.find((element) => element.id === 'head');
|
|
65
|
+
expect(head).to.include({ kind: 'ellipse', x: 0, y: 0, w: 100, h: 100 });
|
|
66
|
+
// Arc sugar reads back as a curve.
|
|
67
|
+
const smile = read.elements.find((element) => element.id === 'smile');
|
|
68
|
+
expect(smile?.kind).to.eq('curve');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('rendered records pass tldraw schema validation', ({ expect }) => {
|
|
72
|
+
const content = {};
|
|
73
|
+
applyCommands(content, [
|
|
74
|
+
upsert(face),
|
|
75
|
+
upsert(hat),
|
|
76
|
+
upsert({
|
|
77
|
+
id: 'labels',
|
|
78
|
+
origin: { x: 0, y: 400 },
|
|
79
|
+
elements: [
|
|
80
|
+
{ kind: 'text', id: 'caption', x: 0, y: 0, text: 'portrait' },
|
|
81
|
+
{ kind: 'arrow', id: 'points-at', from: 'caption', to: 'face/head', text: 'head' },
|
|
82
|
+
{
|
|
83
|
+
kind: 'line',
|
|
84
|
+
id: 'zigzag',
|
|
85
|
+
points: [
|
|
86
|
+
{ x: 0, y: 20 },
|
|
87
|
+
{ x: 10, y: 30 },
|
|
88
|
+
{ x: 20, y: 20 },
|
|
89
|
+
],
|
|
90
|
+
closed: true,
|
|
91
|
+
},
|
|
92
|
+
{ kind: 'diamond', id: 'gem', x: 40, y: 20, w: 20, h: 20, rotation: 45, stroke: 'dashed', weight: 'l' },
|
|
93
|
+
],
|
|
94
|
+
}),
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
const schema = createTLSchema();
|
|
98
|
+
// tldraw types its record map as a fixed-key object without an index signature;
|
|
99
|
+
// validating an arbitrary record requires a string-keyed view.
|
|
100
|
+
const validators = schema.types as Record<string, { validate: (record: unknown) => unknown }>;
|
|
101
|
+
for (const record of Object.values(content) as any[]) {
|
|
102
|
+
expect(() => validators[record.typeName].validate(record), record.id).to.not.throw();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('cross-object arrows bind to their targets', ({ expect }) => {
|
|
107
|
+
const content: Record<string, any> = {};
|
|
108
|
+
applyCommands(content, [
|
|
109
|
+
upsert(face),
|
|
110
|
+
upsert({
|
|
111
|
+
id: 'hat',
|
|
112
|
+
origin: { x: 120, y: 40 },
|
|
113
|
+
elements: [...hat.elements, { kind: 'arrow', id: 'sits-on', from: 'brim', to: 'face/head' }],
|
|
114
|
+
}),
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const bindings = Object.values(content).filter((record) => record.typeName === 'binding');
|
|
118
|
+
expect(bindings.map((binding) => binding.toId).sort()).to.deep.eq(['shape:face/head', 'shape:hat/brim']);
|
|
119
|
+
|
|
120
|
+
const { scene } = readScene(content);
|
|
121
|
+
const arrow = scene.objects
|
|
122
|
+
.find((object) => object.id === 'hat')
|
|
123
|
+
?.elements.find((element) => element.id === 'sits-on');
|
|
124
|
+
expect(arrow).to.include({ kind: 'arrow', from: 'brim', to: 'face/head' });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('upsert-elements replaces by id and keeps the rest', ({ expect }) => {
|
|
128
|
+
const content = {};
|
|
129
|
+
applyCommands(content, [upsert(face)]);
|
|
130
|
+
const before = readScene(content).scene.objects[0];
|
|
131
|
+
|
|
132
|
+
// Make the smile a frown (upper arc), leave everything else untouched.
|
|
133
|
+
applyCommands(content, [
|
|
134
|
+
{
|
|
135
|
+
op: 'upsert-elements',
|
|
136
|
+
objectId: 'face',
|
|
137
|
+
elements: [{ kind: 'arc', id: 'smile', cx: 50, cy: 70, r: 20, startAngle: 210, endAngle: 330 }],
|
|
138
|
+
},
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
const after = readScene(content).scene.objects[0];
|
|
142
|
+
expect(after.elements).to.have.length(before.elements.length);
|
|
143
|
+
expect(after.elements.find((element) => element.id === 'head')).to.deep.eq(
|
|
144
|
+
before.elements.find((element) => element.id === 'head'),
|
|
145
|
+
);
|
|
146
|
+
expect(after.elements.find((element) => element.id === 'smile')).to.not.deep.eq(
|
|
147
|
+
before.elements.find((element) => element.id === 'smile'),
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('remove and move commands', ({ expect }) => {
|
|
152
|
+
const content: Record<string, any> = {};
|
|
153
|
+
applyCommands(content, [upsert(face), upsert(hat)]);
|
|
154
|
+
|
|
155
|
+
const moved = applyCommands(content, [{ op: 'move-object', objectId: 'hat', origin: { x: 220, y: 140 } }]);
|
|
156
|
+
expect(moved.upserted).to.deep.eq(['hat']);
|
|
157
|
+
const hatRead = readScene(content).scene.objects.find((object) => object.id === 'hat');
|
|
158
|
+
expect(hatRead?.origin).to.deep.eq({ x: 220, y: 140 });
|
|
159
|
+
// Local geometry is unchanged by a move.
|
|
160
|
+
expect(hatRead?.elements.find((element) => element.id === 'brim')).to.include({ x: 0, y: 25, w: 80, h: 6 });
|
|
161
|
+
|
|
162
|
+
const removedElements = applyCommands(content, [
|
|
163
|
+
{ op: 'remove-elements', objectId: 'face', elementIds: ['left-eye', 'right-eye'] },
|
|
164
|
+
]);
|
|
165
|
+
expect(removedElements.removed).to.eq(2);
|
|
166
|
+
|
|
167
|
+
const removedObject = applyCommands(content, [{ op: 'remove-object', objectId: 'hat' }]);
|
|
168
|
+
expect(removedObject.removed).to.eq(2);
|
|
169
|
+
expect(readScene(content).scene.objects.map((object) => object.id)).to.deep.eq(['face']);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('removing an arrow target also drops its bindings', ({ expect }) => {
|
|
173
|
+
const content: Record<string, any> = {};
|
|
174
|
+
applyCommands(content, [
|
|
175
|
+
upsert({
|
|
176
|
+
id: 'graph',
|
|
177
|
+
origin: { x: 0, y: 0 },
|
|
178
|
+
elements: [
|
|
179
|
+
{ kind: 'rect', id: 'a', x: 0, y: 0, w: 40, h: 20 },
|
|
180
|
+
{ kind: 'rect', id: 'b', x: 100, y: 0, w: 40, h: 20 },
|
|
181
|
+
{ kind: 'arrow', id: 'a-to-b', from: 'a', to: 'b' },
|
|
182
|
+
],
|
|
183
|
+
}),
|
|
184
|
+
]);
|
|
185
|
+
expect(Object.values(content).filter((record) => record.typeName === 'binding')).to.have.length(2);
|
|
186
|
+
|
|
187
|
+
applyCommands(content, [{ op: 'remove-elements', objectId: 'graph', elementIds: ['b'] }]);
|
|
188
|
+
const bindings = Object.values(content).filter((record) => record.typeName === 'binding');
|
|
189
|
+
// Only the binding to the surviving shape remains; nothing dangles.
|
|
190
|
+
expect(bindings).to.have.length(1);
|
|
191
|
+
expect(bindings[0].toId).to.eq('shape:graph/a');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('unmanaged shapes are counted but not read as scene objects', ({ expect }) => {
|
|
195
|
+
const content: Record<string, any> = {};
|
|
196
|
+
applyCommands(content, [upsert(hat)]);
|
|
197
|
+
content['shape:user'] = {
|
|
198
|
+
id: 'shape:user',
|
|
199
|
+
typeName: 'shape',
|
|
200
|
+
type: 'geo',
|
|
201
|
+
x: 0,
|
|
202
|
+
y: 0,
|
|
203
|
+
rotation: 0,
|
|
204
|
+
index: 'a99',
|
|
205
|
+
parentId: 'page:page',
|
|
206
|
+
isLocked: false,
|
|
207
|
+
opacity: 1,
|
|
208
|
+
meta: {},
|
|
209
|
+
props: { geo: 'rectangle', w: 10, h: 10 },
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const { scene, unmanaged } = readScene(content);
|
|
213
|
+
expect(unmanaged).to.eq(1);
|
|
214
|
+
expect(scene.objects.map((object) => object.id)).to.deep.eq(['hat']);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('upsert-elements on an unknown object fails with guidance', ({ expect }) => {
|
|
218
|
+
expect(() => applyCommands({}, [{ op: 'upsert-elements', objectId: 'ghost', elements: [] }])).to.throw(
|
|
219
|
+
/unknown object/,
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
});
|