优化交互
This commit is contained in:
@@ -237,8 +237,12 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
const interactionPortalRef = useRef<HTMLDivElement | null>(null);
|
||||
const dragFloatImgRefs = useRef<Map<string, HTMLImageElement>>(new Map());
|
||||
const dragFloatUrlsRef = useRef<Record<string, string>>({});
|
||||
const dragFloatElementsRef = useRef<TemplateElement[]>([]);
|
||||
const dragVisualActivatedRef = useRef(false);
|
||||
const dragTransformLoopRef = useRef<number | null>(null);
|
||||
const [dragFloatElements, setDragFloatElements] = useState<TemplateElement[]>([]);
|
||||
const [dragInteractionVisualActive, setDragInteractionVisualActive] = useState(false);
|
||||
const [interactionCanvasReady, setInteractionCanvasReady] = useState(false);
|
||||
const interactionFloatPaintRef = useRef(0);
|
||||
const interactionFloatTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
@@ -513,6 +517,10 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
setInteractionSnapshot(null);
|
||||
setInteractionFloatElements(null);
|
||||
setDragFloatElements([]);
|
||||
dragFloatElementsRef.current = [];
|
||||
dragVisualActivatedRef.current = false;
|
||||
setDragInteractionVisualActive(false);
|
||||
setInteractionCanvasReady(false);
|
||||
dragFloatImgRefs.current.clear();
|
||||
dragFloatUrlsRef.current = {};
|
||||
pendingDragDeltaRef.current = { x: 0, y: 0 };
|
||||
@@ -643,6 +651,36 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
const maxZoomRef = useRef(maxZoom);
|
||||
maxZoomRef.current = maxZoom;
|
||||
|
||||
const prefetchDragFloatImages = useCallback(
|
||||
(elements: TemplateElement[]) => {
|
||||
if (elements.length === 0) return;
|
||||
void Promise.all(
|
||||
elements.map(async (el) => {
|
||||
try {
|
||||
const url = await renderElementFloatPreview(
|
||||
el,
|
||||
canvasRenderScale,
|
||||
previewDefaults
|
||||
);
|
||||
dragFloatUrlsRef.current[el.id] = url;
|
||||
const img = dragFloatImgRefs.current.get(el.id);
|
||||
if (img) {
|
||||
img.src = url;
|
||||
if (img.complete) {
|
||||
img.style.opacity = '1';
|
||||
const placeholder = img.previousElementSibling as HTMLElement | null;
|
||||
if (placeholder) placeholder.style.display = 'none';
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Drag float preview error:', err);
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
[canvasRenderScale, previewDefaults]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setZoom((z) => (z > maxZoom ? clampZoomValue(z, maxZoom) : z));
|
||||
}, [maxZoom]);
|
||||
@@ -788,6 +826,11 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
}
|
||||
if (dragState.type === 'drag' && pastDragThreshold) {
|
||||
dragMovedRef.current = true;
|
||||
if (!dragVisualActivatedRef.current) {
|
||||
dragVisualActivatedRef.current = true;
|
||||
setDragInteractionVisualActive(true);
|
||||
prefetchDragFloatImages(dragFloatElementsRef.current);
|
||||
}
|
||||
}
|
||||
const MM_PER_PX = 1 / (3.78 * displayZoomRef.current);
|
||||
const deltaXMm = deltaX * MM_PER_PX;
|
||||
@@ -933,7 +976,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
window.removeEventListener('pointerup', handlePointerUp);
|
||||
window.removeEventListener('pointercancel', handlePointerUp);
|
||||
};
|
||||
}, [dragState, template, displayZoom, onChange, cancelActiveInteraction, clearInteractionCanvasState]);
|
||||
}, [dragState, template, displayZoom, onChange, cancelActiveInteraction, clearInteractionCanvasState, prefetchDragFloatImages]);
|
||||
|
||||
const buildGroupStartPositions = (element: TemplateElement) => {
|
||||
const moveIds =
|
||||
@@ -1048,6 +1091,10 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
dragFloatImgRefs.current.clear();
|
||||
dragCommitHistoryRef.current = null;
|
||||
dragCommitHistoryPrevRef.current = null;
|
||||
dragVisualActivatedRef.current = false;
|
||||
setDragInteractionVisualActive(false);
|
||||
setInteractionCanvasReady(false);
|
||||
dragFloatElementsRef.current = floating;
|
||||
setDragFloatElements(floating);
|
||||
setDragState({
|
||||
elementId: element.id,
|
||||
@@ -1060,32 +1107,6 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
startElemH: element.height,
|
||||
groupStartPositions,
|
||||
});
|
||||
|
||||
if (actionType === 'drag' && floating.length > 0) {
|
||||
void Promise.all(
|
||||
floating.map(async (el) => {
|
||||
try {
|
||||
const url = await renderElementFloatPreview(
|
||||
el,
|
||||
canvasRenderScale,
|
||||
previewDefaults
|
||||
);
|
||||
dragFloatUrlsRef.current[el.id] = url;
|
||||
const img = dragFloatImgRefs.current.get(el.id);
|
||||
if (img) {
|
||||
img.src = url;
|
||||
if (img.complete) {
|
||||
img.style.opacity = '1';
|
||||
const placeholder = img.previousElementSibling as HTMLElement | null;
|
||||
if (placeholder) placeholder.style.display = 'none';
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Drag float preview error:', err);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResizeStart = (
|
||||
@@ -1103,6 +1124,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
setInteractionSnapshot(template.elements);
|
||||
setInteractionFloatElements([element]);
|
||||
interactionFloatPaintRef.current = 0;
|
||||
setInteractionCanvasReady(false);
|
||||
setDragState({
|
||||
elementId: element.id,
|
||||
type: 'resize',
|
||||
@@ -1149,25 +1171,72 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
return new Set(Object.keys(dragState.groupStartPositions));
|
||||
}, [dragState]);
|
||||
|
||||
const liftSelectedAboveDim = activeTool === 'move' && selectedElementIds.length > 0;
|
||||
|
||||
const staticCanvasElements = useMemo(() => {
|
||||
if (dragState?.type === 'drag' && interactionSnapshot && dragMovingIdSet) {
|
||||
return filterVisibleElements(
|
||||
let elements: TemplateElement[];
|
||||
if (
|
||||
dragState?.type === 'drag' &&
|
||||
interactionSnapshot &&
|
||||
dragMovingIdSet &&
|
||||
dragInteractionVisualActive &&
|
||||
interactionCanvasReady
|
||||
) {
|
||||
elements = filterVisibleElements(
|
||||
interactionSnapshot.filter((el) => !dragMovingIdSet.has(el.id))
|
||||
);
|
||||
}
|
||||
if (dragState?.type === 'resize' && interactionSnapshot) {
|
||||
return filterVisibleElements(
|
||||
} else if (dragState?.type === 'resize' && interactionSnapshot && interactionCanvasReady) {
|
||||
elements = filterVisibleElements(
|
||||
interactionSnapshot.filter((el) => el.id !== dragState.elementId)
|
||||
);
|
||||
} else {
|
||||
elements = filterVisibleElements(template.elements);
|
||||
}
|
||||
return filterVisibleElements(template.elements);
|
||||
|
||||
if (liftSelectedAboveDim) {
|
||||
const selectedSet = new Set(selectedElementIds);
|
||||
elements = elements.filter((el) => !selectedSet.has(el.id));
|
||||
}
|
||||
|
||||
return elements;
|
||||
}, [
|
||||
dragState?.type,
|
||||
dragState?.elementId,
|
||||
interactionSnapshot,
|
||||
dragMovingIdSet,
|
||||
dragInteractionVisualActive,
|
||||
interactionCanvasReady,
|
||||
template.elements,
|
||||
filterVisibleElements,
|
||||
liftSelectedAboveDim,
|
||||
selectedElementIds,
|
||||
]);
|
||||
|
||||
const selectedLiftElements = useMemo(() => {
|
||||
if (!liftSelectedAboveDim) return null;
|
||||
|
||||
let elements = filterVisibleElements(
|
||||
template.elements.filter((el) => selectedElementIds.includes(el.id))
|
||||
);
|
||||
|
||||
if (dragState?.type === 'drag' && dragInteractionVisualActive && dragMovingIdSet) {
|
||||
elements = elements.filter((el) => !dragMovingIdSet.has(el.id));
|
||||
}
|
||||
if (dragState?.type === 'resize' && interactionCanvasReady && dragState.elementId) {
|
||||
elements = elements.filter((el) => el.id !== dragState.elementId);
|
||||
}
|
||||
|
||||
return elements.length > 0 ? elements : null;
|
||||
}, [
|
||||
liftSelectedAboveDim,
|
||||
selectedElementIds,
|
||||
template.elements,
|
||||
filterVisibleElements,
|
||||
dragState?.type,
|
||||
dragState?.elementId,
|
||||
dragInteractionVisualActive,
|
||||
dragMovingIdSet,
|
||||
interactionCanvasReady,
|
||||
]);
|
||||
|
||||
const resizeFloatElements = useMemo(() => {
|
||||
@@ -1199,7 +1268,30 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
};
|
||||
}, [allowInteractionOverflow, syncInteractionPortalLayout]);
|
||||
|
||||
const hideSelectionChrome = suppressSelectionChrome || dragState?.type === 'drag';
|
||||
useEffect(() => {
|
||||
const shouldPrepareCanvas =
|
||||
(dragState?.type === 'drag' && dragInteractionVisualActive) ||
|
||||
dragState?.type === 'resize';
|
||||
if (!shouldPrepareCanvas) {
|
||||
setInteractionCanvasReady(false);
|
||||
return;
|
||||
}
|
||||
setInteractionCanvasReady(false);
|
||||
let cancelled = false;
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!cancelled) setInteractionCanvasReady(true);
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [dragState, dragInteractionVisualActive]);
|
||||
|
||||
const hideSelectionChrome =
|
||||
suppressSelectionChrome || (dragState?.type === 'drag' && dragInteractionVisualActive);
|
||||
const showUnselectedDimLayer = activeTool === 'move';
|
||||
const showUnselectedDim = showUnselectedDimLayer && selectedElementIds.length > 0;
|
||||
const resizingPreviewElement =
|
||||
dragState?.type === 'resize'
|
||||
? interactionFloatElements?.find((el) => el.id === dragState.elementId)
|
||||
@@ -1264,7 +1356,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
>
|
||||
<ZoomOut className="w-3 h-3" />
|
||||
</button>
|
||||
<span className="px-1.5 text-[10px] font-mono text-[#ccc] min-w-[42px] text-center select-none">
|
||||
<span className="px-1 text-[10px] font-mono text-[#ccc] min-w-[32px] text-center select-none">
|
||||
{Math.round(displayZoom * 100)}%
|
||||
</span>
|
||||
<button
|
||||
@@ -1289,7 +1381,9 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
|
||||
const showInteractionFloatPortal =
|
||||
typeof document !== 'undefined' &&
|
||||
((dragState?.type === 'drag' && dragFloatElements.length > 0) ||
|
||||
((dragState?.type === 'drag' &&
|
||||
dragInteractionVisualActive &&
|
||||
dragFloatElements.length > 0) ||
|
||||
(resizeFloatElements && resizeFloatElements.length > 0));
|
||||
|
||||
const interactionFloatPortal = showInteractionFloatPortal
|
||||
@@ -1367,13 +1461,25 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
style={{ overflow: 'visible' }}
|
||||
>
|
||||
{resizeFloatElements.map((el) => (
|
||||
<ElementFloatPreview
|
||||
key={el.id}
|
||||
element={el}
|
||||
renderScale={canvasRenderScale}
|
||||
displayZoom={displayZoom}
|
||||
variableDefaults={previewDefaults}
|
||||
/>
|
||||
<React.Fragment key={el.id}>
|
||||
<div
|
||||
className="absolute pointer-events-none border border-dashed border-[#31a8ff]/40 resize-float-placeholder"
|
||||
style={{
|
||||
left: `${mmToPx(el.x, displayZoom)}px`,
|
||||
top: `${mmToPx(el.y, displayZoom)}px`,
|
||||
width: `${mmToPx(el.width, displayZoom)}px`,
|
||||
height: `${mmToPx(el.height, displayZoom)}px`,
|
||||
transform: el.rotation ? `rotate(${el.rotation}deg)` : undefined,
|
||||
transformOrigin: 'center center',
|
||||
}}
|
||||
/>
|
||||
<ElementFloatPreview
|
||||
element={el}
|
||||
renderScale={canvasRenderScale}
|
||||
displayZoom={displayZoom}
|
||||
variableDefaults={previewDefaults}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -1456,6 +1562,56 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showUnselectedDimLayer && (
|
||||
<div
|
||||
className={`ps-unselected-dim-layer${
|
||||
showUnselectedDim ? ' ps-unselected-dim-layer--visible' : ''
|
||||
}`}
|
||||
aria-hidden={!showUnselectedDim}
|
||||
>
|
||||
{overlayElements.map((element) => {
|
||||
if (selectedElementIds.includes(element.id)) return null;
|
||||
if (dragState?.type === 'drag' && dragMovingIdSet?.has(element.id)) return null;
|
||||
const elemX = mmToPx(element.x, displayZoom);
|
||||
const elemY = mmToPx(element.y, displayZoom);
|
||||
const elemW = mmToPx(element.width, displayZoom);
|
||||
const elemH = mmToPx(element.height, displayZoom);
|
||||
return (
|
||||
<div
|
||||
key={`dim-${element.id}`}
|
||||
className="ps-unselected-dim absolute"
|
||||
style={{
|
||||
left: `${elemX}px`,
|
||||
top: `${elemY}px`,
|
||||
width: `${elemW}px`,
|
||||
height: `${elemH}px`,
|
||||
transform: element.rotation
|
||||
? `rotate(${element.rotation}deg)`
|
||||
: undefined,
|
||||
transformOrigin: 'center center',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedLiftElements && (
|
||||
<div className="ps-selected-lift-layer">
|
||||
<CanvasLabelImage
|
||||
widthMm={template.width}
|
||||
heightMm={template.height}
|
||||
elements={selectedLiftElements}
|
||||
rowData={null}
|
||||
rowIndex={0}
|
||||
showBorder={false}
|
||||
transparentBackground
|
||||
variableDefaults={previewDefaults}
|
||||
renderScale={canvasRenderScale}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTool === 'move' && (
|
||||
<div
|
||||
className="absolute inset-0 z-[1] cursor-crosshair touch-none"
|
||||
@@ -1481,7 +1637,9 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
const isSelected = !isLocked && selectedElementIds.includes(element.id);
|
||||
const isPrimary = selectedElementIds.indexOf(element.id) === 0;
|
||||
const isBeingDragged =
|
||||
dragState?.type === 'drag' && !!dragMovingIdSet?.has(element.id);
|
||||
dragInteractionVisualActive &&
|
||||
dragState?.type === 'drag' &&
|
||||
!!dragMovingIdSet?.has(element.id);
|
||||
const isBeingResized =
|
||||
dragState?.type === 'resize' && dragState.elementId === element.id;
|
||||
if (isBeingDragged) {
|
||||
@@ -1491,9 +1649,10 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
const overlayElement =
|
||||
isBeingResized && resizingPreviewElement ? resizingPreviewElement : element;
|
||||
|
||||
const showSelectionRing = isSelected && !hideSelectionChrome;
|
||||
const showResizeHandles =
|
||||
showSelectionRing &&
|
||||
isSelected &&
|
||||
!hideSelectionChrome &&
|
||||
!isLocked &&
|
||||
selectedElementIds.length === 1 &&
|
||||
dragState?.type !== 'resize';
|
||||
const elemX = mmToPx(overlayElement.x, displayZoom);
|
||||
@@ -1508,8 +1667,6 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
className={`absolute group touch-none ${
|
||||
isLocked
|
||||
? 'pointer-events-none'
|
||||
: showSelectionRing
|
||||
? 'ps-selection-ring cursor-move'
|
||||
: isSelected
|
||||
? 'cursor-move'
|
||||
: 'cursor-move hover:outline hover:outline-1 hover:outline-[#31a8ff]/40'
|
||||
@@ -1589,7 +1746,6 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
<div className="mobile-design-toolstrip">
|
||||
<div className="mobile-design-toolstrip-tools">
|
||||
{mainToolButtons}
|
||||
{deleteToolButton}
|
||||
</div>
|
||||
{zoomControls}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user