Skip to content

Commit 7622386

Browse files
Merge remote-tracking branch 'origin/develop' into fb-ROOT-212
2 parents 2374bd6 + e80029c commit 7622386

File tree

8 files changed

+302
-94
lines changed

8 files changed

+302
-94
lines changed

.github/workflows/frontend-command.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
ref: ${{ github.event.client_payload.pull_request.head.ref }}
3434

3535
- name: Setup node
36-
uses: actions/setup-node@v5
36+
uses: actions/setup-node@v6
3737
with:
3838
node-version: "${{ env.NODE }}"
3939

.github/workflows/link-check.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
uses: actions/checkout@v5
2626

2727
- name: Set up Node.js
28-
uses: actions/setup-node@v5
28+
uses: actions/setup-node@v6
2929
with:
3030
node-version: "20"
3131

web/libs/editor/src/components/KonvaVector/KonvaVector.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,9 @@ export const KonvaVector = forwardRef<KonvaVectorRef, KonvaVectorProps>((props,
203203
[],
204204
);
205205

206+
// Flag to track programmatic selection to prevent infinite loops
207+
const isProgrammaticSelection = useRef(false);
208+
206209
const {
207210
initialPoints: rawInitialPoints = [],
208211
onPointsChange,
@@ -506,6 +509,16 @@ export const KonvaVector = forwardRef<KonvaVectorRef, KonvaVectorProps>((props,
506509
const getFitScaleStable = useCallback(() => fitScale, [fitScale]);
507510
const getBoundsStable = useCallback(() => ({ width, height }), [width, height]);
508511

512+
// Wrapper for onPointSelected to prevent infinite loops during programmatic selection
513+
const onPointSelectedWrapper = useCallback(
514+
(index: number | null) => {
515+
if (!isProgrammaticSelection.current && onPointSelected) {
516+
onPointSelected(index);
517+
}
518+
},
519+
[onPointSelected],
520+
);
521+
509522
// Register instance with tracker
510523
useEffect(() => {
511524
const vectorInstance: VectorInstance = {
@@ -514,7 +527,7 @@ export const KonvaVector = forwardRef<KonvaVectorRef, KonvaVectorProps>((props,
514527
updatePoints,
515528
setSelectedPoints: setSelectedPointsStable,
516529
setSelectedPointIndex: setSelectedPointIndexStable,
517-
onPointSelected,
530+
onPointSelected: onPointSelectedWrapper,
518531
onTransformationComplete,
519532
getTransform: getTransformStable,
520533
getFitScale: getFitScaleStable,
@@ -534,7 +547,7 @@ export const KonvaVector = forwardRef<KonvaVectorRef, KonvaVectorProps>((props,
534547
updatePoints,
535548
setSelectedPointsStable,
536549
setSelectedPointIndexStable,
537-
onPointSelected,
550+
onPointSelectedWrapper,
538551
onTransformationComplete,
539552
getTransformStable,
540553
getFitScaleStable,
@@ -935,11 +948,19 @@ export const KonvaVector = forwardRef<KonvaVectorRef, KonvaVectorProps>((props,
935948
}
936949

937950
// Use tracker for global selection management
951+
isProgrammaticSelection.current = true;
938952
tracker.selectPoints(instanceId, selectedIndices);
953+
setTimeout(() => {
954+
isProgrammaticSelection.current = false;
955+
}, 0);
939956
},
940957
clearSelection: () => {
941958
// Use tracker for global selection management
959+
isProgrammaticSelection.current = true;
942960
tracker.selectPoints(instanceId, new Set());
961+
setTimeout(() => {
962+
isProgrammaticSelection.current = false;
963+
}, 0);
943964
},
944965
getSelectedPointIds: () => {
945966
const selectedIds: string[] = [];

web/libs/editor/src/components/KonvaVector/components/VectorTransformer.tsx

Lines changed: 118 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import React from "react";
22
import { Transformer as KonvaTransformer } from "react-konva";
33
import type Konva from "konva";
44
import type { BezierPoint } from "../types";
5-
import { applyTransformationToPoints, resetTransformState } from "../utils/transformUtils";
5+
import {
6+
applyTransformationToPoints,
7+
resetTransformState,
8+
applyTransformationToControlPoints,
9+
updateOriginalPositions,
10+
} from "../utils/transformUtils";
611

712
interface VectorTransformerProps {
813
selectedPoints: Set<number>;
@@ -57,8 +62,24 @@ export const VectorTransformer: React.FC<VectorTransformerProps> = ({
5762
controlPoint1?: { x: number; y: number };
5863
controlPoint2?: { x: number; y: number };
5964
};
65+
initialRotation?: number;
6066
}>({});
6167

68+
// RAF for smooth control point updates
69+
const rafIdRef = React.useRef<number | null>(null);
70+
71+
// Track if this is the first transformation tick to avoid control point jumping
72+
const isFirstTransformTickRef = React.useRef<boolean>(true);
73+
74+
// Cleanup RAF on unmount
75+
React.useEffect(() => {
76+
return () => {
77+
if (rafIdRef.current) {
78+
cancelAnimationFrame(rafIdRef.current);
79+
}
80+
};
81+
}, []);
82+
6283
if (selectedPoints.size <= 1 || initialPoints.length === 0) return null;
6384

6485
// Calculate the bounding box of selected points for the drag area
@@ -135,13 +156,40 @@ export const VectorTransformer: React.FC<VectorTransformerProps> = ({
135156
transformer,
136157
initialPoints,
137158
proxyRefs,
138-
true,
159+
false, // Don't update control points here
139160
originalPositionsRef.current,
140161
transformerCenter,
141162
constrainToBounds,
142163
bounds,
143164
);
144-
onPointsChange?.(newPoints);
165+
166+
// Skip control point transformations on the first tick to avoid jumping
167+
if (isFirstTransformTickRef.current) {
168+
isFirstTransformTickRef.current = false;
169+
onPointsChange?.(newPoints);
170+
} else {
171+
// Apply transformation to control points using RAF
172+
if (rafIdRef.current) {
173+
cancelAnimationFrame(rafIdRef.current);
174+
}
175+
rafIdRef.current = requestAnimationFrame(() => {
176+
// Check if this is actually a rotation operation (not just scaling)
177+
const isActualRotation = Math.abs(transformer.rotation()) > 1.0;
178+
179+
// Apply transformation to control points using original positions as base
180+
const updatedPoints = applyTransformationToControlPoints(
181+
newPoints,
182+
originalPositionsRef.current,
183+
transformer.rotation(),
184+
transformer.scaleX(),
185+
transformer.scaleY(),
186+
transformerCenter.x,
187+
transformerCenter.y,
188+
isActualRotation, // Only apply rotation logic if there's actual rotation
189+
);
190+
onPointsChange?.(updatedPoints);
191+
});
192+
}
145193
} catch (error) {
146194
console.warn("Transform error:", error);
147195
}
@@ -151,19 +199,6 @@ export const VectorTransformer: React.FC<VectorTransformerProps> = ({
151199
// Notify that transformation has started
152200
onTransformationStart?.();
153201

154-
// Store the initial state when transformation starts
155-
const transformer = transformerRef.current;
156-
if (transformer) {
157-
const initialState = {
158-
rotation: transformer.rotation(),
159-
scaleX: transformer.scaleX(),
160-
scaleY: transformer.scaleY(),
161-
centerX: transformer.x() + transformer.width() / 2,
162-
centerY: transformer.y() + transformer.height() / 2,
163-
};
164-
transformerStateRef.current = initialState;
165-
}
166-
167202
// Store original positions of selected points
168203
originalPositionsRef.current = {};
169204
Array.from(selectedPoints).forEach((index) => {
@@ -178,8 +213,14 @@ export const VectorTransformer: React.FC<VectorTransformerProps> = ({
178213
}
179214
});
180215

216+
// Store initial rotation
217+
originalPositionsRef.current.initialRotation = transformerRef.current?.rotation() || 0;
218+
181219
// Reset the first transform flag to ensure proper rotation tracking
182220
resetTransformState();
221+
222+
// Reset the first transform tick flag
223+
isFirstTransformTickRef.current = true;
183224
}}
184225
onDragStart={(_e: any) => {
185226
// Store original positions when dragging starts (for pure drag operations)
@@ -195,6 +236,12 @@ export const VectorTransformer: React.FC<VectorTransformerProps> = ({
195236
};
196237
}
197238
});
239+
240+
// Store initial rotation
241+
originalPositionsRef.current.initialRotation = transformerRef.current?.rotation() || 0;
242+
243+
// Reset the first transform tick flag
244+
isFirstTransformTickRef.current = true;
198245
}}
199246
onDragMove={(_e: any) => {
200247
// Apply drag movement to real points in real-time
@@ -209,13 +256,31 @@ export const VectorTransformer: React.FC<VectorTransformerProps> = ({
209256
transformer,
210257
initialPoints,
211258
proxyRefs,
212-
true,
259+
false, // Don't update control points here
213260
originalPositionsRef.current,
214261
transformerCenter,
215262
constrainToBounds,
216263
bounds,
217264
);
218-
onPointsChange?.(newPoints);
265+
266+
// Apply transformation to control points using RAF
267+
if (rafIdRef.current) {
268+
cancelAnimationFrame(rafIdRef.current);
269+
}
270+
rafIdRef.current = requestAnimationFrame(() => {
271+
// Apply transformation to control points using original positions as base
272+
const updatedPoints = applyTransformationToControlPoints(
273+
newPoints,
274+
originalPositionsRef.current,
275+
transformer.rotation(),
276+
transformer.scaleX(),
277+
transformer.scaleY(),
278+
transformerCenter.x,
279+
transformerCenter.y,
280+
false, // isRotation = false for onDragMove (translation only)
281+
);
282+
onPointsChange?.(updatedPoints);
283+
});
219284
} catch (error) {
220285
console.warn("Drag move error:", error);
221286
}
@@ -244,7 +309,23 @@ export const VectorTransformer: React.FC<VectorTransformerProps> = ({
244309
constrainToBounds,
245310
bounds,
246311
);
247-
onPointsChange?.(newPoints);
312+
313+
// Apply control point transformations
314+
const updatedPoints = applyTransformationToControlPoints(
315+
newPoints,
316+
originalPositionsRef.current,
317+
transformer.rotation(),
318+
transformer.scaleX(),
319+
transformer.scaleY(),
320+
transformerCenter.x,
321+
transformerCenter.y,
322+
false, // isRotation = false for drag operations
323+
);
324+
325+
onPointsChange?.(updatedPoints);
326+
327+
// Update original positions with the final transformed positions
328+
updateOriginalPositions(updatedPoints, originalPositionsRef.current);
248329

249330
// Store the transformer state for future updates
250331
onTransformStateChange?.({
@@ -284,7 +365,24 @@ export const VectorTransformer: React.FC<VectorTransformerProps> = ({
284365
constrainToBounds,
285366
bounds,
286367
);
287-
onPointsChange?.(newPoints);
368+
// Apply control point transformations
369+
const isActualRotation = Math.abs(transformer.rotation()) > 1.0;
370+
const updatedPoints = applyTransformationToControlPoints(
371+
newPoints,
372+
originalPositionsRef.current,
373+
transformer.rotation(),
374+
transformer.scaleX(),
375+
transformer.scaleY(),
376+
transformerCenter.x,
377+
transformerCenter.y,
378+
isActualRotation,
379+
);
380+
381+
onPointsChange?.(updatedPoints);
382+
383+
// Update original positions with the final transformed positions
384+
// This ensures that subsequent transformations use the current state as the base
385+
updateOriginalPositions(updatedPoints, originalPositionsRef.current);
288386

289387
// Store the transformer state for future updates
290388
onTransformStateChange?.({

0 commit comments

Comments
 (0)