@pictor-sdk/ui 0.1.0
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/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/index.d.mts +833 -0
- package/dist/index.d.ts +833 -0
- package/dist/index.js +4620 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4643 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +68 -0
- package/src/Editor.tsx +276 -0
- package/src/ToolPanelHost.tsx +98 -0
- package/src/components/Slider.tsx +172 -0
- package/src/components/StraightenRuler.tsx +57 -0
- package/src/components/ToolRail.tsx +58 -0
- package/src/components/TopBar.tsx +53 -0
- package/src/composer/TextComposer.tsx +540 -0
- package/src/composer/keyboardLayout.ts +130 -0
- package/src/composer/useTextSession.ts +230 -0
- package/src/gestures/CropGestureSurface.tsx +270 -0
- package/src/gestures/CropGrid.tsx +24 -0
- package/src/gestures/DrawingSurface.tsx +77 -0
- package/src/gestures/EditorGestures.tsx +97 -0
- package/src/gestures/InteractionGuides.tsx +161 -0
- package/src/gestures/LayerGestureSurface.tsx +462 -0
- package/src/gestures/StoryMediaGestureSurface.tsx +517 -0
- package/src/gestures/TiltShiftGestureSurface.tsx +189 -0
- package/src/gestures/controller.ts +152 -0
- package/src/gestures/drawController.ts +104 -0
- package/src/gestures/interactionState.tsx +81 -0
- package/src/gestures/layerGestureWorklet.ts +198 -0
- package/src/gestures/mediaGestureWorklet.ts +159 -0
- package/src/gestures/routing.ts +42 -0
- package/src/gestures/settleCoordinator.ts +61 -0
- package/src/gestures/transformOwnership.ts +91 -0
- package/src/hostCallbacks.tsx +62 -0
- package/src/index.ts +72 -0
- package/src/panels/AddPhotoPanel.tsx +101 -0
- package/src/panels/AdjustPanel.tsx +158 -0
- package/src/panels/ColorPanel.tsx +193 -0
- package/src/panels/CropPanel.tsx +185 -0
- package/src/panels/DrawPanel.tsx +106 -0
- package/src/panels/FilterStrip.tsx +153 -0
- package/src/panels/LayersPanel.tsx +85 -0
- package/src/panels/LayoutPanel.tsx +55 -0
- package/src/panels/LuxPanel.tsx +122 -0
- package/src/panels/MediaPanel.tsx +73 -0
- package/src/panels/PerspectivePanel.tsx +157 -0
- package/src/panels/StickerPanel.tsx +78 -0
- package/src/panels/TextPanel.tsx +177 -0
- package/src/panels/TiltShiftPanel.tsx +146 -0
- package/src/panels/addPhotoController.ts +196 -0
- package/src/panels/cropMath.ts +37 -0
- package/src/panels/layoutMath.ts +58 -0
- package/src/panels/useTargetImageLayer.ts +41 -0
- package/src/theme.ts +61 -0
- package/src/uiState.tsx +57 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deleteTargetFromProfile,
|
|
3
|
+
hitTestDocument,
|
|
4
|
+
isDirectManipulationLayer,
|
|
5
|
+
isSameTransform,
|
|
6
|
+
layerNaturalSize,
|
|
7
|
+
layerGestureConfigFromProfile,
|
|
8
|
+
NO_SNAP,
|
|
9
|
+
useEditor,
|
|
10
|
+
useEditorStore,
|
|
11
|
+
type DeleteTarget,
|
|
12
|
+
type LayerSnapState,
|
|
13
|
+
type Transform,
|
|
14
|
+
} from '@pictor-sdk/core'
|
|
15
|
+
import { preferMeasuredSize, useCanvasMetrics, useRenderer } from '@pictor-sdk/skia'
|
|
16
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
17
|
+
import { StyleSheet } from 'react-native'
|
|
18
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
|
|
19
|
+
import Animated, { useDerivedValue, useSharedValue } from 'react-native-reanimated'
|
|
20
|
+
import { runOnJS } from 'react-native-worklets'
|
|
21
|
+
import {
|
|
22
|
+
composeLayerGestureUI,
|
|
23
|
+
isOverDeleteTargetUI,
|
|
24
|
+
hitTestLayerCandidatesUI,
|
|
25
|
+
layerPreviewDeltaUI,
|
|
26
|
+
} from './layerGestureWorklet'
|
|
27
|
+
import { useInteractionState } from './interactionState'
|
|
28
|
+
import { type GestureSnapEvent } from './controller'
|
|
29
|
+
|
|
30
|
+
/** Overrides for overlay-layer gestures, defaulting to the profile's settings. */
|
|
31
|
+
export interface LayerGestureSurfaceProps {
|
|
32
|
+
/** Fired once per snap engagement — hook up haptics here if you want them. */
|
|
33
|
+
onSnap?: (event: GestureSnapEvent) => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Direct manipulation of overlay layers: grab the top-most layer under the
|
|
38
|
+
* finger and pan, focal-pinch and rotate it on the UI thread.
|
|
39
|
+
*
|
|
40
|
+
* Three things make this feel right, and each is covered by tests:
|
|
41
|
+
*
|
|
42
|
+
* - **Focal composition.** Scale and rotation happen about the gesture
|
|
43
|
+
* centroid, so the document point under the fingers stays under them.
|
|
44
|
+
* - **Pointer-count re-basing.** Gesture Handler restarts its cumulative
|
|
45
|
+
* translation/scale/rotation when a finger lands or lifts. Every recognizer
|
|
46
|
+
* value is read relative to an origin captured at the last re-base, and the
|
|
47
|
+
* frame re-bases from the live transform whenever the pointer count changes
|
|
48
|
+
* — so two fingers going to one does not jump.
|
|
49
|
+
* - **One history entry.** The document is written exactly once, when the
|
|
50
|
+
* gesture settles, and not at all if nothing actually moved.
|
|
51
|
+
*/
|
|
52
|
+
export function LayerGestureSurface({ onSnap }: LayerGestureSurfaceProps) {
|
|
53
|
+
const { document, dispatch, profile } = useEditor()
|
|
54
|
+
const store = useEditorStore()
|
|
55
|
+
const metrics = useCanvasMetrics()
|
|
56
|
+
// Destructure the setters: the context value's identity changes on every
|
|
57
|
+
// interaction state change (including ones this component causes), so
|
|
58
|
+
// depending on the object would tear down effects mid-gesture. The setters
|
|
59
|
+
// themselves are stable.
|
|
60
|
+
const { setPreview, setLayerDragActive, setOverDeleteTarget, setSnap } = useInteractionState()
|
|
61
|
+
|
|
62
|
+
const renderer = useRenderer()
|
|
63
|
+
|
|
64
|
+
const config = useMemo(
|
|
65
|
+
() => layerGestureConfigFromProfile(profile.interaction?.layers),
|
|
66
|
+
[profile],
|
|
67
|
+
)
|
|
68
|
+
const deleteTarget = useMemo(
|
|
69
|
+
() => deleteTargetFromProfile(profile.interaction?.layers),
|
|
70
|
+
[profile],
|
|
71
|
+
)
|
|
72
|
+
const aspect = document.size.height / document.size.width
|
|
73
|
+
// Rebuild hit geometry when the renderer publishes a new text measurement,
|
|
74
|
+
// so a layer's touch box tracks the box it actually draws.
|
|
75
|
+
const [measureVersion, setMeasureVersion] = useState(0)
|
|
76
|
+
useEffect(
|
|
77
|
+
() => renderer.textMeasures.subscribe(() => setMeasureVersion((v) => v + 1)),
|
|
78
|
+
[renderer],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const hitCandidates = useMemo(
|
|
82
|
+
() =>
|
|
83
|
+
document.layers
|
|
84
|
+
.map((layer, index) => ({ layer, index }))
|
|
85
|
+
.filter(
|
|
86
|
+
({ layer, index }) =>
|
|
87
|
+
!layer.hidden &&
|
|
88
|
+
!layer.locked &&
|
|
89
|
+
isDirectManipulationLayer(layer, index, profile.interaction?.layers),
|
|
90
|
+
)
|
|
91
|
+
.reverse()
|
|
92
|
+
.map(({ layer }) => {
|
|
93
|
+
// The renderer's measurement when it has one, core's estimate
|
|
94
|
+
// otherwise — core cannot lay out text, so its estimate is a
|
|
95
|
+
// character-count approximation that does not match the drawn box.
|
|
96
|
+
const natural = preferMeasuredSize(
|
|
97
|
+
renderer.textMeasures.get(layer.id),
|
|
98
|
+
layerNaturalSize(layer, store.registry, document),
|
|
99
|
+
)
|
|
100
|
+
const anchor = layer.transform.anchor ?? { x: 0.5, y: 0.5 }
|
|
101
|
+
return {
|
|
102
|
+
id: layer.id,
|
|
103
|
+
x: layer.transform.x,
|
|
104
|
+
y: layer.transform.y,
|
|
105
|
+
scale: layer.transform.scale,
|
|
106
|
+
rotation: layer.transform.rotation,
|
|
107
|
+
width: natural.width,
|
|
108
|
+
height: natural.height,
|
|
109
|
+
anchorX: anchor.x,
|
|
110
|
+
anchorY: anchor.y,
|
|
111
|
+
flipX: layer.transform.flipX ?? false,
|
|
112
|
+
flipY: layer.transform.flipY ?? false,
|
|
113
|
+
}
|
|
114
|
+
}),
|
|
115
|
+
// `measureVersion` is deliberate: the measurements live outside React, so
|
|
116
|
+
// this is how a new one re-derives the hit geometry.
|
|
117
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
118
|
+
[document, profile, store.registry, renderer, measureVersion],
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
// ── live transform (UI thread) ──
|
|
122
|
+
const liveX = useSharedValue(0.5)
|
|
123
|
+
const liveY = useSharedValue(0.5)
|
|
124
|
+
const liveScale = useSharedValue(1)
|
|
125
|
+
const liveRotation = useSharedValue(0)
|
|
126
|
+
// Immutable for the lifetime of a gesture. The renderer preview is always a
|
|
127
|
+
// delta from the still-committed document transform, never from a frame base.
|
|
128
|
+
const committedX = useSharedValue(0.5)
|
|
129
|
+
const committedY = useSharedValue(0.5)
|
|
130
|
+
const committedScale = useSharedValue(1)
|
|
131
|
+
const committedRotation = useSharedValue(0)
|
|
132
|
+
// ── frame base, re-captured on every pointer-count change ──
|
|
133
|
+
const baseX = useSharedValue(0.5)
|
|
134
|
+
const baseY = useSharedValue(0.5)
|
|
135
|
+
const baseScale = useSharedValue(1)
|
|
136
|
+
const baseRotation = useSharedValue(0)
|
|
137
|
+
// ── recognizer origins, subtracted so each frame starts from zero ──
|
|
138
|
+
const originPanX = useSharedValue(0)
|
|
139
|
+
const originPanY = useSharedValue(0)
|
|
140
|
+
const originPinch = useSharedValue(1)
|
|
141
|
+
const originRotate = useSharedValue(0)
|
|
142
|
+
const rawPanX = useSharedValue(0)
|
|
143
|
+
const rawPanY = useSharedValue(0)
|
|
144
|
+
const rawPinch = useSharedValue(1)
|
|
145
|
+
const rawRotate = useSharedValue(0)
|
|
146
|
+
const focalX = useSharedValue(0.5)
|
|
147
|
+
const focalY = useSharedValue(0.5)
|
|
148
|
+
const pointers = useSharedValue(0)
|
|
149
|
+
const active = useSharedValue(false)
|
|
150
|
+
const snapCX = useSharedValue(false)
|
|
151
|
+
const snapCY = useSharedValue(false)
|
|
152
|
+
const snapR = useSharedValue(false)
|
|
153
|
+
const overDelete = useSharedValue(false)
|
|
154
|
+
|
|
155
|
+
// ── preview transform: a delta from the committed transform, UI-thread only ──
|
|
156
|
+
const previewTransform = useDerivedValue(() => {
|
|
157
|
+
return layerPreviewDeltaUI(
|
|
158
|
+
{
|
|
159
|
+
x: committedX.value,
|
|
160
|
+
y: committedY.value,
|
|
161
|
+
scale: committedScale.value,
|
|
162
|
+
rotation: committedRotation.value,
|
|
163
|
+
},
|
|
164
|
+
{ x: liveX.value, y: liveY.value, scale: liveScale.value, rotation: liveRotation.value },
|
|
165
|
+
document.size.width,
|
|
166
|
+
document.size.height,
|
|
167
|
+
)
|
|
168
|
+
}, [document.size.width, document.size.height])
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Fresh render state for the JS callbacks below.
|
|
172
|
+
*
|
|
173
|
+
* The callbacks themselves must be identity-stable, because a worklet has to
|
|
174
|
+
* capture them *directly*: Reanimated serialises captured objects, so a
|
|
175
|
+
* function reached through `someRef.current` arrives on the UI thread as a
|
|
176
|
+
* plain value and calling it throws "Object is not a function". Stable
|
|
177
|
+
* callbacks + a mutable snapshot gives both fresh data and a gesture object
|
|
178
|
+
* that never needs rebuilding.
|
|
179
|
+
*/
|
|
180
|
+
const latest = useRef({ document, metrics, store, dispatch, onSnap, profile })
|
|
181
|
+
latest.current = { document, metrics, store, dispatch, onSnap, profile }
|
|
182
|
+
|
|
183
|
+
const grabbedId = useRef<string | null>(null)
|
|
184
|
+
const grabbedFrom = useRef<Transform | null>(null)
|
|
185
|
+
const snapState = useRef<LayerSnapState>(NO_SNAP)
|
|
186
|
+
|
|
187
|
+
const onGrabLayer = useCallback((id: string) => {
|
|
188
|
+
const { document: doc, store: s } = latest.current
|
|
189
|
+
const hit = doc.layers.find((layer) => layer.id === id)
|
|
190
|
+
if (!hit) return
|
|
191
|
+
const t = hit.transform
|
|
192
|
+
grabbedId.current = hit.id
|
|
193
|
+
grabbedFrom.current = { ...t }
|
|
194
|
+
s.select([hit.id])
|
|
195
|
+
setPreview({ layerId: hit.id, transform: previewTransform })
|
|
196
|
+
setLayerDragActive(true)
|
|
197
|
+
}, [setPreview, setLayerDragActive, previewTransform])
|
|
198
|
+
|
|
199
|
+
const onSelectAt = useCallback((xDp: number, yDp: number) => {
|
|
200
|
+
const { document: doc, metrics: m, store: s } = latest.current
|
|
201
|
+
if (!m) return
|
|
202
|
+
const layerInteraction = latest.current.profile.interaction?.layers
|
|
203
|
+
const hit = hitTestDocument(
|
|
204
|
+
doc,
|
|
205
|
+
{ x: xDp / m.width, y: yDp / m.height },
|
|
206
|
+
s.registry,
|
|
207
|
+
(layer, index) => isDirectManipulationLayer(layer, index, layerInteraction),
|
|
208
|
+
)
|
|
209
|
+
s.select(hit && !hit.locked ? [hit.id] : [])
|
|
210
|
+
}, [])
|
|
211
|
+
|
|
212
|
+
const onSnapChanged = useCallback(
|
|
213
|
+
(centerX: boolean, centerY: boolean, rotation: boolean) => {
|
|
214
|
+
const previous = snapState.current
|
|
215
|
+
const next = { centerX, centerY, rotation }
|
|
216
|
+
snapState.current = next
|
|
217
|
+
const notify = latest.current.onSnap
|
|
218
|
+
if (centerX && !previous.centerX) notify?.({ kind: 'centerX' })
|
|
219
|
+
if (centerY && !previous.centerY) notify?.({ kind: 'centerY' })
|
|
220
|
+
if (rotation && !previous.rotation) notify?.({ kind: 'rotation' })
|
|
221
|
+
setSnap(next)
|
|
222
|
+
},
|
|
223
|
+
[setSnap],
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
const onHoverChanged = useCallback(
|
|
227
|
+
(over: boolean) => setOverDeleteTarget(over),
|
|
228
|
+
[setOverDeleteTarget],
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
const onGestureFinished = useCallback(
|
|
232
|
+
(t: Transform, deleted: boolean) => {
|
|
233
|
+
const { dispatch: d, store: s } = latest.current
|
|
234
|
+
const id = grabbedId.current
|
|
235
|
+
const from = grabbedFrom.current
|
|
236
|
+
grabbedId.current = null
|
|
237
|
+
grabbedFrom.current = null
|
|
238
|
+
snapState.current = NO_SNAP
|
|
239
|
+
setLayerDragActive(false)
|
|
240
|
+
setOverDeleteTarget(false)
|
|
241
|
+
setSnap(NO_SNAP)
|
|
242
|
+
setPreview(null)
|
|
243
|
+
if (!id) return
|
|
244
|
+
if (deleted) {
|
|
245
|
+
d({ type: 'layer/remove', id })
|
|
246
|
+
s.select([])
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
// a tap, or a drag that ended where it started, must not create an undo entry
|
|
250
|
+
if (from && isSameTransform(from, t)) return
|
|
251
|
+
d({ type: 'layer/transform', id, patch: t })
|
|
252
|
+
},
|
|
253
|
+
[setLayerDragActive, setOverDeleteTarget, setSnap, setPreview],
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
useEffect(() => () => setPreview(null), [setPreview])
|
|
257
|
+
|
|
258
|
+
const gesture = useMemo(() => {
|
|
259
|
+
const metricsWidth = metrics?.width ?? 0
|
|
260
|
+
const metricsHeight = metrics?.height ?? 0
|
|
261
|
+
const target: DeleteTarget | null = deleteTarget
|
|
262
|
+
const candidates = hitCandidates
|
|
263
|
+
|
|
264
|
+
/** Re-capture the frame base and zero every recognizer origin. */
|
|
265
|
+
const rebase = () => {
|
|
266
|
+
'worklet'
|
|
267
|
+
baseX.value = liveX.value
|
|
268
|
+
baseY.value = liveY.value
|
|
269
|
+
baseScale.value = liveScale.value
|
|
270
|
+
baseRotation.value = liveRotation.value
|
|
271
|
+
originPanX.value = rawPanX.value
|
|
272
|
+
originPanY.value = rawPanY.value
|
|
273
|
+
originPinch.value = rawPinch.value
|
|
274
|
+
originRotate.value = rawRotate.value
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const recompute = (settle: boolean) => {
|
|
278
|
+
'worklet'
|
|
279
|
+
if (!active.value || metricsWidth === 0 || metricsHeight === 0) return
|
|
280
|
+
const out = composeLayerGestureUI(
|
|
281
|
+
{
|
|
282
|
+
x: baseX.value,
|
|
283
|
+
y: baseY.value,
|
|
284
|
+
scale: baseScale.value,
|
|
285
|
+
rotation: baseRotation.value,
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
panX: (rawPanX.value - originPanX.value) / metricsWidth,
|
|
289
|
+
panY: (rawPanY.value - originPanY.value) / metricsHeight,
|
|
290
|
+
pinch: rawPinch.value / Math.max(originPinch.value, 1e-9),
|
|
291
|
+
rotate: rawRotate.value - originRotate.value,
|
|
292
|
+
focalX: focalX.value,
|
|
293
|
+
focalY: focalY.value,
|
|
294
|
+
},
|
|
295
|
+
aspect,
|
|
296
|
+
config,
|
|
297
|
+
{ centerX: snapCX.value, centerY: snapCY.value, rotation: snapR.value },
|
|
298
|
+
settle,
|
|
299
|
+
)
|
|
300
|
+
liveX.value = out.x
|
|
301
|
+
liveY.value = out.y
|
|
302
|
+
liveScale.value = out.scale
|
|
303
|
+
liveRotation.value = out.rotation
|
|
304
|
+
|
|
305
|
+
if (
|
|
306
|
+
out.snapCenterX !== snapCX.value ||
|
|
307
|
+
out.snapCenterY !== snapCY.value ||
|
|
308
|
+
out.snapRotation !== snapR.value
|
|
309
|
+
) {
|
|
310
|
+
snapCX.value = out.snapCenterX
|
|
311
|
+
snapCY.value = out.snapCenterY
|
|
312
|
+
snapR.value = out.snapRotation
|
|
313
|
+
runOnJS(onSnapChanged)(out.snapCenterX, out.snapCenterY, out.snapRotation)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (target) {
|
|
317
|
+
const over = isOverDeleteTargetUI(out.x, out.y, target, aspect)
|
|
318
|
+
if (over !== overDelete.value) {
|
|
319
|
+
overDelete.value = over
|
|
320
|
+
runOnJS(onHoverChanged)(over)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Track pointer count and re-base the frame whenever it changes. */
|
|
326
|
+
const syncPointers = (count: number) => {
|
|
327
|
+
'worklet'
|
|
328
|
+
if (active.value && count !== pointers.value) {
|
|
329
|
+
pointers.value = count
|
|
330
|
+
rebase()
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const endIfIdle = () => {
|
|
335
|
+
'worklet'
|
|
336
|
+
if (!active.value) {
|
|
337
|
+
// an inert gesture (nothing was grabbed) still has to clear UI state
|
|
338
|
+
runOnJS(onGestureFinished)({ x: 0, y: 0, scale: 1, rotation: 0 }, false)
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
recompute(true)
|
|
342
|
+
active.value = false
|
|
343
|
+
pointers.value = 0
|
|
344
|
+
const deleted = overDelete.value
|
|
345
|
+
overDelete.value = false
|
|
346
|
+
snapCX.value = false
|
|
347
|
+
snapCY.value = false
|
|
348
|
+
snapR.value = false
|
|
349
|
+
runOnJS(onGestureFinished)(
|
|
350
|
+
{
|
|
351
|
+
x: liveX.value,
|
|
352
|
+
y: liveY.value,
|
|
353
|
+
scale: liveScale.value,
|
|
354
|
+
rotation: liveRotation.value,
|
|
355
|
+
},
|
|
356
|
+
deleted,
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const pan = Gesture.Pan()
|
|
361
|
+
.minPointers(1)
|
|
362
|
+
.maxPointers(2)
|
|
363
|
+
.onBegin((e) => {
|
|
364
|
+
'worklet'
|
|
365
|
+
if (active.value) return
|
|
366
|
+
rawPanX.value = 0
|
|
367
|
+
rawPanY.value = 0
|
|
368
|
+
rawPinch.value = 1
|
|
369
|
+
rawRotate.value = 0
|
|
370
|
+
originPanX.value = 0
|
|
371
|
+
originPanY.value = 0
|
|
372
|
+
originPinch.value = 1
|
|
373
|
+
originRotate.value = 0
|
|
374
|
+
pointers.value = e.numberOfPointers
|
|
375
|
+
const hit = hitTestLayerCandidatesUI(
|
|
376
|
+
e.x / Math.max(metricsWidth, 1e-9),
|
|
377
|
+
e.y / Math.max(metricsHeight, 1e-9),
|
|
378
|
+
candidates,
|
|
379
|
+
document.size.width,
|
|
380
|
+
document.size.height,
|
|
381
|
+
)
|
|
382
|
+
if (!hit) {
|
|
383
|
+
active.value = false
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
liveX.value = hit.x
|
|
387
|
+
liveY.value = hit.y
|
|
388
|
+
liveScale.value = hit.scale
|
|
389
|
+
liveRotation.value = hit.rotation
|
|
390
|
+
committedX.value = hit.x
|
|
391
|
+
committedY.value = hit.y
|
|
392
|
+
committedScale.value = hit.scale
|
|
393
|
+
committedRotation.value = hit.rotation
|
|
394
|
+
baseX.value = hit.x
|
|
395
|
+
baseY.value = hit.y
|
|
396
|
+
baseScale.value = hit.scale
|
|
397
|
+
baseRotation.value = hit.rotation
|
|
398
|
+
focalX.value = hit.x
|
|
399
|
+
focalY.value = hit.y
|
|
400
|
+
active.value = true
|
|
401
|
+
runOnJS(onGrabLayer)(hit.id)
|
|
402
|
+
})
|
|
403
|
+
.onUpdate((e) => {
|
|
404
|
+
'worklet'
|
|
405
|
+
rawPanX.value = e.translationX
|
|
406
|
+
rawPanY.value = e.translationY
|
|
407
|
+
syncPointers(e.numberOfPointers)
|
|
408
|
+
recompute(false)
|
|
409
|
+
})
|
|
410
|
+
.onFinalize(() => {
|
|
411
|
+
'worklet'
|
|
412
|
+
endIfIdle()
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
const pinch = Gesture.Pinch().onUpdate((e) => {
|
|
416
|
+
'worklet'
|
|
417
|
+
rawPinch.value = e.scale
|
|
418
|
+
focalX.value = e.focalX / metricsWidth
|
|
419
|
+
focalY.value = e.focalY / metricsHeight
|
|
420
|
+
syncPointers(e.numberOfPointers)
|
|
421
|
+
recompute(false)
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
const rotate = Gesture.Rotation()
|
|
425
|
+
.enabled(config.rotationEnabled)
|
|
426
|
+
.onUpdate((e) => {
|
|
427
|
+
'worklet'
|
|
428
|
+
rawRotate.value = e.rotation
|
|
429
|
+
focalX.value = e.anchorX / metricsWidth
|
|
430
|
+
focalY.value = e.anchorY / metricsHeight
|
|
431
|
+
syncPointers(e.numberOfPointers)
|
|
432
|
+
recompute(false)
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
const tap = Gesture.Tap().onEnd((e) => {
|
|
436
|
+
'worklet'
|
|
437
|
+
runOnJS(onSelectAt)(e.x, e.y)
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
return Gesture.Simultaneous(Gesture.Race(pan, tap), pinch, rotate)
|
|
441
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
442
|
+
}, [
|
|
443
|
+
metrics,
|
|
444
|
+
config,
|
|
445
|
+
deleteTarget,
|
|
446
|
+
aspect,
|
|
447
|
+
onGrabLayer,
|
|
448
|
+
onSelectAt,
|
|
449
|
+
onSnapChanged,
|
|
450
|
+
onHoverChanged,
|
|
451
|
+
onGestureFinished,
|
|
452
|
+
hitCandidates,
|
|
453
|
+
document.size.width,
|
|
454
|
+
document.size.height,
|
|
455
|
+
])
|
|
456
|
+
|
|
457
|
+
return (
|
|
458
|
+
<GestureDetector gesture={gesture}>
|
|
459
|
+
<Animated.View style={StyleSheet.absoluteFill} />
|
|
460
|
+
</GestureDetector>
|
|
461
|
+
)
|
|
462
|
+
}
|