@pictor-sdk/ui 0.1.0 → 0.2.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/README.md +12 -1
- package/dist/index.d.mts +34 -2
- package/dist/index.d.ts +34 -2
- package/dist/index.js +76 -34
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +74 -34
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +1 -0
- package/src/panels/LayoutPanel.tsx +11 -5
- package/src/panels/layoutMath.ts +86 -32
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pictor-sdk/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Default slot-based editor UI for Pictor, built entirely on the public headless API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"react-native-gesture-handler": ">=2.20.0",
|
|
34
34
|
"react-native-reanimated": ">=3.16.0",
|
|
35
35
|
"react-native-worklets": ">=0.5.1",
|
|
36
|
-
"@pictor-sdk/core": "^0.
|
|
37
|
-
"@pictor-sdk/skia": "^0.
|
|
36
|
+
"@pictor-sdk/core": "^0.2.0",
|
|
37
|
+
"@pictor-sdk/skia": "^0.2.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@shopify/react-native-skia": "^2.0.0",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"react-native-gesture-handler": "^2.28.0",
|
|
45
45
|
"react-native-reanimated": "^4.1.0",
|
|
46
46
|
"react-native-worklets": "0.5.1",
|
|
47
|
-
"@pictor-sdk/core": "0.
|
|
48
|
-
"@pictor-sdk/skia": "0.
|
|
47
|
+
"@pictor-sdk/core": "0.2.0",
|
|
48
|
+
"@pictor-sdk/skia": "0.2.0"
|
|
49
49
|
},
|
|
50
50
|
"publishConfig": {
|
|
51
51
|
"access": "public",
|
package/src/index.ts
CHANGED
|
@@ -67,6 +67,7 @@ export { StickerPanel, type StickerAsset } from './panels/StickerPanel'
|
|
|
67
67
|
export { LayersPanel } from './panels/LayersPanel'
|
|
68
68
|
export { LayoutPanel } from './panels/LayoutPanel'
|
|
69
69
|
export { layoutCells, cellToTransformPatch, type LayoutCell, type LayoutPreset } from './panels/layoutMath'
|
|
70
|
+
export { layoutCellBounds, fitImageToLayoutCell, type LayoutCropOptions, type LayoutImageFit } from './panels/layoutMath'
|
|
70
71
|
export { DrawingSurface, DEFAULT_BRUSH } from './gestures/DrawingSurface'
|
|
71
72
|
export { createDrawController, type BrushConfig, type DrawController } from './gestures/drawController'
|
|
72
73
|
export { UiStateProvider, useUiState, createUiStateStore, type UiStateStore } from './uiState'
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { useEditor } from '@pictor-sdk/core'
|
|
1
|
+
import { useEditor, type ImageLayerData } from '@pictor-sdk/core'
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import { Pressable, ScrollView, Text } from 'react-native'
|
|
4
4
|
import { useTheme } from '../theme'
|
|
5
|
-
import {
|
|
5
|
+
import { fitImageToLayoutCell, layoutCellBounds, type LayoutPreset } from './layoutMath'
|
|
6
6
|
|
|
7
7
|
const PRESETS: Array<{ id: LayoutPreset; label: string }> = [
|
|
8
8
|
{ id: '1', label: 'Single' },
|
|
@@ -10,6 +10,7 @@ const PRESETS: Array<{ id: LayoutPreset; label: string }> = [
|
|
|
10
10
|
{ id: '2h', label: '2 cols' },
|
|
11
11
|
{ id: '3v', label: '3 rows' },
|
|
12
12
|
{ id: '4grid', label: 'Grid' },
|
|
13
|
+
{ id: '6grid', label: '6 grid' },
|
|
13
14
|
]
|
|
14
15
|
|
|
15
16
|
/** Layout grids: snap the image layers into the chosen arrangement. */
|
|
@@ -18,11 +19,16 @@ export function LayoutPanel() {
|
|
|
18
19
|
const theme = useTheme()
|
|
19
20
|
|
|
20
21
|
const apply = (preset: LayoutPreset) => {
|
|
21
|
-
const cells =
|
|
22
|
+
const cells = layoutCellBounds(preset)
|
|
22
23
|
const images = document.layers.filter((l) => l.type === 'image')
|
|
24
|
+
// Calculate before opening history: invalid intrinsic data cannot leave a batch open.
|
|
25
|
+
const fits = images.slice(0, cells.length).map((layer, i) => ({
|
|
26
|
+
layer, fit: fitImageToLayoutCell(cells[i]!, document.size, (layer.data as ImageLayerData).intrinsic),
|
|
27
|
+
}))
|
|
23
28
|
dispatch({ type: 'history/beginBatch', label: 'layout' })
|
|
24
|
-
|
|
25
|
-
dispatch({ type: 'layer/transform', id: layer.id, patch:
|
|
29
|
+
fits.forEach(({ layer, fit }) => {
|
|
30
|
+
dispatch({ type: 'layer/transform', id: layer.id, patch: fit.transform })
|
|
31
|
+
dispatch({ type: 'layer/patchData', id: layer.id, patch: { crop: fit.crop, pixelSnap: fit.pixelSnap } })
|
|
26
32
|
})
|
|
27
33
|
dispatch({ type: 'history/endBatch' })
|
|
28
34
|
}
|
package/src/panels/layoutMath.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { type Size, type Transform } from '@pictor-sdk/core'
|
|
1
|
+
import { type Rect, type Size, type Transform } from '@pictor-sdk/core'
|
|
2
|
+
import { centeredCropForAspect } from './cropMath'
|
|
2
3
|
|
|
3
4
|
/** The bundled multi-photo arrangements, by cell count and split direction. */
|
|
4
|
-
export type LayoutPreset = '1' | '2v' | '2h' | '3v' | '4grid'
|
|
5
|
+
export type LayoutPreset = '1' | '2v' | '2h' | '3v' | '4grid' | '6grid'
|
|
5
6
|
|
|
6
7
|
/** One slot in a layout: where its photo sits, in normalized document units. */
|
|
7
8
|
export interface LayoutCell {
|
|
@@ -18,38 +19,91 @@ export interface LayoutCell {
|
|
|
18
19
|
* canvas size works.
|
|
19
20
|
*/
|
|
20
21
|
export function layoutCells(preset: LayoutPreset, _doc: Size): LayoutCell[] {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
22
|
+
return layoutCellBounds(preset).map((cell) => ({
|
|
23
|
+
x: (cell.x / cell.width + 0.5) * cell.width,
|
|
24
|
+
y: (cell.y / cell.height + 0.5) * cell.height,
|
|
25
|
+
scale: cell.width,
|
|
26
|
+
}))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Axis-aligned cell rectangles, row-major, normalized to document width/height.
|
|
31
|
+
* x/y are TOP-LEFT coordinates, unlike the centres in legacy LayoutCell.
|
|
32
|
+
* Returns fresh rectangles; no document aspect ratio is assumed.
|
|
33
|
+
*/
|
|
34
|
+
export function layoutCellBounds(preset: LayoutPreset): Rect[] {
|
|
35
|
+
const dimensions: Record<LayoutPreset, readonly [number, number]> = {
|
|
36
|
+
'1': [1, 1], '2v': [1, 2], '2h': [2, 1], '3v': [1, 3],
|
|
37
|
+
'4grid': [2, 2], '6grid': [2, 3],
|
|
38
|
+
}
|
|
39
|
+
const grid = dimensions[preset]
|
|
40
|
+
if (!grid) throw new RangeError('Unknown layout preset')
|
|
41
|
+
const [columns, rows] = grid
|
|
42
|
+
return Array.from({ length: columns * rows }, (_, index) => ({
|
|
43
|
+
x: (index % columns) / columns, y: Math.floor(index / columns) / rows,
|
|
44
|
+
width: 1 / columns, height: 1 / rows,
|
|
45
|
+
}))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Source-window controls; pan is a source-space crop centre, not a layer translation. */
|
|
49
|
+
export interface LayoutCropOptions {
|
|
50
|
+
/** Magnification relative to crop-to-fill. Clamped to [1, 32]; default 1. */
|
|
51
|
+
zoom?: number
|
|
52
|
+
/** Desired source-normalized crop centre; clamped so the crop stays in the source. */
|
|
53
|
+
center?: { x: number; y: number }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Crop and complete transform to apply together in one history batch. */
|
|
57
|
+
export interface LayoutImageFit {
|
|
58
|
+
crop: Rect
|
|
59
|
+
transform: Transform
|
|
60
|
+
/** Apply to ImageLayerData together with crop for seam-free cell edges. */
|
|
61
|
+
pixelSnap: true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Fill a fixed cell without bleed. Zoom/pan alter only the source crop, never
|
|
66
|
+
* the cell bounds. intrinsic must describe the decoded, orientation-corrected
|
|
67
|
+
* source. Recompute after source replacement or document resize.
|
|
68
|
+
* Throws RangeError for non-finite controls, invalid sizes or out-of-document cells.
|
|
69
|
+
* Apply transform with layer/transform and crop + pixelSnap with layer/patchData in one batch.
|
|
70
|
+
* General layer gestures must not subsequently move/rotate/scale a fixed cell.
|
|
71
|
+
*/
|
|
72
|
+
export function fitImageToLayoutCell(
|
|
73
|
+
cell: Rect, documentSize: Size, intrinsic: Size, options: LayoutCropOptions = {},
|
|
74
|
+
): LayoutImageFit {
|
|
75
|
+
for (const size of [documentSize, intrinsic]) {
|
|
76
|
+
if (![size.width, size.height].every((v) => Number.isFinite(v) && v > 0)) {
|
|
77
|
+
throw new RangeError('Layout sizes must be finite and positive')
|
|
51
78
|
}
|
|
52
79
|
}
|
|
80
|
+
if (![cell.x, cell.y, cell.width, cell.height].every(Number.isFinite) ||
|
|
81
|
+
cell.x < 0 || cell.y < 0 || cell.width <= 0 || cell.height <= 0 ||
|
|
82
|
+
cell.x + cell.width > 1 + 1e-12 || cell.y + cell.height > 1 + 1e-12) {
|
|
83
|
+
throw new RangeError('Layout cell must lie inside the document')
|
|
84
|
+
}
|
|
85
|
+
const zoom = options.zoom ?? 1
|
|
86
|
+
const center = options.center ?? { x: 0.5, y: 0.5 }
|
|
87
|
+
if (![zoom, center.x, center.y].every(Number.isFinite)) {
|
|
88
|
+
throw new RangeError('Layout controls must be finite')
|
|
89
|
+
}
|
|
90
|
+
const base = centeredCropForAspect(intrinsic,
|
|
91
|
+
(cell.width * documentSize.width) / (cell.height * documentSize.height))
|
|
92
|
+
const factor = Math.max(1, Math.min(32, zoom))
|
|
93
|
+
const width = base.width / factor
|
|
94
|
+
const height = base.height / factor
|
|
95
|
+
return {
|
|
96
|
+
pixelSnap: true,
|
|
97
|
+
crop: {
|
|
98
|
+
x: Math.max(0, Math.min(1 - width, center.x - width / 2)),
|
|
99
|
+
y: Math.max(0, Math.min(1 - height, center.y - height / 2)), width, height,
|
|
100
|
+
},
|
|
101
|
+
transform: {
|
|
102
|
+
x: cell.x + cell.width / 2, y: cell.y + cell.height / 2,
|
|
103
|
+
scale: cell.width, rotation: 0, anchor: { x: 0.5, y: 0.5 },
|
|
104
|
+
flipX: false, flipY: false,
|
|
105
|
+
},
|
|
106
|
+
}
|
|
53
107
|
}
|
|
54
108
|
|
|
55
109
|
/** The transform patch that moves a layer into a cell, leaving its other fields alone. */
|