d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
196 lines
6.1 KiB
TypeScript
196 lines
6.1 KiB
TypeScript
import { useCallback, useMemo } from 'react'
|
|
import { BLOCK_DIMENSIONS } from '@sim/workflow-renderer'
|
|
import type { Node, ReactFlowInstance } from 'reactflow'
|
|
|
|
interface VisibleBounds {
|
|
width: number
|
|
height: number
|
|
offsetLeft: number
|
|
offsetRight: number
|
|
offsetBottom: number
|
|
}
|
|
|
|
export interface CanvasViewportOptions {
|
|
embedded?: boolean
|
|
}
|
|
|
|
/**
|
|
* Gets the visible canvas bounds accounting for sidebar, terminal, and panel overlays.
|
|
* When embedded, uses the container rect directly since CSS variable offsets don't apply.
|
|
*/
|
|
function getVisibleCanvasBounds(options?: CanvasViewportOptions): VisibleBounds {
|
|
const flowContainer = document.querySelector('.react-flow')
|
|
|
|
if (options?.embedded && flowContainer) {
|
|
const rect = flowContainer.getBoundingClientRect()
|
|
return {
|
|
width: rect.width,
|
|
height: rect.height,
|
|
offsetLeft: 0,
|
|
offsetRight: 0,
|
|
offsetBottom: 0,
|
|
}
|
|
}
|
|
|
|
const style = getComputedStyle(document.documentElement)
|
|
|
|
const sidebarWidth = Number.parseInt(style.getPropertyValue('--sidebar-width') || '0', 10)
|
|
const terminalHeight = Number.parseInt(style.getPropertyValue('--terminal-height') || '0', 10)
|
|
const panelWidth = Number.parseInt(style.getPropertyValue('--panel-width') || '0', 10)
|
|
|
|
if (!flowContainer) {
|
|
return {
|
|
width: window.innerWidth - sidebarWidth - panelWidth,
|
|
height: window.innerHeight - terminalHeight,
|
|
offsetLeft: sidebarWidth,
|
|
offsetRight: panelWidth,
|
|
offsetBottom: terminalHeight,
|
|
}
|
|
}
|
|
|
|
const rect = flowContainer.getBoundingClientRect()
|
|
|
|
// Calculate actual visible area in screen coordinates
|
|
// This works regardless of whether the container extends under overlays
|
|
const visibleLeft = Math.max(rect.left, sidebarWidth)
|
|
const visibleRight = Math.min(rect.right, window.innerWidth - panelWidth)
|
|
const visibleBottom = Math.min(rect.bottom, window.innerHeight - terminalHeight)
|
|
|
|
// Calculate visible dimensions and offsets relative to the container
|
|
const visibleWidth = Math.max(0, visibleRight - visibleLeft)
|
|
const visibleHeight = Math.max(0, visibleBottom - rect.top)
|
|
|
|
return {
|
|
width: visibleWidth,
|
|
height: visibleHeight,
|
|
offsetLeft: visibleLeft - rect.left,
|
|
offsetRight: rect.right - visibleRight,
|
|
offsetBottom: rect.bottom - visibleBottom,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the center of the visible canvas in screen coordinates.
|
|
*/
|
|
function getVisibleCanvasCenter(options?: CanvasViewportOptions): { x: number; y: number } {
|
|
const bounds = getVisibleCanvasBounds(options)
|
|
|
|
const flowContainer = document.querySelector('.react-flow')
|
|
const rect = flowContainer?.getBoundingClientRect()
|
|
const containerLeft = rect?.left ?? 0
|
|
const containerTop = rect?.top ?? 0
|
|
|
|
return {
|
|
x: containerLeft + bounds.offsetLeft + bounds.width / 2,
|
|
y: containerTop + bounds.height / 2,
|
|
}
|
|
}
|
|
|
|
interface FitViewToBoundsOptions {
|
|
padding?: number
|
|
maxZoom?: number
|
|
minZoom?: number
|
|
duration?: number
|
|
nodes?: Node[]
|
|
}
|
|
|
|
/**
|
|
* Hook providing canvas viewport utilities that account for sidebar, panel, and terminal overlays.
|
|
*/
|
|
export function useCanvasViewport(
|
|
reactFlowInstance: ReactFlowInstance | null,
|
|
viewportOptions?: CanvasViewportOptions
|
|
) {
|
|
const embedded = viewportOptions?.embedded
|
|
const stableOptions = useMemo<CanvasViewportOptions | undefined>(
|
|
() => (embedded ? { embedded } : undefined),
|
|
[embedded]
|
|
)
|
|
|
|
/**
|
|
* Gets the center of the visible canvas in flow coordinates.
|
|
*/
|
|
const getViewportCenter = useCallback(() => {
|
|
if (!reactFlowInstance) {
|
|
return { x: 0, y: 0 }
|
|
}
|
|
|
|
const center = getVisibleCanvasCenter(stableOptions)
|
|
return reactFlowInstance.screenToFlowPosition(center)
|
|
}, [reactFlowInstance, stableOptions])
|
|
|
|
/**
|
|
* Fits the view to show all nodes within the visible canvas bounds,
|
|
* accounting for sidebar, panel, and terminal overlays.
|
|
* @param padding - Fraction of viewport to leave as margin (0.1 = 10% on each side)
|
|
*/
|
|
const fitViewToBounds = useCallback(
|
|
(options: FitViewToBoundsOptions = {}) => {
|
|
if (!reactFlowInstance) return
|
|
|
|
const {
|
|
padding = 0.1,
|
|
maxZoom = 1,
|
|
minZoom = 0.1,
|
|
duration = 300,
|
|
nodes: targetNodes,
|
|
} = options
|
|
|
|
const nodes = targetNodes ?? reactFlowInstance.getNodes()
|
|
if (nodes.length === 0) {
|
|
return
|
|
}
|
|
|
|
const bounds = getVisibleCanvasBounds(stableOptions)
|
|
|
|
// Calculate node bounds
|
|
let minX = Number.POSITIVE_INFINITY
|
|
let minY = Number.POSITIVE_INFINITY
|
|
let maxX = Number.NEGATIVE_INFINITY
|
|
let maxY = Number.NEGATIVE_INFINITY
|
|
|
|
nodes.forEach((node) => {
|
|
const nodeWidth = node.width ?? BLOCK_DIMENSIONS.FIXED_WIDTH
|
|
const nodeHeight = node.height ?? BLOCK_DIMENSIONS.MIN_HEIGHT
|
|
|
|
minX = Math.min(minX, node.position.x)
|
|
minY = Math.min(minY, node.position.y)
|
|
maxX = Math.max(maxX, node.position.x + nodeWidth)
|
|
maxY = Math.max(maxY, node.position.y + nodeHeight)
|
|
})
|
|
|
|
const contentWidth = maxX - minX
|
|
const contentHeight = maxY - minY
|
|
|
|
// Apply padding as fraction of viewport (matches ReactFlow's fitView behavior)
|
|
const availableWidth = bounds.width * (1 - padding * 2)
|
|
const availableHeight = bounds.height * (1 - padding * 2)
|
|
|
|
// Calculate zoom to fit content in available area
|
|
const zoomX = availableWidth / contentWidth
|
|
const zoomY = availableHeight / contentHeight
|
|
const zoom = Math.max(minZoom, Math.min(maxZoom, Math.min(zoomX, zoomY)))
|
|
|
|
// Calculate center of content in flow coordinates
|
|
const contentCenterX = minX + contentWidth / 2
|
|
const contentCenterY = minY + contentHeight / 2
|
|
|
|
// Calculate viewport position to center content in visible area
|
|
// Account for sidebar offset on the left
|
|
const visibleCenterX = bounds.offsetLeft + bounds.width / 2
|
|
const visibleCenterY = bounds.height / 2
|
|
|
|
const x = visibleCenterX - contentCenterX * zoom
|
|
const y = visibleCenterY - contentCenterY * zoom
|
|
|
|
reactFlowInstance.setViewport({ x, y, zoom }, { duration })
|
|
},
|
|
[reactFlowInstance, stableOptions]
|
|
)
|
|
|
|
return {
|
|
getViewportCenter,
|
|
fitViewToBounds,
|
|
}
|
|
}
|