fix(05-04): use ghost rect for field placement, canvas offset for overlays
- Replace cursor+delta calculation with active.rect.current.translated (ghost bounding rect) - Measure coordinates relative to the inner <canvas> element, not the DroppableZone wrapper - Add canvasOffset state + useLayoutEffect to offset field overlays by canvas position within wrapper - Inline PDF coordinate math in handleDragEnd; clamp uses field pixel dimensions
This commit is contained in:
@@ -130,6 +130,8 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children }: FieldPla
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
// Track rendered canvas dimensions in state so renderFields re-runs when they change
|
||||
const [containerSize, setContainerSize] = useState<{ w: number; h: number } | null>(null);
|
||||
// Offset of the <canvas> element relative to the DroppableZone wrapper div
|
||||
const [canvasOffset, setCanvasOffset] = useState({ x: 0, y: 0 });
|
||||
|
||||
// Configure sensors: require a minimum drag distance so clicks on delete buttons
|
||||
// are not intercepted as drag starts
|
||||
@@ -162,65 +164,67 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children }: FieldPla
|
||||
setContainerSize({ w: pageInfo.width, h: pageInfo.height });
|
||||
}, [pageInfo]);
|
||||
|
||||
// Track the canvas element's offset within the DroppableZone wrapper div so that
|
||||
// field overlays (position: absolute relative to the wrapper) are placed correctly.
|
||||
useLayoutEffect(() => {
|
||||
if (!pageInfo || !containerRef.current) return;
|
||||
const canvas = containerRef.current.querySelector('canvas');
|
||||
if (!canvas) return;
|
||||
const canvasRect = canvas.getBoundingClientRect();
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
setCanvasOffset({
|
||||
x: canvasRect.left - containerRect.left,
|
||||
y: canvasRect.top - containerRect.top,
|
||||
});
|
||||
}, [pageInfo]);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
setIsDraggingToken(false);
|
||||
|
||||
const { over, activatorEvent, delta } = event;
|
||||
|
||||
// Only process if dropped onto the PDF zone
|
||||
const { over, active } = event;
|
||||
if (!over || over.id !== 'pdf-drop-zone') return;
|
||||
if (!pageInfo) return;
|
||||
|
||||
const containerRect = containerRef.current?.getBoundingClientRect();
|
||||
if (!containerRect) return;
|
||||
// Use the ghost's final bounding rect so the field appears exactly where
|
||||
// the ghost was — not at cursor, which is offset by the grab point.
|
||||
const ghostRect = active.rect.current.translated;
|
||||
if (!ghostRect) return;
|
||||
|
||||
// Use the actual <canvas> element as the coordinate reference to avoid any
|
||||
// offset between the DroppableZone wrapper div and the inner canvas.
|
||||
const canvas = containerRef.current?.querySelector('canvas');
|
||||
const refRect = (canvas ?? containerRef.current)?.getBoundingClientRect();
|
||||
if (!refRect) return;
|
||||
|
||||
// Use pageInfo.width/height as the authoritative rendered canvas size —
|
||||
// containerRect.width/height could be slightly off if the wrapper div has
|
||||
// any extra decoration that doesn't match the canvas exactly.
|
||||
const renderedW = pageInfo.width;
|
||||
const renderedH = pageInfo.height;
|
||||
|
||||
// Compute the final screen position relative to the container
|
||||
// activatorEvent is the initial MouseEvent/TouchEvent at drag start
|
||||
// delta is the displacement from drag start to drop
|
||||
let clientX: number;
|
||||
let clientY: number;
|
||||
// Ghost's top-left position relative to the canvas
|
||||
const rawX = ghostRect.left - refRect.left;
|
||||
const rawY = ghostRect.top - refRect.top;
|
||||
|
||||
if (activatorEvent instanceof MouseEvent) {
|
||||
clientX = activatorEvent.clientX;
|
||||
clientY = activatorEvent.clientY;
|
||||
} else if (activatorEvent instanceof TouchEvent && activatorEvent.touches.length > 0) {
|
||||
clientX = activatorEvent.touches[0].clientX;
|
||||
clientY = activatorEvent.touches[0].clientY;
|
||||
} else {
|
||||
// Fallback: use center of container
|
||||
clientX = containerRect.left + renderedW / 2;
|
||||
clientY = containerRect.top + renderedH / 2;
|
||||
}
|
||||
// Field dimensions in screen pixels (for clamping)
|
||||
const fieldWpx = (144 / pageInfo.originalWidth) * renderedW;
|
||||
const fieldHpx = (36 / pageInfo.originalHeight) * renderedH;
|
||||
|
||||
// finalClient = activatorClient + delta (displacement during drag)
|
||||
const finalClientX = clientX + delta.x;
|
||||
const finalClientY = clientY + delta.y;
|
||||
// Clamp so field stays within canvas bounds
|
||||
const clampedX = Math.max(0, Math.min(rawX, renderedW - fieldWpx));
|
||||
const clampedY = Math.max(0, Math.min(rawY, renderedH - fieldHpx));
|
||||
|
||||
// Convert to coordinates relative to the container's top-left corner
|
||||
const screenX = finalClientX - containerRect.left;
|
||||
const screenY = finalClientY - containerRect.top;
|
||||
|
||||
// Clamp to canvas bounds
|
||||
const clampedX = Math.max(0, Math.min(screenX, renderedW));
|
||||
const clampedY = Math.max(0, Math.min(screenY, renderedH));
|
||||
|
||||
// Convert screen coords to PDF user-space (Y-flip applied)
|
||||
const { x: pdfX, y: pdfY } = screenToPdfCoords(clampedX, clampedY, renderedW, renderedH, pageInfo);
|
||||
// Convert to PDF user-space coordinates.
|
||||
// pdfX = left edge (X increases right in both screen and PDF).
|
||||
// pdfY = bottom edge (PDF Y=0 is bottom, increases upward).
|
||||
const pdfX = (clampedX / renderedW) * pageInfo.originalWidth;
|
||||
const pdfY = ((renderedH - (clampedY + fieldHpx)) / renderedH) * pageInfo.originalHeight;
|
||||
|
||||
const newField: SignatureFieldData = {
|
||||
id: crypto.randomUUID(),
|
||||
page: currentPage,
|
||||
x: pdfX,
|
||||
y: pdfY,
|
||||
width: 144, // 2 inches at 72 DPI
|
||||
height: 36, // 0.5 inches at 72 DPI
|
||||
width: 144,
|
||||
height: 36,
|
||||
};
|
||||
|
||||
const next = [...fields, newField];
|
||||
@@ -250,8 +254,8 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children }: FieldPla
|
||||
key={field.id}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left,
|
||||
top: top - heightPx, // top is y of bottom-left corner; shift up by height
|
||||
left: left + canvasOffset.x,
|
||||
top: top - heightPx + canvasOffset.y, // top is y of bottom-left corner; shift up by height
|
||||
width: widthPx,
|
||||
height: heightPx,
|
||||
border: '2px solid #2563eb',
|
||||
|
||||
Reference in New Issue
Block a user