Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { useMemo } from '@wordpress/element';
import { DEFAULT_ASPECT_RATIOS } from '../../image-editor/core/constants';
import type { AspectRatioPreset } from '../../image-editor/core/constants';
import { useMediaEditor, resolveAspectRatio } from '../../state';
import {
DEFAULT_ASPECT_RATIOS,
useMediaEditor,
resolveAspectRatio,
} from '../../state';

interface UseCropOptionsArgs {
aspectRatioPresets?: AspectRatioPreset[];
Expand Down
14 changes: 7 additions & 7 deletions packages/media-editor/src/image-editor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
The image editor is the cropper engine used by the media editor:

- **Core:** state, camera math, containment, source-region, transforms, and canvas export.
- **React:** `<Cropper>`, `useCropperReducer`, and the optional `CropperProvider` / `useCropper` context wrapper.
- **React:** `<Cropper>`, `useCropperController`, and the optional `CropperProvider` / `useCropper` context wrapper.

`useCropperReducer` does not provide undo/redo. The media editor's composite state layer adds history and sidebar crop options on top of the cropper reducer.
`useCropperController` does not provide undo/redo. The media editor's composite state layer adds history and sidebar crop options on top of the cropper reducer, through `image-editor/internals`, which is not part of the contract.

## Quick Start

```tsx
import { Cropper, useCropperReducer } from '../image-editor';
import { Cropper, useCropperController } from '../image-editor';

function ImageCropper() {
const controller = useCropperReducer();
const controller = useCropperController();

return (
<div style={ { width: 600, height: 400 } }>
Expand All @@ -31,21 +31,21 @@ function ImageCropper() {
}
```

`useCropperReducer` returns a controller object with the current `state`, named setters, `reset`, `isDirty`, and `getCroppedImage`.
`useCropperController` returns a controller object with the current `state`, named setters, `reset`, `isDirty`, and `getCroppedImage`.

## Internal APIs

### `Cropper`

Fills its parent container. Required props are `src` and `controller`. Common options include `freeformCrop`, `aspectRatio`, `showGrid`, `showDimming`, `showDimensions`, `onGestureStart`, and `onGestureEnd`.

### `useCropperReducer( initialState? )`
### `useCropperController( initialState? )`

Pure React state hook for the cropper reducer. Returns a `CropperController`: current state, named setters, `reset`, `isDirty`, and `getCroppedImage`.

### `CropperProvider` / `useCropper`

Convenience context wrapper around `useCropperReducer` for component trees that need shared cropper state.
Convenience context wrapper around `useCropperController` for component trees that need shared cropper state.

## Core Helpers

Expand Down
13 changes: 0 additions & 13 deletions packages/media-editor/src/image-editor/core/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { __ } from '@wordpress/i18n';
import type { NormalizedRect, Flip, CropperState } from './types';

/**
Expand Down Expand Up @@ -136,15 +135,3 @@ export interface AspectRatioPreset {
* to the image's natural width / height.
*/
export const ORIGINAL_ASPECT_RATIO = -1;

export const DEFAULT_ASPECT_RATIOS: AspectRatioPreset[] = [
{ label: __( 'Free' ), value: 0 },
{ label: __( 'Original' ), value: ORIGINAL_ASPECT_RATIO },
{ label: __( 'Square (1:1)' ), value: 1 },
{ label: __( 'Landscape (16:9)' ), value: 16 / 9 },
{ label: __( 'Portrait (9:16)' ), value: 9 / 16 },
{ label: __( 'Classic (4:3)' ), value: 4 / 3 },
{ label: __( 'Classic portrait (3:4)' ), value: 3 / 4 },
{ label: __( 'Photo (3:2)' ), value: 3 / 2 },
{ label: __( 'Photo portrait (2:3)' ), value: 2 / 3 },
];
6 changes: 1 addition & 5 deletions packages/media-editor/src/image-editor/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ export type {
} from './types';

// Constants
export {
DEFAULT_STATE,
DEFAULT_ASPECT_RATIOS,
ORIGINAL_ASPECT_RATIO,
} from './constants';
export { DEFAULT_STATE, ORIGINAL_ASPECT_RATIO } from './constants';
export type { AspectRatioPreset } from './constants';

// Reducer + state helpers. `CropperAction` and `cropperReducer` are
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { getMinZoom, restrictPanZoom } from './containment';
* the crop center. Returns `null` when the clamped zoom matches the
* current zoom (no-op).
*
* Extracted so multiple hooks (the pure `useCropperReducer` and the
* Extracted so multiple hooks (the pure `useCropperController` and the
* composite `useMediaEditorState`) can share the math without
* duplicating it.
*
Expand Down
2 changes: 1 addition & 1 deletion packages/media-editor/src/image-editor/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export interface CropperState {
*
* This type is intentionally not exported from the image-editor public barrel.
* Consumers should drive state through the controller returned by
* `useCropperReducer()` (or a composite store) or through serializable
* `useCropperController()` (or a composite store) or through serializable
* `TransformOperation` values.
*/
export type CropperAction =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
| Layer | Files | Responsibility |
| --- | --- | --- |
| Core cropper | `src/image-editor/core/` | Pure state, camera math, containment, source-region, transforms, and canvas export. |
| React cropper | `src/image-editor/react/` | DOM measurement, interaction hooks, overlays, stencil rendering, and the pure `useCropperReducer` hook. |
| React cropper | `src/image-editor/react/` | DOM measurement, interaction hooks, overlays, stencil rendering, and the pure `useCropperController` hook. |
| Media editor state | `src/state/` | Composite state for the media editor: cropper state, crop options, undo/redo, and gesture boundaries. |

The cropper layer does not own editor history. History belongs to the media editor session because sidebar controls and cropper gestures need one shared undo stack.
Expand Down
6 changes: 3 additions & 3 deletions packages/media-editor/src/image-editor/docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
## Mount a Cropper

```tsx
import { Cropper, useCropperReducer } from '../image-editor';
import { Cropper, useCropperController } from '../image-editor';

function ImageCropper( { src }: { src: string } ) {
const controller = useCropperReducer();
const controller = useCropperController();

return (
<div style={ { width: 600, height: 400 } }>
Expand All @@ -30,7 +30,7 @@ The cropper fills its parent. Always render it inside a sized container.

```tsx
function ImageCropperWithControls( { src }: { src: string } ) {
const controller = useCropperReducer();
const controller = useCropperController();
const { state, setZoom, snapRotate90, toggleFlip, reset } = controller;

return (
Expand Down
23 changes: 7 additions & 16 deletions packages/media-editor/src/image-editor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@
*
* Keep this barrel explicit so implementation details from `core/`,
* `react/hooks/`, or `react/components/` do not become supported API by
* accident. Internal modules may export additional helpers for local tests or
* React wiring; consumers should import through this file.
* accident. What a composite store needs to delegate cropper actions
* through the pure reducer is in `./internals`; a consumer drives the
* cropper through the controller and the helpers here.
*/

// React surface.
export { useCropperReducer, buildCropperSetters } from './react/hooks';
export { useCropperController } from './react/hooks';
export { Cropper, CropperProvider, useCropper } from './react/components';
export type { CropperController, CropperSetters } from './react/hooks';
export type { CropperController } from './react/hooks';
export type { CropperProps } from './react/components';

// State and extension types.
export type {
CropperState,
CropperAction,
TransformOperation,
NormalizedPoint,
NormalizedRect,
Expand All @@ -26,21 +26,12 @@ export type {
HandlePosition,
} from './core';

// Deterministic cropper helpers. The reducer + containment + state
// equality helpers are exported so consumers can build composite
// stores that delegate cropper actions (see `useMediaEditorState` for
// the media editor's composite store).
// Deterministic helpers: where the crop lands in the source, the
// replayable operation list, and export without a controller.
export {
DEFAULT_STATE,
DEFAULT_ASPECT_RATIOS,
ORIGINAL_ASPECT_RATIO,
cropperReducer,
enforceContainment,
areCropperStatesEqual,
computeInscribedRect,
getSourceRegion,
getSourceRegionPercent,
applyOperationToState,
stateFromPipeline,
exportCroppedImage,
applyToCanvas,
Expand Down
18 changes: 18 additions & 0 deletions packages/media-editor/src/image-editor/internals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* The cropper's internals, for a composite store that delegates cropper
* actions through the pure reducer, the way the media editor's session
* does. Not part of the consumer contract: a consumer drives the cropper
* through the controller and the helpers in `./index`.
*/
export {
DEFAULT_STATE,
cropperReducer,
enforceContainment,
areCropperStatesEqual,
isStateDirty,
applyOperationToState,
computeInscribedRect,
} from './core';
export type { CropperAction } from './core';
export { buildCropperSetters } from './react/hooks';
export type { CropperSetters } from './react/hooks';
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { createContext, useContext } from '@wordpress/element';
import {
useCropperReducer,
useCropperController,
type CropperController,
} from '../hooks/use-cropper-reducer';
} from '../hooks/use-cropper-controller';
import type { CropperState } from '../../core/types';

/**
* The context value type for the CropperProvider.
* Contains the cropper controller from useCropperReducer.
* Contains the cropper controller from useCropperController.
*/
type CropperContextValue = CropperController | null;

Expand All @@ -24,7 +24,7 @@ interface CropperProviderProps {
}

/**
* Convenience context provider that wraps `useCropperReducer`.
* Convenience context provider that wraps `useCropperController`.
*
* Provides the pure cropper controller (state + setters, NO undo
* history) to all descendant components via React context.
Expand All @@ -42,7 +42,7 @@ export function CropperProvider( {
initialState,
children,
}: CropperProviderProps ) {
const cropperReturn = useCropperReducer( initialState );
const cropperReturn = useCropperController( initialState );

return (
<CropperContext.Provider value={ cropperReturn }>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type {
Size,
NormalizedRect,
} from '../../core/types';
import type { CropperController } from '../hooks/use-cropper-reducer';
import type { CropperController } from '../hooks/use-cropper-controller';
import { getImageFit, getRotatedBBox, getViewScale } from '../../core/camera';
import { getImageCropBounds, getMinZoom } from '../../core/containment';
import {
Expand Down Expand Up @@ -66,7 +66,7 @@ const SETTLE_STENCIL_TRANSITION = [
export interface CropperProps {
/** Image source URL. */
src: string;
/** The cropper controller from `useCropperReducer` or a composite store. */
/** The cropper controller from `useCropperController` or a composite store. */
controller: CropperController;
/** Stencil component for the crop area. Defaults to RectangleStencil. */
stencil?: React.ComponentType< StencilProps >;
Expand Down Expand Up @@ -132,7 +132,7 @@ export interface CropperProps {
*
* @param root0 Component props implementing CropperProps.
* @param root0.src Image source URL.
* @param root0.controller The cropper controller from `useCropperReducer` or a composite store.
* @param root0.controller The cropper controller from `useCropperController` or a composite store.
* @param root0.stencil Custom stencil component.
* @param root0.showGrid Grid overlay mode: false | true | 'interactive'.
* @param root0.isPlacementActive Keep grid visible during external placement activity.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { userEvent } from 'vitest/browser';
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
import { render } from 'vitest-browser-react';
import { Cropper } from '../cropper';
import type { CropperController } from '../../hooks/use-cropper-reducer';
import type { CropperController } from '../../hooks/use-cropper-controller';
import { DEFAULT_STATE } from '../../../core/constants';
import { getSourceRegion } from '../../../core/source-region';
// Browser Mode needs the package's real styles for layout and transitions.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { buildFocalPointZoomAction } from '../../core/setter-helpers';

/**
* The named cropper setters shared by every hook that drives the
* cropper reducer — the pure `useCropperReducer` and any composite
* cropper reducer — the pure `useCropperController` and any composite
* store that delegates cropper actions (e.g. `useMediaEditorState`).
*
* `setImage` and `reset` are intentionally NOT included: each hook
Expand All @@ -34,7 +34,7 @@ export interface CropperSetters {
* Build the named cropper setters from a dispatch function and a
* state getter. The two consuming hooks differ in how they dispatch:
*
* - `useCropperReducer` dispatches directly into its `useReducer`.
* - `useCropperController` dispatches directly into its `useReducer`.
* - `useMediaEditorState` wraps cropper actions in a composite
* `CROPPER` envelope so the history machinery can record them.
*
Expand Down
4 changes: 2 additions & 2 deletions packages/media-editor/src/image-editor/react/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { useCropperReducer } from './use-cropper-reducer';
export type { CropperController } from './use-cropper-reducer';
export { useCropperController } from './use-cropper-controller';
export type { CropperController } from './use-cropper-controller';
export { buildCropperSetters } from './build-cropper-setters';
export type { CropperSetters } from './build-cropper-setters';
Loading
Loading