fix: properly reset pin state to prevent shape jumping on re-pin
When pinning a shape again after unpinning, leftover state from the previous session was causing the shape to jump/resize unexpectedly. Changes: - Add clearPinState() helper to reset all refs and cancel animations - Add cleanShapeMeta() helper to remove all pin-related meta properties - Clear all state immediately when pinning starts (before setting new state) - Clear refs immediately when unpinning (not in setTimeout) - Remove pinnedAtZoom from meta cleanup (legacy from CSS scaling) - Don't call updatePinnedPosition() on pin start - shape is already at correct position, only need to listen for future camera changes 🤖 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
1bde78bb29
commit
678df2bbca
|
|
@ -46,17 +46,52 @@ export function usePinnedToView(
|
||||||
const shape = editor.getShape(shapeId as TLShapeId)
|
const shape = editor.getShape(shapeId as TLShapeId)
|
||||||
if (!shape) return
|
if (!shape) return
|
||||||
|
|
||||||
|
// Helper to clear all pin-related state
|
||||||
|
const clearPinState = () => {
|
||||||
|
pinnedScreenPositionRef.current = null
|
||||||
|
originalCoordinatesRef.current = null
|
||||||
|
isUpdatingRef.current = false
|
||||||
|
if (driftAnimationRef.current) {
|
||||||
|
cancelAnimationFrame(driftAnimationRef.current)
|
||||||
|
driftAnimationRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to clean shape meta of any pin-related properties
|
||||||
|
const cleanShapeMeta = (currentShape: any) => {
|
||||||
|
const meta = currentShape.meta || {}
|
||||||
|
// Remove all pin-related meta properties
|
||||||
|
const { originalX, originalY, pinnedAtZoom, ...cleanMeta } = meta as any
|
||||||
|
// Only update if there were pin properties to remove
|
||||||
|
if ('originalX' in meta || 'originalY' in meta || 'pinnedAtZoom' in meta) {
|
||||||
|
try {
|
||||||
|
editor.updateShape({
|
||||||
|
id: shapeId as TLShapeId,
|
||||||
|
type: currentShape.type,
|
||||||
|
meta: cleanMeta,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore errors during cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleanMeta
|
||||||
|
}
|
||||||
|
|
||||||
// If just became pinned (transition from false to true)
|
// If just became pinned (transition from false to true)
|
||||||
if (isPinned && !wasPinnedRef.current) {
|
if (isPinned && !wasPinnedRef.current) {
|
||||||
|
// Clear any leftover state from previous pin sessions
|
||||||
|
clearPinState()
|
||||||
|
|
||||||
// Store the original coordinates - these will be restored when unpinned
|
// Store the original coordinates - these will be restored when unpinned
|
||||||
originalCoordinatesRef.current = { x: shape.x, y: shape.y }
|
originalCoordinatesRef.current = { x: shape.x, y: shape.y }
|
||||||
|
|
||||||
// Store original position in meta for unpinning
|
// Store original position in meta for unpinning (clean any old meta first)
|
||||||
|
const cleanMeta = cleanShapeMeta(shape)
|
||||||
editor.updateShape({
|
editor.updateShape({
|
||||||
id: shapeId as TLShapeId,
|
id: shapeId as TLShapeId,
|
||||||
type: shape.type,
|
type: shape.type,
|
||||||
meta: {
|
meta: {
|
||||||
...shape.meta,
|
...cleanMeta,
|
||||||
originalX: shape.x,
|
originalX: shape.x,
|
||||||
originalY: shape.y,
|
originalY: shape.y,
|
||||||
},
|
},
|
||||||
|
|
@ -85,7 +120,7 @@ export function usePinnedToView(
|
||||||
y: viewport.y + (viewport.h / 2) - (shapeHeight * currentCamera.z / 2) + offsetY,
|
y: viewport.y + (viewport.h / 2) - (shapeHeight * currentCamera.z / 2) + offsetY,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Default: use current position
|
// Default: use current position - shape stays exactly where it is
|
||||||
const pagePoint = { x: shape.x, y: shape.y }
|
const pagePoint = { x: shape.x, y: shape.y }
|
||||||
screenPoint = editor.pageToScreen(pagePoint)
|
screenPoint = editor.pageToScreen(pagePoint)
|
||||||
}
|
}
|
||||||
|
|
@ -116,6 +151,12 @@ export function usePinnedToView(
|
||||||
|
|
||||||
// If just became unpinned, animate back to original coordinates
|
// If just became unpinned, animate back to original coordinates
|
||||||
if (!isPinned && wasPinnedRef.current) {
|
if (!isPinned && wasPinnedRef.current) {
|
||||||
|
// Cancel any ongoing animations
|
||||||
|
if (driftAnimationRef.current) {
|
||||||
|
cancelAnimationFrame(driftAnimationRef.current)
|
||||||
|
driftAnimationRef.current = null
|
||||||
|
}
|
||||||
|
|
||||||
// Get original coordinates from meta
|
// Get original coordinates from meta
|
||||||
const currentShape = editor.getShape(shapeId as TLShapeId)
|
const currentShape = editor.getShape(shapeId as TLShapeId)
|
||||||
if (currentShape) {
|
if (currentShape) {
|
||||||
|
|
@ -132,6 +173,10 @@ export function usePinnedToView(
|
||||||
Math.pow(targetX - startX, 2) + Math.pow(targetY - startY, 2)
|
Math.pow(targetX - startX, 2) + Math.pow(targetY - startY, 2)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Immediately clear refs so next pin session starts fresh
|
||||||
|
pinnedScreenPositionRef.current = null
|
||||||
|
originalCoordinatesRef.current = null
|
||||||
|
|
||||||
if (distance > 1) {
|
if (distance > 1) {
|
||||||
// Animation parameters
|
// Animation parameters
|
||||||
const duration = 600 // 600ms for a calm drift
|
const duration = 600 // 600ms for a calm drift
|
||||||
|
|
@ -164,18 +209,7 @@ export function usePinnedToView(
|
||||||
driftAnimationRef.current = requestAnimationFrame(animateDrift)
|
driftAnimationRef.current = requestAnimationFrame(animateDrift)
|
||||||
} else {
|
} else {
|
||||||
// Animation complete - clear pinned meta data
|
// Animation complete - clear pinned meta data
|
||||||
try {
|
cleanShapeMeta(currentShape)
|
||||||
const { originalX, originalY, ...cleanMeta } = (currentShape.meta || {}) as any
|
|
||||||
editor.updateShape({
|
|
||||||
id: shapeId as TLShapeId,
|
|
||||||
type: currentShape.type,
|
|
||||||
x: targetX,
|
|
||||||
y: targetY,
|
|
||||||
meta: cleanMeta,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error setting final position:', error)
|
|
||||||
}
|
|
||||||
driftAnimationRef.current = null
|
driftAnimationRef.current = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -184,25 +218,22 @@ export function usePinnedToView(
|
||||||
} else {
|
} else {
|
||||||
// Distance is too small, just set directly and clear meta
|
// Distance is too small, just set directly and clear meta
|
||||||
try {
|
try {
|
||||||
const { originalX, originalY, ...cleanMeta } = (currentShape.meta || {}) as any
|
|
||||||
editor.updateShape({
|
editor.updateShape({
|
||||||
id: shapeId as TLShapeId,
|
id: shapeId as TLShapeId,
|
||||||
type: currentShape.type,
|
type: currentShape.type,
|
||||||
x: targetX,
|
x: targetX,
|
||||||
y: targetY,
|
y: targetY,
|
||||||
meta: cleanMeta,
|
|
||||||
})
|
})
|
||||||
|
cleanShapeMeta(currentShape)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error restoring original coordinates:', error)
|
console.error('Error restoring original coordinates:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
// Shape doesn't exist, just clear refs
|
||||||
// Clear refs
|
|
||||||
setTimeout(() => {
|
|
||||||
pinnedScreenPositionRef.current = null
|
pinnedScreenPositionRef.current = null
|
||||||
originalCoordinatesRef.current = null
|
originalCoordinatesRef.current = null
|
||||||
}, 50)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wasPinnedRef.current = isPinned
|
wasPinnedRef.current = isPinned
|
||||||
|
|
@ -282,18 +313,19 @@ export function usePinnedToView(
|
||||||
const unsubscribe = editor.store.listen(
|
const unsubscribe = editor.store.listen(
|
||||||
(entry) => {
|
(entry) => {
|
||||||
// Only react to camera changes
|
// Only react to camera changes
|
||||||
const dominated = Object.entries(entry.changes.updated).some(
|
const hasCamera = Object.entries(entry.changes.updated).some(
|
||||||
([, [, record]]) => record.typeName === 'camera'
|
([, [, record]]) => record.typeName === 'camera'
|
||||||
)
|
)
|
||||||
if (dominated && !isUpdatingRef.current) {
|
if (hasCamera && !isUpdatingRef.current) {
|
||||||
updatePinnedPosition()
|
updatePinnedPosition()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ source: 'all', scope: 'document' }
|
{ source: 'all', scope: 'document' }
|
||||||
)
|
)
|
||||||
|
|
||||||
// Also do an immediate update to sync position
|
// Don't call updatePinnedPosition immediately - the shape is already at
|
||||||
updatePinnedPosition()
|
// the correct position since we just captured its current screen position
|
||||||
|
// Only start listening for camera changes
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (driftAnimationRef.current) {
|
if (driftAnimationRef.current) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue