fix: improve pinned shape zoom behavior - maintain constant visual size
- Pinned shapes now stay exactly the same size visually during zoom - Uses CSS transform scale instead of changing w/h props - Content inside shapes renders identically at all zoom levels - Stores pinnedAtZoom in shape.meta for reference - Returns to original position smoothly when unpinned - Removed size-changing logic that was causing content reflow issues The transform approach ensures text, UI elements, and all content inside pinned shapes remain pixel-perfect regardless of canvas zoom. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5d9f41c64b
commit
6e29384a79
|
|
@ -64,6 +64,8 @@ export interface StandardizedToolWrapperProps {
|
|||
onTagsChange?: (tags: string[]) => void
|
||||
/** Whether tags can be edited */
|
||||
tagsEditable?: boolean
|
||||
/** Zoom level when the shape was pinned (for constant visual size) */
|
||||
pinnedAtZoom?: number
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -90,13 +92,67 @@ export const StandardizedToolWrapper: React.FC<StandardizedToolWrapperProps> = (
|
|||
tags = [],
|
||||
onTagsChange,
|
||||
tagsEditable = true,
|
||||
pinnedAtZoom,
|
||||
}) => {
|
||||
const [isHoveringHeader, setIsHoveringHeader] = useState(false)
|
||||
const [isEditingTags, setIsEditingTags] = useState(false)
|
||||
const [editingTagInput, setEditingTagInput] = useState('')
|
||||
const [zoomScale, setZoomScale] = useState(1)
|
||||
const tagInputRef = useRef<HTMLInputElement>(null)
|
||||
const isDarkMode = useIsDarkMode()
|
||||
|
||||
// Calculate zoom compensation scale when pinned
|
||||
useEffect(() => {
|
||||
if (!isPinnedToView || !editor) {
|
||||
setZoomScale(1)
|
||||
return
|
||||
}
|
||||
|
||||
// Get pinnedAtZoom from shape meta if not passed as prop
|
||||
let originalZoom = pinnedAtZoom
|
||||
if (!originalZoom && shapeId) {
|
||||
const shape = editor.getShape(shapeId)
|
||||
originalZoom = (shape?.meta as any)?.pinnedAtZoom
|
||||
}
|
||||
|
||||
if (!originalZoom) {
|
||||
setZoomScale(1)
|
||||
return
|
||||
}
|
||||
|
||||
const updateScale = () => {
|
||||
const currentZoom = editor.getCamera().z
|
||||
// Scale inversely to zoom to maintain constant visual size
|
||||
const scale = originalZoom / currentZoom
|
||||
setZoomScale(scale)
|
||||
}
|
||||
|
||||
// Initial scale calculation
|
||||
updateScale()
|
||||
|
||||
// Listen for camera changes
|
||||
const handleChange = () => {
|
||||
updateScale()
|
||||
}
|
||||
|
||||
// Use requestAnimationFrame for smooth updates
|
||||
let rafId: number | null = null
|
||||
const throttledUpdate = () => {
|
||||
if (rafId) return
|
||||
rafId = requestAnimationFrame(() => {
|
||||
updateScale()
|
||||
rafId = null
|
||||
})
|
||||
}
|
||||
|
||||
editor.on('change' as any, throttledUpdate)
|
||||
|
||||
return () => {
|
||||
editor.off('change' as any, throttledUpdate)
|
||||
if (rafId) cancelAnimationFrame(rafId)
|
||||
}
|
||||
}, [isPinnedToView, editor, shapeId, pinnedAtZoom])
|
||||
|
||||
// Handle Esc key to exit maximize mode
|
||||
useEffect(() => {
|
||||
if (!isMaximized || !onMaximize) return
|
||||
|
|
@ -173,6 +229,11 @@ export const StandardizedToolWrapper: React.FC<StandardizedToolWrapperProps> = (
|
|||
? `${primaryColor}15` // 15% opacity
|
||||
: `${primaryColor}10` // 10% opacity
|
||||
|
||||
// Calculate transform for pinned shapes
|
||||
const pinnedTransform = isPinnedToView && zoomScale !== 1
|
||||
? `scale(${zoomScale})`
|
||||
: undefined
|
||||
|
||||
const wrapperStyle: React.CSSProperties = {
|
||||
width: typeof width === 'number' ? `${width}px` : width,
|
||||
height: isMinimized ? 40 : (typeof height === 'number' ? `${height}px` : height), // Minimized height is just the header
|
||||
|
|
@ -188,8 +249,11 @@ export const StandardizedToolWrapper: React.FC<StandardizedToolWrapperProps> = (
|
|||
fontFamily: "Inter, sans-serif",
|
||||
position: 'relative',
|
||||
pointerEvents: 'auto',
|
||||
transition: 'height 0.2s ease, box-shadow 0.2s ease',
|
||||
transition: isPinnedToView ? 'box-shadow 0.2s ease' : 'height 0.2s ease, box-shadow 0.2s ease',
|
||||
boxSizing: 'border-box',
|
||||
// Apply zoom compensation transform for pinned shapes
|
||||
transform: pinnedTransform,
|
||||
transformOrigin: 'top left',
|
||||
}
|
||||
|
||||
const headerStyle: React.CSSProperties = {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ export interface PinnedViewOptions {
|
|||
|
||||
/**
|
||||
* Hook to manage shapes pinned to the viewport.
|
||||
* When a shape is pinned, it stays in the same screen position as the camera moves.
|
||||
* When a shape is pinned, it stays in the same screen position AND visual size
|
||||
* as the camera moves and zooms. Content inside the shape remains unchanged.
|
||||
*/
|
||||
export function usePinnedToView(
|
||||
editor: Editor | null,
|
||||
|
|
@ -30,13 +31,10 @@ export function usePinnedToView(
|
|||
const { position = 'current', offsetY = 0, offsetX = 0 } = options
|
||||
const pinnedScreenPositionRef = useRef<{ x: number; y: number } | null>(null)
|
||||
const originalCoordinatesRef = useRef<{ x: number; y: number } | null>(null)
|
||||
const originalSizeRef = useRef<{ w: number; h: number } | null>(null)
|
||||
const originalZoomRef = useRef<number | null>(null)
|
||||
const wasPinnedRef = useRef<boolean>(false)
|
||||
const isUpdatingRef = useRef<boolean>(false)
|
||||
const animationFrameRef = useRef<number | null>(null)
|
||||
const lastCameraRef = useRef<{ x: number; y: number; z: number } | null>(null)
|
||||
const pendingUpdateRef = useRef<{ x: number; y: number } | null>(null)
|
||||
const lastUpdateTimeRef = useRef<number>(0)
|
||||
const driftAnimationRef = useRef<number | null>(null)
|
||||
|
||||
|
|
@ -48,19 +46,26 @@ export function usePinnedToView(
|
|||
const shape = editor.getShape(shapeId as TLShapeId)
|
||||
if (!shape) return
|
||||
|
||||
// If just became pinned (transition from false to true), capture the current screen position
|
||||
// If just became pinned (transition from false to true)
|
||||
if (isPinned && !wasPinnedRef.current) {
|
||||
// Store the original coordinates - these will be restored when unpinned
|
||||
originalCoordinatesRef.current = { x: shape.x, y: shape.y }
|
||||
|
||||
// Store the original size and zoom - needed to maintain constant visual size
|
||||
|
||||
// Store the original zoom level in shape meta for CSS transform calculation
|
||||
const currentCamera = editor.getCamera()
|
||||
originalSizeRef.current = {
|
||||
w: (shape.props as any).w || 0,
|
||||
h: (shape.props as any).h || 0
|
||||
}
|
||||
originalZoomRef.current = currentCamera.z
|
||||
|
||||
|
||||
// Store original zoom in meta so StandardizedToolWrapper can access it
|
||||
editor.updateShape({
|
||||
id: shapeId as TLShapeId,
|
||||
type: shape.type,
|
||||
meta: {
|
||||
...shape.meta,
|
||||
pinnedAtZoom: currentCamera.z,
|
||||
originalX: shape.x,
|
||||
originalY: shape.y,
|
||||
},
|
||||
})
|
||||
|
||||
// Calculate screen position based on position option
|
||||
let screenPoint: { x: number; y: number }
|
||||
const viewport = editor.getViewportScreenBounds()
|
||||
|
|
@ -68,19 +73,16 @@ export function usePinnedToView(
|
|||
const shapeHeight = (shape.props as any).h || 0
|
||||
|
||||
if (position === 'top-center') {
|
||||
// Center horizontally at the top of the viewport
|
||||
screenPoint = {
|
||||
x: viewport.x + (viewport.w / 2) - (shapeWidth * currentCamera.z / 2) + offsetX,
|
||||
y: viewport.y + offsetY,
|
||||
}
|
||||
} else if (position === 'bottom-center') {
|
||||
// Center horizontally at the bottom of the viewport
|
||||
screenPoint = {
|
||||
x: viewport.x + (viewport.w / 2) - (shapeWidth * currentCamera.z / 2) + offsetX,
|
||||
y: viewport.y + viewport.h - (shapeHeight * currentCamera.z) - offsetY,
|
||||
}
|
||||
} else if (position === 'center') {
|
||||
// Center in the viewport
|
||||
screenPoint = {
|
||||
x: viewport.x + (viewport.w / 2) - (shapeWidth * currentCamera.z / 2) + offsetX,
|
||||
y: viewport.y + (viewport.h / 2) - (shapeHeight * currentCamera.z / 2) + offsetY,
|
||||
|
|
@ -93,20 +95,16 @@ export function usePinnedToView(
|
|||
|
||||
pinnedScreenPositionRef.current = { x: screenPoint.x, y: screenPoint.y }
|
||||
lastCameraRef.current = { ...currentCamera }
|
||||
|
||||
// Bring the shape to the front using tldraw's proper index functions
|
||||
|
||||
// Bring the shape to the front
|
||||
try {
|
||||
const allShapes = editor.getCurrentPageShapes()
|
||||
|
||||
// Find the highest index among all shapes
|
||||
let highestIndex = shape.index
|
||||
for (const s of allShapes) {
|
||||
if (s.id !== shape.id && s.index > highestIndex) {
|
||||
highestIndex = s.index
|
||||
}
|
||||
}
|
||||
|
||||
// Only update if we need to move higher
|
||||
if (highestIndex > shape.index) {
|
||||
const newIndex = getIndexAbove(highestIndex)
|
||||
editor.updateShape({
|
||||
|
|
@ -127,133 +125,102 @@ export function usePinnedToView(
|
|||
cancelAnimationFrame(animationFrameRef.current)
|
||||
animationFrameRef.current = null
|
||||
}
|
||||
|
||||
// Animate back to original coordinates and size with a calm drift
|
||||
if (originalCoordinatesRef.current && originalSizeRef.current && originalZoomRef.current !== null) {
|
||||
const currentShape = editor.getShape(shapeId as TLShapeId)
|
||||
if (currentShape) {
|
||||
const startX = currentShape.x
|
||||
const startY = currentShape.y
|
||||
const targetX = originalCoordinatesRef.current.x
|
||||
const targetY = originalCoordinatesRef.current.y
|
||||
|
||||
// Return to the exact original size (not calculated based on current zoom)
|
||||
const originalW = originalSizeRef.current.w
|
||||
const originalH = originalSizeRef.current.h
|
||||
|
||||
// Use the original size directly
|
||||
const targetW = originalW
|
||||
const targetH = originalH
|
||||
|
||||
const currentW = (currentShape.props as any).w || originalW
|
||||
const currentH = (currentShape.props as any).h || originalH
|
||||
|
||||
const startW = currentW
|
||||
const startH = currentH
|
||||
|
||||
// Only animate if there's a meaningful distance to travel or size change
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(targetX - startX, 2) + Math.pow(targetY - startY, 2)
|
||||
)
|
||||
const sizeChange = Math.abs(targetW - startW) > 0.1 || Math.abs(targetH - startH) > 0.1
|
||||
|
||||
if (distance > 1 || sizeChange) {
|
||||
// Animation parameters
|
||||
const duration = 600 // 600ms for a calm drift
|
||||
const startTime = performance.now()
|
||||
|
||||
// Easing function: ease-out for a calm deceleration
|
||||
const easeOutCubic = (t: number): number => {
|
||||
return 1 - Math.pow(1 - t, 3)
|
||||
}
|
||||
|
||||
const animateDrift = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime
|
||||
const progress = Math.min(elapsed / duration, 1) // Clamp to 0-1
|
||||
const easedProgress = easeOutCubic(progress)
|
||||
|
||||
// Interpolate position
|
||||
const currentX = startX + (targetX - startX) * easedProgress
|
||||
const currentY = startY + (targetY - startY) * easedProgress
|
||||
|
||||
// Interpolate size
|
||||
const currentW = startW + (targetW - startW) * easedProgress
|
||||
const currentH = startH + (targetH - startH) * easedProgress
|
||||
|
||||
try {
|
||||
editor.updateShape({
|
||||
id: shapeId as TLShapeId,
|
||||
type: currentShape.type,
|
||||
x: currentX,
|
||||
y: currentY,
|
||||
props: {
|
||||
...currentShape.props,
|
||||
w: currentW,
|
||||
h: currentH,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error during drift animation:', error)
|
||||
driftAnimationRef.current = null
|
||||
return
|
||||
}
|
||||
|
||||
// Continue animation if not complete
|
||||
if (progress < 1) {
|
||||
driftAnimationRef.current = requestAnimationFrame(animateDrift)
|
||||
} else {
|
||||
// Animation complete - ensure we're exactly at target
|
||||
try {
|
||||
editor.updateShape({
|
||||
id: shapeId as TLShapeId,
|
||||
type: currentShape.type,
|
||||
x: targetX,
|
||||
y: targetY,
|
||||
props: {
|
||||
...currentShape.props,
|
||||
w: targetW,
|
||||
h: targetH,
|
||||
},
|
||||
})
|
||||
console.log(`📍 Drifted back to original coordinates: (${targetX}, ${targetY}) and size: (${targetW}, ${targetH})`)
|
||||
} catch (error) {
|
||||
console.error('Error setting final position/size:', error)
|
||||
}
|
||||
driftAnimationRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
// Start the animation
|
||||
driftAnimationRef.current = requestAnimationFrame(animateDrift)
|
||||
} else {
|
||||
// Distance is too small, just set directly
|
||||
|
||||
// Get original coordinates from meta
|
||||
const currentShape = editor.getShape(shapeId as TLShapeId)
|
||||
if (currentShape) {
|
||||
const originalX = (currentShape.meta as any)?.originalX ?? originalCoordinatesRef.current?.x ?? currentShape.x
|
||||
const originalY = (currentShape.meta as any)?.originalY ?? originalCoordinatesRef.current?.y ?? currentShape.y
|
||||
|
||||
const startX = currentShape.x
|
||||
const startY = currentShape.y
|
||||
const targetX = originalX
|
||||
const targetY = originalY
|
||||
|
||||
// Calculate distance
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(targetX - startX, 2) + Math.pow(targetY - startY, 2)
|
||||
)
|
||||
|
||||
if (distance > 1) {
|
||||
// Animation parameters
|
||||
const duration = 600 // 600ms for a calm drift
|
||||
const startTime = performance.now()
|
||||
|
||||
const easeOutCubic = (t: number): number => 1 - Math.pow(1 - t, 3)
|
||||
|
||||
const animateDrift = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
const easedProgress = easeOutCubic(progress)
|
||||
|
||||
const currentX = startX + (targetX - startX) * easedProgress
|
||||
const currentY = startY + (targetY - startY) * easedProgress
|
||||
|
||||
try {
|
||||
editor.updateShape({
|
||||
id: shapeId as TLShapeId,
|
||||
type: currentShape.type,
|
||||
x: targetX,
|
||||
y: targetY,
|
||||
props: {
|
||||
...currentShape.props,
|
||||
w: targetW,
|
||||
h: targetH,
|
||||
},
|
||||
x: currentX,
|
||||
y: currentY,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error restoring original coordinates/size:', error)
|
||||
console.error('Error during drift animation:', error)
|
||||
driftAnimationRef.current = null
|
||||
return
|
||||
}
|
||||
|
||||
if (progress < 1) {
|
||||
driftAnimationRef.current = requestAnimationFrame(animateDrift)
|
||||
} else {
|
||||
// Animation complete - clear pinned meta data
|
||||
try {
|
||||
editor.updateShape({
|
||||
id: shapeId as TLShapeId,
|
||||
type: currentShape.type,
|
||||
x: targetX,
|
||||
y: targetY,
|
||||
meta: {
|
||||
...currentShape.meta,
|
||||
pinnedAtZoom: undefined,
|
||||
originalX: undefined,
|
||||
originalY: undefined,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error setting final position:', error)
|
||||
}
|
||||
driftAnimationRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
driftAnimationRef.current = requestAnimationFrame(animateDrift)
|
||||
} else {
|
||||
// Distance is too small, just set directly and clear meta
|
||||
try {
|
||||
editor.updateShape({
|
||||
id: shapeId as TLShapeId,
|
||||
type: currentShape.type,
|
||||
x: targetX,
|
||||
y: targetY,
|
||||
meta: {
|
||||
...currentShape.meta,
|
||||
pinnedAtZoom: undefined,
|
||||
originalX: undefined,
|
||||
originalY: undefined,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error restoring original coordinates:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear refs after a short delay to allow animation to start
|
||||
|
||||
// Clear refs
|
||||
setTimeout(() => {
|
||||
pinnedScreenPositionRef.current = null
|
||||
originalCoordinatesRef.current = null
|
||||
originalSizeRef.current = null
|
||||
originalZoomRef.current = null
|
||||
lastCameraRef.current = null
|
||||
pendingUpdateRef.current = null
|
||||
}, 50)
|
||||
}
|
||||
|
||||
|
|
@ -263,18 +230,15 @@ export function usePinnedToView(
|
|||
return
|
||||
}
|
||||
|
||||
// Use requestAnimationFrame for smooth, continuous updates
|
||||
// Throttle updates to reduce jitter
|
||||
// Update position (NOT size) to maintain screen position
|
||||
const updatePinnedPosition = (timestamp: number) => {
|
||||
if (isUpdatingRef.current) {
|
||||
animationFrameRef.current = requestAnimationFrame(updatePinnedPosition)
|
||||
if (isUpdatingRef.current || !editor || !shapeId || !isPinned) {
|
||||
if (isPinned) {
|
||||
animationFrameRef.current = requestAnimationFrame(updatePinnedPosition)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!editor || !shapeId || !isPinned) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const currentShape = editor.getShape(shapeId as TLShapeId)
|
||||
if (!currentShape) {
|
||||
animationFrameRef.current = requestAnimationFrame(updatePinnedPosition)
|
||||
|
|
@ -284,29 +248,34 @@ export function usePinnedToView(
|
|||
const currentCamera = editor.getCamera()
|
||||
const lastCamera = lastCameraRef.current
|
||||
|
||||
// For preset positions (top-center, etc.), always recalculate based on viewport
|
||||
// For 'current' position, use the stored screen position
|
||||
// Calculate pinned screen position
|
||||
let pinnedScreenPos: { x: number; y: number }
|
||||
|
||||
if (position !== 'current') {
|
||||
const viewport = editor.getViewportScreenBounds()
|
||||
const shapeWidth = (currentShape.props as any).w || 0
|
||||
const shapeHeight = (currentShape.props as any).h || 0
|
||||
const pinnedAtZoom = (currentShape.meta as any)?.pinnedAtZoom || currentCamera.z
|
||||
|
||||
// For preset positions, account for the visual scale
|
||||
const visualScale = pinnedAtZoom / currentCamera.z
|
||||
const visualWidth = shapeWidth * visualScale
|
||||
const visualHeight = shapeHeight * visualScale
|
||||
|
||||
if (position === 'top-center') {
|
||||
pinnedScreenPos = {
|
||||
x: viewport.x + (viewport.w / 2) - (shapeWidth * currentCamera.z / 2) + offsetX,
|
||||
x: viewport.x + (viewport.w / 2) - (visualWidth * currentCamera.z / 2) + offsetX,
|
||||
y: viewport.y + offsetY,
|
||||
}
|
||||
} else if (position === 'bottom-center') {
|
||||
pinnedScreenPos = {
|
||||
x: viewport.x + (viewport.w / 2) - (shapeWidth * currentCamera.z / 2) + offsetX,
|
||||
y: viewport.y + viewport.h - (shapeHeight * currentCamera.z) - offsetY,
|
||||
x: viewport.x + (viewport.w / 2) - (visualWidth * currentCamera.z / 2) + offsetX,
|
||||
y: viewport.y + viewport.h - (visualHeight * currentCamera.z) - offsetY,
|
||||
}
|
||||
} else if (position === 'center') {
|
||||
pinnedScreenPos = {
|
||||
x: viewport.x + (viewport.w / 2) - (shapeWidth * currentCamera.z / 2) + offsetX,
|
||||
y: viewport.y + (viewport.h / 2) - (shapeHeight * currentCamera.z / 2) + offsetY,
|
||||
x: viewport.x + (viewport.w / 2) - (visualWidth * currentCamera.z / 2) + offsetX,
|
||||
y: viewport.y + (viewport.h / 2) - (visualHeight * currentCamera.z / 2) + offsetY,
|
||||
}
|
||||
} else {
|
||||
pinnedScreenPos = pinnedScreenPositionRef.current!
|
||||
|
|
@ -319,124 +288,78 @@ export function usePinnedToView(
|
|||
pinnedScreenPos = pinnedScreenPositionRef.current
|
||||
}
|
||||
|
||||
// Check if camera has changed significantly
|
||||
// Check if camera has changed
|
||||
const cameraChanged = !lastCamera || (
|
||||
Math.abs(currentCamera.x - lastCamera.x) > 0.1 ||
|
||||
Math.abs(currentCamera.y - lastCamera.y) > 0.1 ||
|
||||
Math.abs(currentCamera.z - lastCamera.z) > 0.001
|
||||
)
|
||||
|
||||
// For preset positions, always check for updates (viewport might have changed)
|
||||
const shouldUpdate = cameraChanged || position !== 'current'
|
||||
|
||||
if (shouldUpdate) {
|
||||
// Throttle updates to max 60fps (every ~16ms)
|
||||
const timeSinceLastUpdate = timestamp - lastUpdateTimeRef.current
|
||||
const minUpdateInterval = 16 // ~60fps
|
||||
|
||||
if (timeSinceLastUpdate >= minUpdateInterval) {
|
||||
try {
|
||||
// Convert the pinned screen position back to page coordinates
|
||||
// Convert screen position back to page coordinates
|
||||
const newPagePoint = editor.screenToPage(pinnedScreenPos)
|
||||
|
||||
// Calculate delta
|
||||
|
||||
const deltaX = Math.abs(currentShape.x - newPagePoint.x)
|
||||
const deltaY = Math.abs(currentShape.y - newPagePoint.y)
|
||||
|
||||
// Check if zoom changed - if so, adjust size to maintain constant visual size
|
||||
const zoomChanged = lastCamera && Math.abs(currentCamera.z - lastCamera.z) > 0.001
|
||||
let needsSizeUpdate = false
|
||||
let newW = (currentShape.props as any).w
|
||||
let newH = (currentShape.props as any).h
|
||||
|
||||
if (zoomChanged && originalSizeRef.current && originalZoomRef.current !== null) {
|
||||
// Calculate the size needed to maintain constant visual size
|
||||
// Visual size = page size * zoom
|
||||
// To keep visual size constant: new_page_size = (original_page_size * original_zoom) / new_zoom
|
||||
const originalW = originalSizeRef.current.w
|
||||
const originalH = originalSizeRef.current.h
|
||||
const originalZoom = originalZoomRef.current
|
||||
const currentZoom = currentCamera.z
|
||||
|
||||
newW = (originalW * originalZoom) / currentZoom
|
||||
newH = (originalH * originalZoom) / currentZoom
|
||||
|
||||
const currentW = (currentShape.props as any).w || originalW
|
||||
const currentH = (currentShape.props as any).h || originalH
|
||||
|
||||
// Check if size needs updating
|
||||
needsSizeUpdate = Math.abs(newW - currentW) > 0.1 || Math.abs(newH - currentH) > 0.1
|
||||
}
|
||||
|
||||
// Only update if the position would actually change significantly or size needs updating
|
||||
if (deltaX > 0.5 || deltaY > 0.5 || needsSizeUpdate) {
|
||||
|
||||
// Only update position if it changed significantly
|
||||
// Note: We do NOT update w/h - visual scaling is handled by CSS transform
|
||||
if (deltaX > 0.5 || deltaY > 0.5) {
|
||||
isUpdatingRef.current = true
|
||||
|
||||
// Batch the update using editor.batch for smoother updates
|
||||
|
||||
editor.batch(() => {
|
||||
const updateData: any = {
|
||||
id: shapeId,
|
||||
editor.updateShape({
|
||||
id: shapeId as TLShapeId,
|
||||
type: currentShape.type,
|
||||
x: newPagePoint.x,
|
||||
y: newPagePoint.y,
|
||||
}
|
||||
|
||||
// Only update size if it changed
|
||||
if (needsSizeUpdate) {
|
||||
updateData.props = {
|
||||
...currentShape.props,
|
||||
w: newW,
|
||||
h: newH,
|
||||
}
|
||||
}
|
||||
|
||||
editor.updateShape(updateData)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
lastUpdateTimeRef.current = timestamp
|
||||
isUpdatingRef.current = false
|
||||
}
|
||||
|
||||
lastCameraRef.current = { ...currentCamera }
|
||||
} catch (error) {
|
||||
console.error('Error updating pinned shape position/size:', error)
|
||||
console.error('Error updating pinned shape position:', error)
|
||||
isUpdatingRef.current = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Continue monitoring
|
||||
animationFrameRef.current = requestAnimationFrame(updatePinnedPosition)
|
||||
}
|
||||
|
||||
// Start the animation loop
|
||||
lastUpdateTimeRef.current = performance.now()
|
||||
animationFrameRef.current = requestAnimationFrame(updatePinnedPosition)
|
||||
|
||||
// Also listen for shape changes (in case user drags the shape while pinned)
|
||||
// This updates the pinned position to the new location
|
||||
// Listen for shape changes (user dragging while pinned)
|
||||
const handleShapeChange = (event: any) => {
|
||||
if (isUpdatingRef.current) return // Don't update if we're programmatically moving it
|
||||
|
||||
if (!editor || !shapeId || !isPinned) return
|
||||
|
||||
// Only respond to changes that affect this specific shape
|
||||
if (isUpdatingRef.current || !editor || !shapeId || !isPinned) return
|
||||
|
||||
const changedShapes = event?.changedShapes || event?.shapes || []
|
||||
const shapeChanged = changedShapes.some((s: any) => s?.id === (shapeId as TLShapeId))
|
||||
|
||||
|
||||
if (!shapeChanged) return
|
||||
|
||||
|
||||
const currentShape = editor.getShape(shapeId as TLShapeId)
|
||||
if (!currentShape) return
|
||||
|
||||
// Update the pinned screen position to the shape's current screen position
|
||||
// Update the pinned screen position
|
||||
const pagePoint = { x: currentShape.x, y: currentShape.y }
|
||||
const screenPoint = editor.pageToScreen(pagePoint)
|
||||
pinnedScreenPositionRef.current = { x: screenPoint.x, y: screenPoint.y }
|
||||
lastCameraRef.current = { ...editor.getCamera() }
|
||||
}
|
||||
|
||||
// Listen for shape updates (when user drags the shape)
|
||||
editor.on('change' as any, handleShapeChange)
|
||||
|
||||
return () => {
|
||||
|
|
@ -452,4 +375,3 @@ export function usePinnedToView(
|
|||
}
|
||||
}, [editor, shapeId, isPinned, position, offsetX, offsetY])
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue