@bycrux/editor 0.11.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centre/middle snap for the caption move gesture.
|
|
3
|
+
*
|
|
4
|
+
* Regression: the first implementation of `captionDragGeometry` translated the
|
|
5
|
+
* segment with no snapping at all, so captions were the only draggable thing in
|
|
6
|
+
* the preview that would not settle on the frame's centre line — overlays did,
|
|
7
|
+
* captions did not. Edge snap remains deliberately absent (its geometry does
|
|
8
|
+
* not transfer to a content-sized anchor box); only centre/middle is expected.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect } from 'vitest'
|
|
11
|
+
import {
|
|
12
|
+
captionDragGeometry,
|
|
13
|
+
captionDragPatch,
|
|
14
|
+
CAPTION_SNAP_THRESHOLD,
|
|
15
|
+
type CaptionDragState,
|
|
16
|
+
type CaptionFrameMetrics,
|
|
17
|
+
} from '../preview/captionDragState'
|
|
18
|
+
|
|
19
|
+
// previewScale 1 with a 1080x1920 design frame: 1 screen px == 1 design px, so
|
|
20
|
+
// a dx of 10.8px is exactly 1% of frame width.
|
|
21
|
+
const METRICS: CaptionFrameMetrics = { previewScale: 1, renderW: 1080, renderH: 1920 }
|
|
22
|
+
const PX_PER_PCT_X = 1080 / 100
|
|
23
|
+
const PX_PER_PCT_Y = 1920 / 100
|
|
24
|
+
|
|
25
|
+
function move(initOffsetX: number, initOffsetY: number): CaptionDragState {
|
|
26
|
+
return {
|
|
27
|
+
id: 'cap-1', type: 'move',
|
|
28
|
+
initX: 0, initY: 0,
|
|
29
|
+
initOffsetX, initOffsetY, initScale: 1,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('captionDragGeometry — centre/middle snap', () => {
|
|
34
|
+
it('snaps offsetX to exactly 0 inside the threshold', () => {
|
|
35
|
+
// Start 5% left of centre, drag right to land 1% past centre (inside 2.5%).
|
|
36
|
+
const g = captionDragGeometry(move(-5, 40), 6 * PX_PER_PCT_X, 0, METRICS)
|
|
37
|
+
expect(g.offsetX).toBe(0)
|
|
38
|
+
expect(g.snapX).toBe(true)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('snaps offsetY to exactly 0 inside the threshold', () => {
|
|
42
|
+
const g = captionDragGeometry(move(40, -5), 0, 6 * PX_PER_PCT_Y, METRICS)
|
|
43
|
+
expect(g.offsetY).toBe(0)
|
|
44
|
+
expect(g.snapY).toBe(true)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('snaps both axes at once when dragged through the centre point', () => {
|
|
48
|
+
const g = captionDragGeometry(move(-1, -1), PX_PER_PCT_X, PX_PER_PCT_Y, METRICS)
|
|
49
|
+
expect(g).toMatchObject({ offsetX: 0, offsetY: 0, snapX: true, snapY: true })
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('does NOT snap outside the threshold — the raw offset is preserved', () => {
|
|
53
|
+
// Land 4% right of centre: outside 2.5%, so no snap and no rounding.
|
|
54
|
+
const g = captionDragGeometry(move(0, 40), 4 * PX_PER_PCT_X, 0, METRICS)
|
|
55
|
+
expect(g.offsetX).toBeCloseTo(4, 6)
|
|
56
|
+
expect(g.snapX).toBe(false)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('uses the same threshold as the overlay hook', () => {
|
|
60
|
+
// Just inside snaps, just outside does not — pinning the boundary so the
|
|
61
|
+
// two gestures cannot drift apart.
|
|
62
|
+
const inside = captionDragGeometry(move(0, 40), (CAPTION_SNAP_THRESHOLD - 0.1) * PX_PER_PCT_X, 0, METRICS)
|
|
63
|
+
const outside = captionDragGeometry(move(0, 40), (CAPTION_SNAP_THRESHOLD + 0.1) * PX_PER_PCT_X, 0, METRICS)
|
|
64
|
+
expect(inside.snapX).toBe(true)
|
|
65
|
+
expect(outside.snapX).toBe(false)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('commits the snapped value, not the raw pointer position', () => {
|
|
69
|
+
const d = move(-5, 40)
|
|
70
|
+
const g = captionDragGeometry(d, 6 * PX_PER_PCT_X, 0, METRICS)
|
|
71
|
+
expect(captionDragPatch(d, g)).toEqual({ offsetX: 0, offsetY: g.offsetY })
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('leaves resize gestures unsnapped and untouched', () => {
|
|
75
|
+
const d: CaptionDragState = {
|
|
76
|
+
id: 'cap-1', type: 'resize-se',
|
|
77
|
+
initX: 0, initY: 0, initOffsetX: 1, initOffsetY: 1, initScale: 1,
|
|
78
|
+
}
|
|
79
|
+
const g = captionDragGeometry(d, 20, 20, METRICS)
|
|
80
|
+
// A resize must never move the segment, and must not report a snap.
|
|
81
|
+
expect(g.offsetX).toBe(1)
|
|
82
|
+
expect(g.offsetY).toBe(1)
|
|
83
|
+
expect(g.snapX).toBeUndefined()
|
|
84
|
+
expect(g.snapY).toBeUndefined()
|
|
85
|
+
expect(captionDragPatch(d, g)).toEqual({ scale: g.scale })
|
|
86
|
+
})
|
|
87
|
+
})
|
|
@@ -345,6 +345,18 @@ export default function CaptionPreview({
|
|
|
345
345
|
)}
|
|
346
346
|
</OverlayErrorBoundary>
|
|
347
347
|
|
|
348
|
+
{/* Centre/middle snap guides. Same amber hairlines, same z 50, and the
|
|
349
|
+
same `drag.type === 'move'` gate the overlay layer uses, so a snapped
|
|
350
|
+
caption reads identically to a snapped overlay. Only the centre pair
|
|
351
|
+
exists here — caption edge snap is deliberately not implemented (its
|
|
352
|
+
geometry does not transfer; see captionDragState.ts). */}
|
|
353
|
+
{drag?.type === 'move' && live?.snapX && (
|
|
354
|
+
<div className="absolute top-0 bottom-0 left-1/2 w-px bg-amber-400 pointer-events-none" style={{ zIndex: 50 }} />
|
|
355
|
+
)}
|
|
356
|
+
{drag?.type === 'move' && live?.snapY && (
|
|
357
|
+
<div className="absolute left-0 right-0 top-1/2 h-px bg-amber-400 pointer-events-none" style={{ zIndex: 50 }} />
|
|
358
|
+
)}
|
|
359
|
+
|
|
348
360
|
{/* Selection box — the only interactive part of the caption layer. z 50
|
|
349
361
|
matches the overlay selection handles (see the layering note above);
|
|
350
362
|
the wrapper stays click-through so only the box itself catches events. */}
|
|
@@ -93,9 +93,14 @@ describe('captionDragGeometry — move', () => {
|
|
|
93
93
|
})
|
|
94
94
|
|
|
95
95
|
it('is a no-op at the starting pointer position', () => {
|
|
96
|
+
// 12/34 are both well outside the centre-snap radius, so the geometry is
|
|
97
|
+
// the untouched starting offsets. The move branch also reports whether each
|
|
98
|
+
// axis is currently snapped (see captionSnap.test.ts) — asserted here as
|
|
99
|
+
// false so this stays a genuine no-op check rather than an assertion about
|
|
100
|
+
// the return shape.
|
|
96
101
|
const d = drag({ initOffsetX: 12, initOffsetY: 34, initScale: 1.5 })
|
|
97
102
|
expect(captionDragGeometry(d, d.initX, d.initY, metrics(540)))
|
|
98
|
-
.toEqual({ offsetX: 12, offsetY: 34, scale: 1.5 })
|
|
103
|
+
.toEqual({ offsetX: 12, offsetY: 34, scale: 1.5, snapX: false, snapY: false })
|
|
99
104
|
})
|
|
100
105
|
})
|
|
101
106
|
|
|
@@ -60,8 +60,18 @@ export interface CaptionGeometry {
|
|
|
60
60
|
offsetX: number
|
|
61
61
|
offsetY: number
|
|
62
62
|
scale: number
|
|
63
|
+
/** True while the move gesture is snapped to the frame's horizontal centre. */
|
|
64
|
+
snapX?: boolean
|
|
65
|
+
/** True while the move gesture is snapped to the frame's vertical middle. */
|
|
66
|
+
snapY?: boolean
|
|
63
67
|
}
|
|
64
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Centre/middle snap radius, in percent of frame. Matches `SNAP_THRESHOLD` in
|
|
71
|
+
* `useDragOverlay` so dragging a caption and dragging an overlay feel the same.
|
|
72
|
+
*/
|
|
73
|
+
export const CAPTION_SNAP_THRESHOLD = 2.5
|
|
74
|
+
|
|
65
75
|
/**
|
|
66
76
|
* The preview's design-resolution mapping.
|
|
67
77
|
*
|
|
@@ -145,7 +155,23 @@ export function captionDragGeometry(
|
|
|
145
155
|
const { dx, dy } = screenDeltaToFramePercent(clientX - drag.initX, clientY - drag.initY, metrics)
|
|
146
156
|
|
|
147
157
|
if (drag.type === 'move') {
|
|
148
|
-
|
|
158
|
+
// Centre/middle snap. Unlike the overlay hook's EDGE snap — whose
|
|
159
|
+
// `(0.5 - s/2) * 100` geometry genuinely does not transfer to a caption's
|
|
160
|
+
// content-sized anchor box (see the file header) — the centre snap is
|
|
161
|
+
// geometry-independent: offsetX 0 is the frame's horizontal centre and
|
|
162
|
+
// offsetY 0 its vertical middle, for a caption exactly as for an overlay.
|
|
163
|
+
// Same threshold as useDragOverlay so both gestures feel identical.
|
|
164
|
+
const rawX = drag.initOffsetX + dx
|
|
165
|
+
const rawY = drag.initOffsetY + dy
|
|
166
|
+
const snapX = Math.abs(rawX) < CAPTION_SNAP_THRESHOLD
|
|
167
|
+
const snapY = Math.abs(rawY) < CAPTION_SNAP_THRESHOLD
|
|
168
|
+
return {
|
|
169
|
+
offsetX: snapX ? 0 : rawX,
|
|
170
|
+
offsetY: snapY ? 0 : rawY,
|
|
171
|
+
scale: drag.initScale,
|
|
172
|
+
snapX,
|
|
173
|
+
snapY,
|
|
174
|
+
}
|
|
149
175
|
}
|
|
150
176
|
|
|
151
177
|
// Resize from a corner: project the pointer delta onto the corner's outward
|