snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled

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

View File

@@ -0,0 +1,150 @@
import { createShapeId, StateNode, TLPointerEventInfo, TLShapeId } from 'tldraw'
import { createOrUpdateConnectionBinding } from '../connection/ConnectionBindingUtil.tsx'
import { createNodeAtConnectionEnd } from '../connection/ConnectionShapeUtil.tsx'
import { getNextConnectionIndex } from '../connection/keepConnectionsAtBottom.tsx'
import { DEFAULT_NODE_SPACING_PX } from '../constants.tsx'
import { getNodePortConnections } from '../nodes/nodePorts.tsx'
import { PortId } from './Port.tsx'
// Information about which port is being pointed at
interface PointingPortInfo {
shapeId: TLShapeId
portId: PortId
terminal: 'start' | 'end'
}
// State node that handles pointing at ports to create connections
// This will be added to tldraw's state machine to customize the built-in select tool
export class PointingPort extends StateNode {
static override id = 'pointing_port'
info?: PointingPortInfo
override onEnter(info: PointingPortInfo): void {
this.info = info
}
private getExistingConnection() {
const { shapeId, portId } = this.info!
return getNodePortConnections(this.editor, shapeId).find((c) => c.ownPortId === portId)
}
override onPointerMove(info: TLPointerEventInfo): void {
// isDragging is true if the user has moved the pointer sufficiently. below this threshold,
// we treat the pointer as a click.
if (!this.editor.inputs.getIsDragging()) return
const { shapeId, portId, terminal: connectingTerminal } = this.info!
const existingConnection = this.getExistingConnection()
// If we can't have multiple connections and one already exists, move the existing
// connection by transitioning to dragging the existing connection's handle.
if (connectingTerminal !== 'start' && existingConnection) {
this.parent.transition('dragging_handle', {
...info,
target: 'handle',
shape: this.editor.getShape(existingConnection.connectionId)!,
handle: this.editor
.getShapeHandles(existingConnection.connectionId)!
.find((h) => h.id === connectingTerminal),
})
return
}
// Otherwise, create a new connection, and start dragging that connection's handle instead.
const creatingMarkId = this.editor.markHistoryStoppingPoint()
const connectionShapeId = createShapeId()
const draggingTerminal = connectingTerminal === 'start' ? 'end' : 'start'
const pagePoint = this.editor.inputs.getCurrentPagePoint()
this.editor.createShape({
type: 'connection',
id: connectionShapeId,
x: pagePoint.x,
y: pagePoint.y,
index: getNextConnectionIndex(this.editor),
props: {
start: { x: 0, y: 0 },
end: { x: 0, y: 0 },
},
})
// bind one end of the connection to the port the user pointer-down'd on.
createOrUpdateConnectionBinding(this.editor, connectionShapeId, shapeId, {
portId,
terminal: connectingTerminal,
})
// transition to dragging the other end of the connection:
const handle = this.editor
.getShapeHandles(connectionShapeId)
?.find((h) => h.id === draggingTerminal)
this.parent.transition('dragging_handle', {
...info,
target: 'handle',
shape: this.editor.getShape(connectionShapeId)!,
handle: handle!,
creatingMarkId,
isCreating: true,
})
}
override onPointerUp(info: TLPointerEventInfo): void {
// if we get a pointer up while we're still in this state, it means we haven't transitioned
// into a dragging state so we'll treat this as a click:
this.onClick()
// switch back to the idle state when we're done:
this.parent.transition('idle', info)
}
// Handle clicks on ports (without dragging)
private onClick() {
// Only handle clicks on start ports (output ports)
if (this.info?.terminal !== 'start') return
// Don't create new connections if one already exists
if (this.getExistingConnection()) return
const { shapeId, portId } = this.info
// Get the bounds of the source node
const bounds = this.editor.getShapePageBounds(shapeId)
if (!bounds) return
// Calculate position for new node to the right of the source node
const targetPositionInPageSpace = {
x: bounds.midX,
y: bounds.bottom + DEFAULT_NODE_SPACING_PX,
}
// Create a connection shape
const connectionShapeId = createShapeId()
this.editor.createShape({
type: 'connection',
id: connectionShapeId,
x: bounds.right,
y: bounds.top,
index: getNextConnectionIndex(this.editor),
})
// Bind the connection to the source port
createOrUpdateConnectionBinding(this.editor, connectionShapeId, shapeId, {
portId,
terminal: 'start',
})
// Position the connection end point where the new node will be
const targetPositionInConnectionSpace = this.editor
.getPointInShapeSpace(connectionShapeId, targetPositionInPageSpace)
.addXY(0, 200)
this.editor.updateShape({
id: connectionShapeId,
type: 'connection',
props: {
end: targetPositionInConnectionSpace.toJson(),
},
})
createNodeAtConnectionEnd(this.editor, connectionShapeId, targetPositionInPageSpace)
}
}

View File

@@ -0,0 +1,80 @@
import classNames from 'classnames'
import { T, TLShapeId, useEditor, useValue } from 'tldraw'
import { getNodePortConnections } from '../nodes/nodePorts'
import { portState } from './portState'
export type PortId = string
export const shapePort = T.object({
id: T.string,
terminal: T.literalEnum('start', 'end'),
x: T.number,
y: T.number,
})
export type ShapePort = T.TypeOf<typeof shapePort>
/**
* Port ids are unique within a shape. To identify a port we need both the shape id and the port id.
*/
export interface PortIdentifier {
shapeId: TLShapeId
portId: PortId
}
/**
* This react component renders a port.
*/
export function Port({ shapeId, port }: { shapeId: TLShapeId; port: ShapePort }) {
const editor = useEditor()
// isHinting is true if the user is currently dragging a connection to this port. it means we
// should highlight this port.
const isHinting = useValue(
'isHinting',
() => {
const { hintingPort } = portState.get(editor)
return hintingPort && hintingPort.portId === port.id && hintingPort.shapeId === shapeId
},
[editor, shapeId, port.id]
)
// isEligible is true if the the user is currently dragging a connection, and this port is one
// that the connection can be connected to.
const isEligible = useValue(
'isEligible',
() => {
const { eligiblePorts } = portState.get(editor)
if (!eligiblePorts) return false
if (eligiblePorts.terminal !== port.terminal) return false
if (eligiblePorts.excludeNodes?.has(shapeId)) return false
if (port.terminal === 'end') {
// if the port is an end port, it can only have one connection, so it's not eligible
// when there is a connection
const connections = getNodePortConnections(editor, shapeId)
return !connections.some((c) => c.ownPortId === port.id)
}
return true
},
[editor, shapeId, port.id, port.terminal]
)
return (
<div
className={classNames(
`Port`,
isHinting ? 'Port_hinting' : isEligible ? 'Port_eligible' : undefined
)}
style={{
transform: `translate(${port.x}px, ${port.y}px)`,
}}
onPointerDown={() => {
editor.setCurrentTool('select.pointing_port', {
shapeId,
portId: port.id,
terminal: port.terminal,
})
}}
/>
)
}

View File

@@ -0,0 +1,48 @@
import { Editor, Vec, VecLike } from 'tldraw'
import { getNodePortConnections, getNodePorts } from '../nodes/nodePorts'
import { ShapePort } from './Port'
export function getPortAtPoint(
editor: Editor,
point: VecLike,
opts?: { terminal?: 'start' | 'end'; margin?: number }
) {
// find the shape at that point:
const shape = editor.getShapeAtPoint(point, {
hitInside: true,
// only node shapes can have ports
filter: (shape) => editor.isShapeOfType(shape, 'node'),
...opts,
})
if (!shape || !editor.isShapeOfType(shape, 'node')) return null
// get the ports on that shape
const ports = getNodePorts(editor, shape)
// transform the ports to page space
const shapeTransform = editor.getShapePageTransform(shape)
// find the port closest to the point
let bestPort: ShapePort | null = null
let bestDistance = Infinity
for (const port of Object.values(ports)) {
if (opts?.terminal && port.terminal !== opts.terminal) continue
const portInPageSpace = shapeTransform.applyToPoint(port)
const distance = Vec.Dist(point, portInPageSpace)
if (distance < bestDistance) {
bestPort = port
bestDistance = distance
}
}
// if we didn't find a port, return null
if (!bestPort) return null
// otherwise, return the port and it's existing connections
const existingConnections = getNodePortConnections(editor, shape).filter(
(c) => c.ownPortId === bestPort.id
)
return { shape, port: bestPort, existingConnections }
}

View File

@@ -0,0 +1,24 @@
import { Editor, EditorAtom, TLShapeId } from 'tldraw'
import { PortIdentifier } from './Port'
/**
* The UI state for ports. These mostly highlight ports relevant to the user's current action.
*/
export interface PortState {
// hintingPort is the port that the user is currently dragging a connection to.
hintingPort: PortIdentifier | null
// eligiblePorts is the set of ports that the user can connect a new connection to.
eligiblePorts: {
terminal: 'start' | 'end'
excludeNodes: Set<TLShapeId> | null
} | null
}
export const portState = new EditorAtom<PortState>('port state', () => ({
hintingPort: null,
eligiblePorts: null,
}))
export function updatePortState(editor: Editor, update: Partial<PortState>) {
portState.update(editor, (state) => ({ ...state, ...update }))
}