added scoped propagators (with javascript object on arrow edge to control)
This commit is contained in:
parent
3d51785ecd
commit
2590a86352
|
|
@ -28,6 +28,7 @@
|
||||||
"jspdf": "^2.5.2",
|
"jspdf": "^2.5.2",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"marked": "^15.0.4",
|
"marked": "^15.0.4",
|
||||||
|
"rbush": "^4.0.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^7.0.2",
|
"react-router-dom": "^7.0.2",
|
||||||
|
|
@ -39,6 +40,7 @@
|
||||||
"@cloudflare/types": "^6.0.0",
|
"@cloudflare/types": "^6.0.0",
|
||||||
"@cloudflare/workers-types": "^4.20240821.1",
|
"@cloudflare/workers-types": "^4.20240821.1",
|
||||||
"@types/lodash.throttle": "^4",
|
"@types/lodash.throttle": "^4",
|
||||||
|
"@types/rbush": "^4.0.0",
|
||||||
"@types/react": "^19.0.1",
|
"@types/react": "^19.0.1",
|
||||||
"@types/react-dom": "^19.0.1",
|
"@types/react-dom": "^19.0.1",
|
||||||
"@vitejs/plugin-react": "^4.0.3",
|
"@vitejs/plugin-react": "^4.0.3",
|
||||||
|
|
@ -2852,6 +2854,13 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/rbush": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.0.2",
|
"version": "19.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.2.tgz",
|
||||||
|
|
@ -6940,6 +6949,12 @@
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/quickselect": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/raf": {
|
"node_modules/raf": {
|
||||||
"version": "3.4.1",
|
"version": "3.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||||
|
|
@ -6993,6 +7008,15 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rbush": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rbush/-/rbush-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"quickselect": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
"jspdf": "^2.5.2",
|
"jspdf": "^2.5.2",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"marked": "^15.0.4",
|
"marked": "^15.0.4",
|
||||||
|
"rbush": "^4.0.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^7.0.2",
|
"react-router-dom": "^7.0.2",
|
||||||
|
|
@ -45,6 +46,7 @@
|
||||||
"@cloudflare/types": "^6.0.0",
|
"@cloudflare/types": "^6.0.0",
|
||||||
"@cloudflare/workers-types": "^4.20240821.1",
|
"@cloudflare/workers-types": "^4.20240821.1",
|
||||||
"@types/lodash.throttle": "^4",
|
"@types/lodash.throttle": "^4",
|
||||||
|
"@types/rbush": "^4.0.0",
|
||||||
"@types/react": "^19.0.1",
|
"@types/react": "^19.0.1",
|
||||||
"@types/react-dom": "^19.0.1",
|
"@types/react-dom": "^19.0.1",
|
||||||
"@vitejs/plugin-react": "^4.0.3",
|
"@vitejs/plugin-react": "^4.0.3",
|
||||||
|
|
|
||||||
27
src/App.tsx
27
src/App.tsx
|
|
@ -6,39 +6,12 @@ import { BrowserRouter, Route, Routes } from "react-router-dom"
|
||||||
import { Contact } from "@/routes/Contact"
|
import { Contact } from "@/routes/Contact"
|
||||||
import { Board } from "./routes/Board"
|
import { Board } from "./routes/Board"
|
||||||
import { Inbox } from "./routes/Inbox"
|
import { Inbox } from "./routes/Inbox"
|
||||||
import { ChatBoxShape } from "./shapes/ChatBoxShapeUtil"
|
|
||||||
import { VideoChatShape } from "./shapes/VideoChatShapeUtil"
|
|
||||||
import { ChatBoxTool } from "./tools/ChatBoxTool"
|
|
||||||
import { VideoChatTool } from "./tools/VideoChatTool"
|
|
||||||
import { EmbedTool } from "./tools/EmbedTool"
|
|
||||||
import { EmbedShape } from "./shapes/EmbedShapeUtil"
|
|
||||||
import { MycrozineTemplateTool } from './tools/MycrozineTemplateTool'
|
|
||||||
import { MycrozineTemplateShape } from './shapes/MycrozineTemplateShapeUtil'
|
|
||||||
import { MarkdownShape } from "./shapes/MarkdownShapeUtil"
|
|
||||||
import { MarkdownTool } from "./tools/MarkdownTool"
|
|
||||||
import { createRoot } from "react-dom/client"
|
import { createRoot } from "react-dom/client"
|
||||||
import { handleInitialPageLoad } from "./utils/handleInitialPageLoad"
|
|
||||||
import { DailyProvider } from "@daily-co/daily-react"
|
import { DailyProvider } from "@daily-co/daily-react"
|
||||||
import Daily from "@daily-co/daily-js"
|
import Daily from "@daily-co/daily-js"
|
||||||
|
|
||||||
|
|
||||||
inject()
|
inject()
|
||||||
|
|
||||||
const customShapeUtils = [
|
|
||||||
ChatBoxShape,
|
|
||||||
VideoChatShape,
|
|
||||||
EmbedShape,
|
|
||||||
MycrozineTemplateShape,
|
|
||||||
MarkdownShape,
|
|
||||||
]
|
|
||||||
const customTools = [
|
|
||||||
ChatBoxTool,
|
|
||||||
VideoChatTool,
|
|
||||||
EmbedTool,
|
|
||||||
// MycrozineTemplateTool,
|
|
||||||
// MarkdownTool
|
|
||||||
]
|
|
||||||
|
|
||||||
const callObject = Daily.createCallObject()
|
const callObject = Daily.createCallObject()
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
export class DeltaTime {
|
||||||
|
private static lastTime = Date.now()
|
||||||
|
private static initialized = false
|
||||||
|
private static _dt = 0
|
||||||
|
|
||||||
|
static get dt(): number {
|
||||||
|
if (!DeltaTime.initialized) {
|
||||||
|
DeltaTime.lastTime = Date.now()
|
||||||
|
DeltaTime.initialized = true
|
||||||
|
window.requestAnimationFrame(DeltaTime.tick)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const clamp = (min: number, max: number, value: number) => Math.min(max, Math.max(min, value))
|
||||||
|
return clamp(0, 100, DeltaTime._dt)
|
||||||
|
}
|
||||||
|
|
||||||
|
static tick(nowish: number) {
|
||||||
|
DeltaTime._dt = nowish - DeltaTime.lastTime
|
||||||
|
DeltaTime.lastTime = nowish
|
||||||
|
|
||||||
|
window.requestAnimationFrame(DeltaTime.tick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
import { SpatialIndex } from "@/propagators/SpatialIndex"
|
||||||
|
import { Box, Editor, TLShape, TLShapeId, VecLike, polygonsIntersect } from "tldraw"
|
||||||
|
|
||||||
|
export class Geo {
|
||||||
|
editor: Editor
|
||||||
|
spatialIndex: SpatialIndex
|
||||||
|
constructor(editor: Editor) {
|
||||||
|
this.editor = editor
|
||||||
|
this.spatialIndex = new SpatialIndex(editor)
|
||||||
|
}
|
||||||
|
intersects(shape: TLShape | TLShapeId): boolean {
|
||||||
|
const id = typeof shape === 'string' ? shape : shape?.id ?? null
|
||||||
|
if (!id) return false
|
||||||
|
const sourceTransform = this.editor.getShapePageTransform(id)
|
||||||
|
const sourceGeo = this.editor.getShapeGeometry(id)
|
||||||
|
const sourcePagespace = sourceTransform.applyToPoints(sourceGeo.vertices)
|
||||||
|
const sourceBounds = this.editor.getShapePageBounds(id)
|
||||||
|
|
||||||
|
const shapesInBounds = this.spatialIndex.getShapeIdsInsideBounds(sourceBounds as Box)
|
||||||
|
for (const boundsShapeId of shapesInBounds) {
|
||||||
|
if (boundsShapeId === id) continue
|
||||||
|
const pageShape = this.editor.getShape(boundsShapeId)
|
||||||
|
if (!pageShape) continue
|
||||||
|
if (pageShape.type === 'arrow') continue
|
||||||
|
const pageShapeGeo = this.editor.getShapeGeometry(pageShape)
|
||||||
|
const pageShapeTransform = this.editor.getShapePageTransform(pageShape)
|
||||||
|
const pageShapePagespace = pageShapeTransform.applyToPoints(pageShapeGeo.vertices)
|
||||||
|
const pageShapeBounds = this.editor.getShapePageBounds(boundsShapeId)
|
||||||
|
if (polygonsIntersect(sourcePagespace, pageShapePagespace) || sourceBounds?.contains(pageShapeBounds as Box) || pageShapeBounds?.contains(sourceBounds as Box)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
distance(a: TLShape | TLShapeId, b: TLShape | TLShapeId): VecLike {
|
||||||
|
const idA = typeof a === 'string' ? a : a?.id ?? null
|
||||||
|
const idB = typeof b === 'string' ? b : b?.id ?? null
|
||||||
|
if (!idA || !idB) return { x: 0, y: 0 }
|
||||||
|
const shapeA = this.editor.getShape(idA)
|
||||||
|
const shapeB = this.editor.getShape(idB)
|
||||||
|
if (!shapeA || !shapeB) return { x: 0, y: 0 }
|
||||||
|
return { x: shapeA.x - shapeB.x, y: shapeA.y - shapeB.y }
|
||||||
|
}
|
||||||
|
distanceCenter(a: TLShape | TLShapeId, b: TLShape | TLShapeId): VecLike {
|
||||||
|
const idA = typeof a === 'string' ? a : a?.id ?? null
|
||||||
|
const idB = typeof b === 'string' ? b : b?.id ?? null
|
||||||
|
if (!idA || !idB) return { x: 0, y: 0 }
|
||||||
|
const aBounds = this.editor.getShapePageBounds(idA)
|
||||||
|
const bBounds = this.editor.getShapePageBounds(idB)
|
||||||
|
if (!aBounds || !bBounds) return { x: 0, y: 0 }
|
||||||
|
const aCenter = aBounds.center
|
||||||
|
const bCenter = bBounds.center
|
||||||
|
return { x: aCenter.x - bCenter.x, y: aCenter.y - bCenter.y }
|
||||||
|
}
|
||||||
|
getIntersects(shape: TLShape | TLShapeId): TLShape[] {
|
||||||
|
const id = typeof shape === 'string' ? shape : shape?.id ?? null
|
||||||
|
if (!id) return []
|
||||||
|
const sourceTransform = this.editor.getShapePageTransform(id)
|
||||||
|
const sourceGeo = this.editor.getShapeGeometry(id)
|
||||||
|
const sourcePagespace = sourceTransform.applyToPoints(sourceGeo.vertices)
|
||||||
|
const sourceBounds = this.editor.getShapePageBounds(id)
|
||||||
|
|
||||||
|
const boundsShapes = this.spatialIndex.getShapeIdsInsideBounds(sourceBounds as Box)
|
||||||
|
const overlaps: TLShape[] = []
|
||||||
|
for (const boundsShapeId of boundsShapes) {
|
||||||
|
if (boundsShapeId === id) continue
|
||||||
|
const pageShape = this.editor.getShape(boundsShapeId)
|
||||||
|
if (!pageShape) continue
|
||||||
|
if (pageShape.type === 'arrow') continue
|
||||||
|
const pageShapeGeo = this.editor.getShapeGeometry(pageShape)
|
||||||
|
const pageShapeTransform = this.editor.getShapePageTransform(pageShape)
|
||||||
|
const pageShapePagespace = pageShapeTransform.applyToPoints(pageShapeGeo.vertices)
|
||||||
|
const pageShapeBounds = this.editor.getShapePageBounds(boundsShapeId)
|
||||||
|
if (polygonsIntersect(sourcePagespace, pageShapePagespace) || sourceBounds?.contains(pageShapeBounds as Box) || pageShapeBounds?.contains(sourceBounds as Box )) {
|
||||||
|
overlaps.push(pageShape)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return overlaps
|
||||||
|
}
|
||||||
|
|
||||||
|
contains(shape: TLShape | TLShapeId): boolean {
|
||||||
|
const id = typeof shape === 'string' ? shape : shape?.id ?? null
|
||||||
|
if (!id) return false
|
||||||
|
const sourceBounds = this.editor.getShapePageBounds(id)
|
||||||
|
|
||||||
|
const boundsShapes = this.spatialIndex.getShapeIdsInsideBounds(sourceBounds as Box)
|
||||||
|
for (const boundsShapeId of boundsShapes) {
|
||||||
|
if (boundsShapeId === id) continue
|
||||||
|
const pageShape = this.editor.getShape(boundsShapeId)
|
||||||
|
if (!pageShape) continue
|
||||||
|
if (pageShape.type !== 'geo') continue
|
||||||
|
const pageShapeBounds = this.editor.getShapePageBounds(boundsShapeId)
|
||||||
|
if (sourceBounds?.contains(pageShapeBounds as Box)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
getContains(shape: TLShape | TLShapeId): TLShape[] {
|
||||||
|
const id = typeof shape === 'string' ? shape : shape?.id ?? null
|
||||||
|
if (!id) return []
|
||||||
|
const sourceBounds = this.editor.getShapePageBounds(id)
|
||||||
|
|
||||||
|
const boundsShapes = this.spatialIndex.getShapeIdsInsideBounds(sourceBounds as Box)
|
||||||
|
const contains: TLShape[] = []
|
||||||
|
for (const boundsShapeId of boundsShapes) {
|
||||||
|
if (boundsShapeId === id) continue
|
||||||
|
const pageShape = this.editor.getShape(boundsShapeId)
|
||||||
|
if (!pageShape) continue
|
||||||
|
if (pageShape.type !== 'geo') continue
|
||||||
|
const pageShapeBounds = this.editor.getShapePageBounds(boundsShapeId)
|
||||||
|
if (sourceBounds?.contains(pageShapeBounds as Box)) {
|
||||||
|
contains.push(pageShape)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return contains
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,300 @@
|
||||||
|
import { DeltaTime } from "@/propagators/DeltaTime"
|
||||||
|
import { Geo } from "@/propagators/Geo"
|
||||||
|
import { Edge, getArrowsFromShape, getEdge } from "@/propagators/tlgraph"
|
||||||
|
import { isShapeOfType, updateProps } from "@/propagators/utils"
|
||||||
|
import { Editor, TLArrowShape, TLBinding, TLGroupShape, TLShape, TLShapeId } from "tldraw"
|
||||||
|
|
||||||
|
type Prefix = 'click' | 'tick' | 'geo' | ''
|
||||||
|
|
||||||
|
export function registerDefaultPropagators(editor: Editor) {
|
||||||
|
registerPropagators(editor, [
|
||||||
|
ChangePropagator,
|
||||||
|
ClickPropagator,
|
||||||
|
TickPropagator,
|
||||||
|
SpatialPropagator,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPropagatorOfType(arrow: TLShape, prefix: Prefix) {
|
||||||
|
if (!isShapeOfType<TLArrowShape>(arrow, 'arrow')) return false
|
||||||
|
const regex = new RegExp(`^\\s*${prefix}\\s*\\{`)
|
||||||
|
return regex.test(arrow.props.text)
|
||||||
|
}
|
||||||
|
function isExpandedPropagatorOfType(arrow: TLShape, prefix: Prefix) {
|
||||||
|
if (!isShapeOfType<TLArrowShape>(arrow, 'arrow')) return false
|
||||||
|
const regex = new RegExp(`^\\s*${prefix}\\s*\\(\\)\\s*\\{`)
|
||||||
|
return regex.test(arrow.props.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArrowFunctionCache {
|
||||||
|
private cache: Map<string, Function | null> = new Map<string, Function | null>()
|
||||||
|
|
||||||
|
/** returns undefined if the function could not be found or created */
|
||||||
|
get(editor: Editor, edge: Edge, prefix: Prefix): Function | undefined {
|
||||||
|
if (this.cache.has(edge.arrowId)) {
|
||||||
|
return this.cache.get(edge.arrowId) as Function | undefined
|
||||||
|
}
|
||||||
|
return this.set(editor, edge, prefix)
|
||||||
|
}
|
||||||
|
/** returns undefined if the function could not be created */
|
||||||
|
set(editor: Editor, edge: Edge, prefix: Prefix): Function | undefined {
|
||||||
|
try {
|
||||||
|
const arrowShape = editor.getShape(edge.arrowId)
|
||||||
|
if (!arrowShape) throw new Error('Arrow shape not found')
|
||||||
|
const textWithoutPrefix = edge.text?.replace(prefix, '')
|
||||||
|
const isExpanded = isExpandedPropagatorOfType(arrowShape, prefix)
|
||||||
|
const body = isExpanded ? textWithoutPrefix?.trim().replace(/^\s*\(\)\s*{|}$/g, '') : `
|
||||||
|
const mapping = ${textWithoutPrefix}
|
||||||
|
editor.updateShape(_unpack({...to, ...mapping}))
|
||||||
|
`
|
||||||
|
const func = new Function('editor', 'from', 'to', 'G', 'bounds', 'dt', '_unpack', body as string);
|
||||||
|
this.cache.set(edge.arrowId, func)
|
||||||
|
return func
|
||||||
|
} catch (error) {
|
||||||
|
this.cache.set(edge.arrowId, null)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(edge: Edge): void {
|
||||||
|
this.cache.delete(edge.arrowId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const packShape = (shape: TLShape) => {
|
||||||
|
return {
|
||||||
|
id: shape.id,
|
||||||
|
type: shape.type,
|
||||||
|
x: shape.x,
|
||||||
|
y: shape.y,
|
||||||
|
rotation: shape.rotation,
|
||||||
|
...shape.props,
|
||||||
|
m: shape.meta,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unpackShape = (shape: any) => {
|
||||||
|
const { id, type, x, y, rotation, m, ...props } = shape
|
||||||
|
const cast = (prop: any, constructor: (value: any) => any) => {
|
||||||
|
return prop !== undefined ? constructor(prop) : undefined;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
x: Number(x),
|
||||||
|
y: Number(y),
|
||||||
|
rotation: Number(rotation),
|
||||||
|
props: {
|
||||||
|
...props,
|
||||||
|
text: cast(props.text, String),
|
||||||
|
},
|
||||||
|
meta: m,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setArrowColor(editor: Editor, arrow: TLArrowShape, color: TLArrowShape['props']['color']): void {
|
||||||
|
editor.updateShape({
|
||||||
|
...arrow,
|
||||||
|
props: {
|
||||||
|
...arrow.props,
|
||||||
|
color: color,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerPropagators(editor: Editor, propagators: (new (editor: Editor) => Propagator)[]) {
|
||||||
|
const _propagators = propagators.map((PropagatorClass) => new PropagatorClass(editor))
|
||||||
|
|
||||||
|
for (const prop of _propagators) {
|
||||||
|
for (const shape of editor.getCurrentPageShapes()) {
|
||||||
|
if (isShapeOfType<TLArrowShape>(shape, 'arrow')) {
|
||||||
|
prop.onArrowChange(editor, shape)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editor.sideEffects.registerAfterChangeHandler<"shape">("shape", (_, next) => {
|
||||||
|
if (isShapeOfType<TLGroupShape>(next, 'group')) {
|
||||||
|
const childIds = editor.getSortedChildIdsForParent(next.id)
|
||||||
|
for (const childId of childIds) {
|
||||||
|
const child = editor.getShape(childId)
|
||||||
|
prop.afterChangeHandler?.(editor, child as TLShape)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prop.afterChangeHandler?.(editor, next)
|
||||||
|
if (isShapeOfType<TLArrowShape>(next, 'arrow')) {
|
||||||
|
prop.onArrowChange(editor, next)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function updateOnBindingChange(editor: Editor, binding: TLBinding) {
|
||||||
|
if (binding.type !== 'arrow') return
|
||||||
|
const arrow = editor.getShape(binding.fromId)
|
||||||
|
if (!arrow) return
|
||||||
|
if (!isShapeOfType<TLArrowShape>(arrow, 'arrow')) return
|
||||||
|
prop.onArrowChange(editor, arrow)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: remove this when binding creation
|
||||||
|
editor.sideEffects.registerAfterCreateHandler<"binding">("binding", (binding) => {
|
||||||
|
updateOnBindingChange(editor, binding)
|
||||||
|
})
|
||||||
|
// TODO: remove this when binding creation
|
||||||
|
editor.sideEffects.registerAfterDeleteHandler<"binding">("binding", (binding) => {
|
||||||
|
updateOnBindingChange(editor, binding)
|
||||||
|
})
|
||||||
|
|
||||||
|
editor.on('event', (event) => {
|
||||||
|
prop.eventHandler?.(event)
|
||||||
|
})
|
||||||
|
editor.on('tick', () => {
|
||||||
|
prop.tickHandler?.()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: separate generic propagator setup from scope registration
|
||||||
|
// TODO: handle cycles
|
||||||
|
export abstract class Propagator {
|
||||||
|
abstract prefix: Prefix
|
||||||
|
protected listenerArrows: Set<TLShapeId> = new Set<TLShapeId>()
|
||||||
|
protected listenerShapes: Set<TLShapeId> = new Set<TLShapeId>()
|
||||||
|
protected arrowFunctionCache: ArrowFunctionCache = new ArrowFunctionCache()
|
||||||
|
protected editor: Editor
|
||||||
|
protected geo: Geo
|
||||||
|
protected validateOnArrowChange: boolean = false
|
||||||
|
|
||||||
|
constructor(editor: Editor) {
|
||||||
|
this.editor = editor
|
||||||
|
this.geo = new Geo(editor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** function to check if any listeners need to be added/removed
|
||||||
|
* called on mount and when an arrow changes
|
||||||
|
*/
|
||||||
|
onArrowChange(editor: Editor, arrow: TLArrowShape): void {
|
||||||
|
const edge = getEdge(arrow, editor)
|
||||||
|
if (!edge) return
|
||||||
|
|
||||||
|
const isPropagator = isPropagatorOfType(arrow, this.prefix) || isExpandedPropagatorOfType(arrow, this.prefix)
|
||||||
|
|
||||||
|
if (isPropagator) {
|
||||||
|
if (this.validateOnArrowChange && !this.propagate(editor, arrow.id)) {
|
||||||
|
this.removeListener(arrow.id, edge)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.addListener(arrow.id, edge)
|
||||||
|
|
||||||
|
// TODO: find a way to do this properly so we can run arrow funcs on change without chaos...
|
||||||
|
// this.arrowFunc(editor, arrow.id)
|
||||||
|
} else {
|
||||||
|
this.removeListener(arrow.id, edge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private addListener(arrowId: TLShapeId, edge: Edge): void {
|
||||||
|
this.listenerArrows.add(arrowId)
|
||||||
|
this.listenerShapes.add(edge.from)
|
||||||
|
this.listenerShapes.add(edge.to)
|
||||||
|
this.arrowFunctionCache.set(this.editor, edge, this.prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeListener(arrowId: TLShapeId, edge: Edge): void {
|
||||||
|
this.listenerArrows.delete(arrowId)
|
||||||
|
this.arrowFunctionCache.delete(edge)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** the function to be called when side effect / event is triggered */
|
||||||
|
propagate(editor: Editor, arrow: TLShapeId): boolean {
|
||||||
|
const edge = getEdge(editor.getShape(arrow), editor)
|
||||||
|
if (!edge) return false
|
||||||
|
|
||||||
|
const arrowShape = editor.getShape(arrow) as TLArrowShape
|
||||||
|
const fromShape = editor.getShape(edge.from)
|
||||||
|
const toShape = editor.getShape(edge.to)
|
||||||
|
const fromShapePacked = packShape(fromShape as TLShape)
|
||||||
|
const toShapePacked = packShape(toShape as TLShape)
|
||||||
|
const bounds = (shape: TLShape) => editor.getShapePageBounds(shape.id)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const func = this.arrowFunctionCache.get(editor, edge, this.prefix)
|
||||||
|
const result = func?.(editor, fromShapePacked, toShapePacked, this.geo, bounds, DeltaTime.dt, unpackShape);
|
||||||
|
if (result) {
|
||||||
|
editor.updateShape(unpackShape({ ...toShapePacked, ...result }))
|
||||||
|
}
|
||||||
|
|
||||||
|
setArrowColor(editor, arrowShape, 'black')
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
setArrowColor(editor, arrowShape, 'orange')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** called after every shape change */
|
||||||
|
afterChangeHandler?(editor: Editor, next: TLShape): void
|
||||||
|
/** called on every editor event */
|
||||||
|
eventHandler?(event: any): void
|
||||||
|
/** called every tick */
|
||||||
|
tickHandler?(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ClickPropagator extends Propagator {
|
||||||
|
prefix: Prefix = 'click'
|
||||||
|
|
||||||
|
eventHandler(event: any): void {
|
||||||
|
if (event.type !== 'pointer' || event.name !== 'pointer_down') return;
|
||||||
|
const shapeAtPoint = this.editor.getShapeAtPoint(this.editor.inputs.currentPagePoint, { filter: (shape) => shape.type === 'geo' });
|
||||||
|
if (!shapeAtPoint) return
|
||||||
|
if (!this.listenerShapes.has(shapeAtPoint.id)) return
|
||||||
|
const edgesFromHovered = getArrowsFromShape(this.editor, shapeAtPoint.id)
|
||||||
|
|
||||||
|
const visited = new Set<TLShapeId>()
|
||||||
|
for (const edge of edgesFromHovered) {
|
||||||
|
if (this.listenerArrows.has(edge) && !visited.has(edge)) {
|
||||||
|
this.propagate(this.editor, edge)
|
||||||
|
visited.add(edge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChangePropagator extends Propagator {
|
||||||
|
prefix: Prefix = ''
|
||||||
|
|
||||||
|
afterChangeHandler(editor: Editor, next: TLShape): void {
|
||||||
|
if (this.listenerShapes.has(next.id)) {
|
||||||
|
const arrowsFromShape = getArrowsFromShape(editor, next.id)
|
||||||
|
for (const arrow of arrowsFromShape) {
|
||||||
|
if (this.listenerArrows.has(arrow)) {
|
||||||
|
const bindings = editor.getBindingsInvolvingShape(arrow)
|
||||||
|
if (bindings.length !== 2) continue
|
||||||
|
// don't run func if its pointing to itself to avoid change-induced recursion error
|
||||||
|
if (bindings[0].toId === bindings[1].toId) continue
|
||||||
|
this.propagate(editor, arrow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TickPropagator extends Propagator {
|
||||||
|
prefix: Prefix = 'tick'
|
||||||
|
validateOnArrowChange = true
|
||||||
|
|
||||||
|
tickHandler(): void {
|
||||||
|
for (const arrow of this.listenerArrows) {
|
||||||
|
this.propagate(this.editor, arrow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SpatialPropagator extends Propagator {
|
||||||
|
prefix: Prefix = 'geo'
|
||||||
|
|
||||||
|
// TODO: make this smarter, and scale sublinearly
|
||||||
|
afterChangeHandler(editor: Editor, next: TLShape): void {
|
||||||
|
if (next.type === 'arrow') return
|
||||||
|
for (const arrowId of this.listenerArrows) {
|
||||||
|
this.propagate(editor, arrowId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
import { RESET_VALUE, computed, isUninitialized } from '@tldraw/state'
|
||||||
|
import { TLPageId, TLShapeId, isShape, isShapeId } from '@tldraw/tlschema'
|
||||||
|
import RBush from 'rbush'
|
||||||
|
import { Box, Editor } from 'tldraw'
|
||||||
|
|
||||||
|
type Element = {
|
||||||
|
minX: number
|
||||||
|
minY: number
|
||||||
|
maxX: number
|
||||||
|
maxY: number
|
||||||
|
id: TLShapeId
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SpatialIndex {
|
||||||
|
private readonly spatialIndex: ReturnType<typeof this.createSpatialIndex>
|
||||||
|
private lastPageId: TLPageId | null = null
|
||||||
|
private shapesInTree: Map<TLShapeId, Element>
|
||||||
|
private rBush: RBush<Element>
|
||||||
|
|
||||||
|
constructor(private editor: Editor) {
|
||||||
|
this.spatialIndex = this.createSpatialIndex()
|
||||||
|
this.shapesInTree = new Map<TLShapeId, Element>()
|
||||||
|
this.rBush = new RBush<Element>()
|
||||||
|
}
|
||||||
|
|
||||||
|
private addElement(id: TLShapeId, a: Element[], existingBounds?: Box) {
|
||||||
|
const e = this.getElement(id, existingBounds)
|
||||||
|
if (!e) return
|
||||||
|
a.push(e)
|
||||||
|
this.shapesInTree.set(id, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getElement(id: TLShapeId, existingBounds?: Box): Element | null {
|
||||||
|
const bounds = existingBounds ?? this.editor.getShapeMaskedPageBounds(id)
|
||||||
|
if (!bounds) return null
|
||||||
|
return {
|
||||||
|
minX: bounds.minX,
|
||||||
|
minY: bounds.minY,
|
||||||
|
maxX: bounds.maxX,
|
||||||
|
maxY: bounds.maxY,
|
||||||
|
id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fromScratch(lastComputedEpoch: number) {
|
||||||
|
this.lastPageId = this.editor.getCurrentPageId()
|
||||||
|
this.shapesInTree = new Map<TLShapeId, Element>()
|
||||||
|
const elementsToAdd: Element[] = []
|
||||||
|
|
||||||
|
this.editor.getCurrentPageShapeIds().forEach((id) => {
|
||||||
|
this.addElement(id, elementsToAdd)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.rBush = new RBush<Element>().load(elementsToAdd)
|
||||||
|
|
||||||
|
return lastComputedEpoch
|
||||||
|
}
|
||||||
|
|
||||||
|
private createSpatialIndex() {
|
||||||
|
const shapeHistory = this.editor.store.query.filterHistory('shape')
|
||||||
|
|
||||||
|
return computed<number>('spatialIndex', (prevValue, lastComputedEpoch) => {
|
||||||
|
if (isUninitialized(prevValue)) {
|
||||||
|
return this.fromScratch(lastComputedEpoch)
|
||||||
|
}
|
||||||
|
|
||||||
|
const diff = shapeHistory.getDiffSince(lastComputedEpoch)
|
||||||
|
if (diff === RESET_VALUE) {
|
||||||
|
return this.fromScratch(lastComputedEpoch)
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPageId = this.editor.getCurrentPageId()
|
||||||
|
if (!this.lastPageId || this.lastPageId !== currentPageId) {
|
||||||
|
return this.fromScratch(lastComputedEpoch)
|
||||||
|
}
|
||||||
|
|
||||||
|
let isDirty = false
|
||||||
|
for (const changes of diff) {
|
||||||
|
const elementsToAdd: Element[] = []
|
||||||
|
for (const record of Object.values(changes.added)) {
|
||||||
|
if (isShape(record)) {
|
||||||
|
this.addElement(record.id, elementsToAdd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [_from, to] of Object.values(changes.updated)) {
|
||||||
|
if (isShape(to)) {
|
||||||
|
const currentElement = this.shapesInTree.get(to.id)
|
||||||
|
const newBounds = this.editor.getShapeMaskedPageBounds(to.id)
|
||||||
|
if (currentElement) {
|
||||||
|
if (
|
||||||
|
newBounds?.minX === currentElement.minX &&
|
||||||
|
newBounds.minY === currentElement.minY &&
|
||||||
|
newBounds.maxX === currentElement.maxX &&
|
||||||
|
newBounds.maxY === currentElement.maxY
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
this.shapesInTree.delete(to.id)
|
||||||
|
this.rBush.remove(currentElement)
|
||||||
|
isDirty = true
|
||||||
|
}
|
||||||
|
this.addElement(to.id, elementsToAdd, newBounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (elementsToAdd.length) {
|
||||||
|
this.rBush.load(elementsToAdd)
|
||||||
|
isDirty = true
|
||||||
|
}
|
||||||
|
for (const id of Object.keys(changes.removed)) {
|
||||||
|
if (isShapeId(id)) {
|
||||||
|
const currentElement = this.shapesInTree.get(id)
|
||||||
|
if (currentElement) {
|
||||||
|
this.shapesInTree.delete(id)
|
||||||
|
this.rBush.remove(currentElement)
|
||||||
|
isDirty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDirty ? lastComputedEpoch : prevValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getVisibleShapes() {
|
||||||
|
return computed<Set<TLShapeId>>('visible shapes', (prevValue) => {
|
||||||
|
// Make sure the spatial index is up to date
|
||||||
|
const _index = this.spatialIndex.get()
|
||||||
|
const newValue = this.rBush.search(this.editor.getViewportPageBounds()).map((s: Element) => s.id)
|
||||||
|
if (isUninitialized(prevValue)) {
|
||||||
|
return new Set(newValue)
|
||||||
|
}
|
||||||
|
const isSame = prevValue.size === newValue.length && newValue.every((id: TLShapeId) => prevValue.has(id))
|
||||||
|
return isSame ? prevValue : new Set(newValue)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getVisibleShapes() {
|
||||||
|
return this._getVisibleShapes().get()
|
||||||
|
}
|
||||||
|
|
||||||
|
_getNotVisibleShapes() {
|
||||||
|
return computed<Set<TLShapeId>>('not visible shapes', (prevValue) => {
|
||||||
|
const visibleShapes = this._getVisibleShapes().get()
|
||||||
|
const pageShapes = this.editor.getCurrentPageShapeIds()
|
||||||
|
const nonVisibleShapes = [...pageShapes].filter((id) => !visibleShapes.has(id))
|
||||||
|
if (isUninitialized(prevValue)) return new Set(nonVisibleShapes)
|
||||||
|
const isSame =
|
||||||
|
prevValue.size === nonVisibleShapes.length &&
|
||||||
|
nonVisibleShapes.every((id) => prevValue.has(id))
|
||||||
|
return isSame ? prevValue : new Set(nonVisibleShapes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getNotVisibleShapes() {
|
||||||
|
return this._getNotVisibleShapes().get()
|
||||||
|
}
|
||||||
|
|
||||||
|
getShapeIdsInsideBounds(bounds: Box) {
|
||||||
|
// Make sure the spatial index is up to date
|
||||||
|
const _index = this.spatialIndex.get()
|
||||||
|
return this.rBush.search(bounds).map((s: Element) => s.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { isShapeOfType } from "@/propagators/utils";
|
||||||
|
import { Editor, TLArrowBinding, TLArrowShape, TLShape, TLShapeId } from "tldraw";
|
||||||
|
|
||||||
|
export interface Edge {
|
||||||
|
arrowId: TLShapeId
|
||||||
|
from: TLShapeId
|
||||||
|
to: TLShapeId
|
||||||
|
text?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Graph {
|
||||||
|
nodes: TLShapeId[]
|
||||||
|
edges: Edge[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEdge(shape: TLShape | undefined, editor: Editor): Edge | undefined {
|
||||||
|
if (!shape || !isShapeOfType<TLArrowShape>(shape, 'arrow')) return undefined
|
||||||
|
const bindings = editor.getBindingsInvolvingShape<TLArrowBinding>(shape.id)
|
||||||
|
if (!bindings || bindings.length !== 2) return undefined
|
||||||
|
if (bindings[0].props.terminal === "end") {
|
||||||
|
return {
|
||||||
|
arrowId: shape.id,
|
||||||
|
from: bindings[1].toId,
|
||||||
|
to: bindings[0].toId,
|
||||||
|
text: shape.props.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
arrowId: shape.id,
|
||||||
|
from: bindings[0].toId,
|
||||||
|
to: bindings[1].toId,
|
||||||
|
text: shape.props.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the graph(s) of edges and nodes from a list of shapes
|
||||||
|
*/
|
||||||
|
export function getGraph(shapes: TLShape[], editor: Editor): Graph {
|
||||||
|
const nodes: Set<TLShapeId> = new Set<TLShapeId>()
|
||||||
|
const edges: Edge[] = []
|
||||||
|
|
||||||
|
for (const shape of shapes) {
|
||||||
|
const edge = getEdge(shape, editor)
|
||||||
|
if (edge) {
|
||||||
|
edges.push({
|
||||||
|
arrowId: edge.arrowId,
|
||||||
|
from: edge.from,
|
||||||
|
to: edge.to,
|
||||||
|
text: edge.text
|
||||||
|
})
|
||||||
|
nodes.add(edge.from)
|
||||||
|
nodes.add(edge.to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { nodes: Array.from(nodes), edges }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the start and end nodes of a topologically sorted graph
|
||||||
|
*/
|
||||||
|
export function sortGraph(graph: Graph): { startNodes: TLShapeId[], endNodes: TLShapeId[] } {
|
||||||
|
const targetNodes = new Set<TLShapeId>(graph.edges.map(e => e.to));
|
||||||
|
const sourceNodes = new Set<TLShapeId>(graph.edges.map(e => e.from));
|
||||||
|
|
||||||
|
const startNodes = [];
|
||||||
|
const endNodes = [];
|
||||||
|
|
||||||
|
for (const node of graph.nodes) {
|
||||||
|
if (sourceNodes.has(node) && !targetNodes.has(node)) {
|
||||||
|
startNodes.push(node);
|
||||||
|
} else if (targetNodes.has(node) && !sourceNodes.has(node)) {
|
||||||
|
endNodes.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { startNodes, endNodes };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the arrows starting from the given shape
|
||||||
|
*/
|
||||||
|
export function getArrowsFromShape(editor: Editor, shapeId: TLShapeId): TLShapeId[] {
|
||||||
|
const bindings = editor.getBindingsToShape<TLArrowBinding>(shapeId, 'arrow')
|
||||||
|
return bindings.filter(edge => edge.props.terminal === 'start').map(edge => edge.fromId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the arrows ending at the given shape
|
||||||
|
*/
|
||||||
|
export function getArrowsToShape(editor: Editor, shapeId: TLShapeId): TLShapeId[] {
|
||||||
|
const bindings = editor.getBindingsToShape<TLArrowBinding>(shapeId, 'arrow')
|
||||||
|
return bindings.filter(edge => edge.props.terminal === 'end').map(edge => edge.fromId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the arrows which share the same start shape as the given arrow
|
||||||
|
*/
|
||||||
|
export function getSiblingArrowIds(editor: Editor, arrow: TLShape): TLShapeId[] {
|
||||||
|
if (arrow.type !== 'arrow') return [];
|
||||||
|
|
||||||
|
const bindings = editor.getBindingsInvolvingShape<TLArrowBinding>(arrow.id);
|
||||||
|
if (!bindings || bindings.length !== 2) return [];
|
||||||
|
|
||||||
|
const startShapeId = bindings.find(binding => binding.props.terminal === 'start')?.toId;
|
||||||
|
if (!startShapeId) return [];
|
||||||
|
|
||||||
|
const siblingBindings = editor.getBindingsToShape<TLArrowBinding>(startShapeId, 'arrow');
|
||||||
|
const siblingArrows = siblingBindings
|
||||||
|
.filter(binding => binding.props.terminal === 'start' && binding.fromId !== arrow.id)
|
||||||
|
.map(binding => binding.fromId);
|
||||||
|
|
||||||
|
return siblingArrows;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Editor, TLShape, TLShapePartial } from "tldraw";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns true if the shape is of the given type
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* isShapeOfType<TLArrowShape>(shape, 'arrow')
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function isShapeOfType<T extends TLShape>(shape: TLShape, type: T['type']): shape is T {
|
||||||
|
return shape.type === type;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateProps<T extends TLShape>(editor: Editor, shape: T, props: Partial<T['props']>) {
|
||||||
|
editor.updateShape({
|
||||||
|
...shape,
|
||||||
|
props: {
|
||||||
|
...shape.props,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
} as TLShapePartial)
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ import { unfurlBookmarkUrl } from "../utils/unfurlBookmarkUrl"
|
||||||
import { handleInitialPageLoad } from "@/utils/handleInitialPageLoad"
|
import { handleInitialPageLoad } from "@/utils/handleInitialPageLoad"
|
||||||
import { MycrozineTemplateTool } from "@/tools/MycrozineTemplateTool"
|
import { MycrozineTemplateTool } from "@/tools/MycrozineTemplateTool"
|
||||||
import { MycrozineTemplateShape } from "@/shapes/MycrozineTemplateShapeUtil"
|
import { MycrozineTemplateShape } from "@/shapes/MycrozineTemplateShapeUtil"
|
||||||
|
import { registerPropagators, ChangePropagator, TickPropagator, ClickPropagator } from "@/propagators/ScopedPropagators"
|
||||||
|
|
||||||
// Default to production URL if env var isn't available
|
// Default to production URL if env var isn't available
|
||||||
export const WORKER_URL = "https://jeffemmett-canvas.jeffemmett.workers.dev"
|
export const WORKER_URL = "https://jeffemmett-canvas.jeffemmett.workers.dev"
|
||||||
|
|
@ -88,6 +89,7 @@ export function Board() {
|
||||||
editor.registerExternalAssetHandler("url", unfurlBookmarkUrl)
|
editor.registerExternalAssetHandler("url", unfurlBookmarkUrl)
|
||||||
editor.setCurrentTool("hand")
|
editor.setCurrentTool("hand")
|
||||||
handleInitialPageLoad(editor)
|
handleInitialPageLoad(editor)
|
||||||
|
registerPropagators(editor, [TickPropagator,ChangePropagator,ClickPropagator])
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue