@mirasen/chessboard 1.2.4 → 1.3.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +9 -0
  3. package/dist/animation/planner.js +2 -1
  4. package/dist/extensions/build/index.d.ts +1 -1
  5. package/dist/extensions/first-party/annotations/update.js +1 -1
  6. package/dist/extensions/first-party/board-events/factory.js +2 -2
  7. package/dist/extensions/first-party/main-renderer/animation/factory.d.ts +2 -1
  8. package/dist/extensions/first-party/main-renderer/animation/factory.js +3 -2
  9. package/dist/extensions/first-party/main-renderer/animation/types.d.ts +2 -0
  10. package/dist/extensions/first-party/main-renderer/animation/update.js +4 -2
  11. package/dist/extensions/first-party/main-renderer/board/factory.d.ts +1 -1
  12. package/dist/extensions/first-party/main-renderer/board/factory.js +4 -4
  13. package/dist/extensions/first-party/main-renderer/board/render.js +1 -1
  14. package/dist/extensions/first-party/main-renderer/board/types.d.ts +1 -1
  15. package/dist/extensions/first-party/main-renderer/coordinates/factory.d.ts +1 -1
  16. package/dist/extensions/first-party/main-renderer/coordinates/factory.js +2 -2
  17. package/dist/extensions/first-party/main-renderer/coordinates/render.js +1 -1
  18. package/dist/extensions/first-party/main-renderer/coordinates/types.d.ts +1 -1
  19. package/dist/extensions/first-party/main-renderer/denormalize.d.ts +3 -0
  20. package/dist/extensions/first-party/main-renderer/denormalize.js +18 -0
  21. package/dist/extensions/first-party/main-renderer/drag/factory.d.ts +2 -1
  22. package/dist/extensions/first-party/main-renderer/drag/factory.js +2 -1
  23. package/dist/extensions/first-party/main-renderer/drag/render.js +18 -11
  24. package/dist/extensions/first-party/main-renderer/drag/types.d.ts +2 -0
  25. package/dist/extensions/first-party/main-renderer/drag/update.js +4 -2
  26. package/dist/extensions/first-party/main-renderer/factory.d.ts +2 -1
  27. package/dist/extensions/first-party/main-renderer/factory.js +46 -12
  28. package/dist/extensions/first-party/main-renderer/normalize.d.ts +3 -3
  29. package/dist/extensions/first-party/main-renderer/normalize.js +19 -7
  30. package/dist/extensions/first-party/main-renderer/pieces/suppress.js +2 -1
  31. package/dist/extensions/first-party/main-renderer/public-defaults.d.ts +3 -0
  32. package/dist/extensions/first-party/main-renderer/public-defaults.js +4 -0
  33. package/dist/extensions/first-party/main-renderer/types/extension.d.ts +8 -5
  34. package/dist/extensions/first-party/main-renderer/types/instance.d.ts +10 -8
  35. package/dist/extensions/first-party/main-renderer/types/internal.d.ts +6 -7
  36. package/dist/extensions/first-party/main-renderer/types/internal.js +24 -1
  37. package/dist/extensions/first-party/main-renderer/types/public.d.ts +9 -0
  38. package/dist/extensions/first-party/main-renderer/types/template.d.ts +12 -1
  39. package/dist/extensions/first-party/promotion/types/input.d.ts +2 -2
  40. package/dist/extensions/index.d.ts +1 -0
  41. package/dist/extensions/index.js +1 -0
  42. package/dist/extensions/types/basic/events.d.ts +30 -16
  43. package/dist/extensions/types/basic/interaction.d.ts +12 -2
  44. package/dist/extensions/types/surface/commands.d.ts +4 -2
  45. package/dist/runtime/factory/input.js +50 -24
  46. package/dist/runtime/factory/main.js +11 -0
  47. package/dist/runtime/input/adapter/types.js +2 -1
  48. package/dist/runtime/input/controller/factory.js +36 -9
  49. package/dist/runtime/input/controller/helpers.d.ts +5 -0
  50. package/dist/runtime/input/controller/helpers.js +11 -0
  51. package/dist/runtime/input/controller/pointer.js +48 -21
  52. package/dist/runtime/input/controller/types.d.ts +13 -4
  53. package/dist/runtime/mutation/interaction.js +1 -1
  54. package/dist/runtime/mutation/types.d.ts +2 -2
  55. package/dist/state/interaction/config.d.ts +6 -0
  56. package/dist/state/interaction/config.js +30 -0
  57. package/dist/state/interaction/factory.js +15 -3
  58. package/dist/state/interaction/helpers.d.ts +18 -2
  59. package/dist/state/interaction/helpers.js +12 -0
  60. package/dist/state/interaction/mutation.d.ts +2 -0
  61. package/dist/state/interaction/reducers.d.ts +3 -0
  62. package/dist/state/interaction/reducers.js +22 -7
  63. package/dist/state/interaction/types/config.d.ts +9 -0
  64. package/dist/state/interaction/types/config.js +1 -0
  65. package/dist/state/interaction/types/input.d.ts +3 -0
  66. package/dist/state/interaction/types/internal.d.ts +14 -4
  67. package/dist/state/interaction/types/main.d.ts +11 -5
  68. package/dist/wrapper/factory.js +2 -0
  69. package/dist/wrapper/types.d.ts +1 -1
  70. package/package.json +5 -6
  71. package/dist/extensions/first-party/main-renderer/types/input.d.ts +0 -4
  72. /package/dist/extensions/first-party/main-renderer/types/{input.js → public.js} +0 -0
@@ -1,5 +1,6 @@
1
- import { NonEmptyPieceCode } from '../../../../state/board/types/internal.js';
2
- import { TMainRendererConfig } from './template.js';
1
+ import type { ReadonlyDeep } from 'type-fest';
2
+ import type { NonEmptyPieceCode } from '../../../../state/board/types/internal.js';
3
+ import type { TMainRendererConfig } from './template.js';
3
4
  export interface ConfigColorPair {
4
5
  light: string;
5
6
  dark: string;
@@ -9,9 +10,7 @@ export interface ConfigColors {
9
10
  coordinates: ConfigColorPair;
10
11
  }
11
12
  export type PieceUrls = Record<NonEmptyPieceCode, string>;
12
- export type MainRendererConfig = TMainRendererConfig<PieceUrls>;
13
+ export type MainRendererConfig = ReadonlyDeep<TMainRendererConfig<PieceUrls>>;
13
14
  export declare const CHESSNUT_PIECE_URLS: PieceUrls;
14
- /**
15
- * Default board renderer configuration.
16
- */
17
- export declare const DEFAULT_MAIN_RENDERER_CONFIG: MainRendererConfig;
15
+ export declare const DefaultMainRendererDesktopConfig: MainRendererConfig;
16
+ export declare const DefaultMainRendererMobileConfig: MainRendererConfig;
@@ -1,3 +1,4 @@
1
+ import { cloneDeep } from 'es-toolkit';
1
2
  export const CHESSNUT_PIECE_URLS = {
2
3
  [6 /* PieceCode.WhiteKing */]: new URL('../../../../../assets/pieces/chessnut/wK.svg', import.meta.url).toString(),
3
4
  [5 /* PieceCode.WhiteQueen */]: new URL('../../../../../assets/pieces/chessnut/wQ.svg', import.meta.url).toString(),
@@ -15,7 +16,7 @@ export const CHESSNUT_PIECE_URLS = {
15
16
  /**
16
17
  * Default board renderer configuration.
17
18
  */
18
- export const DEFAULT_MAIN_RENDERER_CONFIG = {
19
+ const DefaultMainRendererConfigPart = {
19
20
  colors: {
20
21
  board: {
21
22
  light: '#d7dde5',
@@ -28,3 +29,25 @@ export const DEFAULT_MAIN_RENDERER_CONFIG = {
28
29
  },
29
30
  pieceUrls: CHESSNUT_PIECE_URLS
30
31
  };
32
+ export const DefaultMainRendererDesktopConfig = {
33
+ ...cloneDeep(DefaultMainRendererConfigPart),
34
+ drag: {
35
+ pieceScale: 1,
36
+ pieceAnchor: 'center',
37
+ pieceAnchorOffsetY: 0
38
+ },
39
+ animation: {
40
+ durationMs: 180
41
+ }
42
+ };
43
+ export const DefaultMainRendererMobileConfig = {
44
+ ...cloneDeep(DefaultMainRendererConfigPart),
45
+ drag: {
46
+ pieceScale: 1.5,
47
+ pieceAnchor: 'bottom',
48
+ pieceAnchorOffsetY: 0.14
49
+ },
50
+ animation: {
51
+ durationMs: 180
52
+ }
53
+ };
@@ -0,0 +1,9 @@
1
+ import type { PartialDeep, ReadonlyDeep, WritableDeep } from 'type-fest';
2
+ import type { PieceString } from '../../../../state/board/types/input.js';
3
+ import type { TMainRendererConfig } from './template.js';
4
+ export type PieceUrlsPublic = Record<PieceString, string>;
5
+ export type MainRendererConfigPublic = ReadonlyDeep<TMainRendererConfig<PieceUrlsPublic>>;
6
+ export interface MainRendererInitOptions extends PartialDeep<WritableDeep<MainRendererConfigPublic>> {
7
+ pieceUrls?: PieceUrlsPublic;
8
+ }
9
+ export type MainRendererSetConfigOptions = Omit<MainRendererInitOptions, 'pieceUrls'>;
@@ -1,5 +1,16 @@
1
- import { ConfigColors } from './internal.js';
1
+ import type { ConfigColors } from './internal.js';
2
+ export type MainRendererConfigDragPieceAnchor = 'center' | 'bottom';
3
+ export interface MainRendererConfigDrag {
4
+ pieceScale: number;
5
+ pieceAnchor: MainRendererConfigDragPieceAnchor;
6
+ pieceAnchorOffsetY: number;
7
+ }
8
+ export interface MainRendererConfigAnimation {
9
+ durationMs: number;
10
+ }
2
11
  export interface TMainRendererConfig<TPieceUrls> {
3
12
  colors: ConfigColors;
4
13
  pieceUrls: TPieceUrls;
14
+ drag: MainRendererConfigDrag;
15
+ animation: MainRendererConfigAnimation;
5
16
  }
@@ -1,8 +1,8 @@
1
1
  import { ColorShort, RolePromotionShort } from '../../../../state/board/types/input.js';
2
2
  import { OpaqueColor } from '../../common/types.js';
3
- import { PieceUrlsInput } from '../../main-renderer/types/input.js';
3
+ import { PieceUrlsPublic } from '../../main-renderer/types/public.js';
4
4
  export type PromotionPieceString = `${ColorShort}${RolePromotionShort}`;
5
- export type PromotionPieceUrlsInput = Pick<PieceUrlsInput, PromotionPieceString>;
5
+ export type PromotionPieceUrlsInput = Pick<PieceUrlsPublic, PromotionPieceString>;
6
6
  export interface PromotionInitConfig {
7
7
  squareColor?: OpaqueColor;
8
8
  hoverColor?: OpaqueColor;
@@ -1 +1,2 @@
1
+ export { DefaultMainRendererDesktopConfigPublic as DefaultMainRendererDesktopConfig, DefaultMainRendererMobileConfigPublic as DefaultMainRendererMobileConfig } from './first-party/main-renderer/public-defaults.js';
1
2
  export { builtInExtensionFactoryMap, type BuiltInExtensionId } from './types/wrapper.js';
@@ -1 +1,2 @@
1
+ export { DefaultMainRendererDesktopConfigPublic as DefaultMainRendererDesktopConfig, DefaultMainRendererMobileConfigPublic as DefaultMainRendererMobileConfig } from './first-party/main-renderer/public-defaults.js';
1
2
  export { builtInExtensionFactoryMap } from './types/wrapper.js';
@@ -11,29 +11,43 @@ export interface ScenePointerEvent {
11
11
  readonly targetSquare: Square | null;
12
12
  }
13
13
  export type SceneEvent = ScenePointerEvent;
14
- export type RuntimeInteractionActionStartLiftedDrag = {
15
- type: 'startLiftedDrag';
16
- source: Square;
17
- target: Square;
14
+ export type RuntimeInteractionActionStartPendingLiftedDragSession = {
15
+ type: 'startLiftedDragSession';
16
+ phase: 'pending';
17
+ sourceSquare: Square;
18
+ targetSquare: Square;
18
19
  startButton: number;
20
+ startPoint: ScenePoint;
21
+ thresholdPx: number;
19
22
  };
20
- export type RuntimeInteractionActionStartReleaseTargetingDrag = {
21
- type: 'startReleaseTargetingDrag';
22
- source: Square;
23
- target: Square;
23
+ export type RuntimeInteractionActionStartActiveLiftedDragSession = {
24
+ type: 'startLiftedDragSession';
25
+ phase: 'active';
26
+ sourceSquare: Square;
27
+ targetSquare: Square;
24
28
  startButton: number;
25
29
  };
26
- export type RuntimeInteractionActionCompleteCoreDrag = {
27
- type: 'completeCoreDragTo';
28
- target: Square;
30
+ export type RuntimeInteractionActionActivatePendingLiftedDragSession = {
31
+ type: 'activatePendingLiftedDragSession';
32
+ targetSquare: Square | null;
29
33
  };
30
- export type RuntimeInteractionActionCompleteExtensionDrag = {
31
- type: 'completeExtensionDrag';
32
- target: Square | null;
34
+ export type RuntimeInteractionActionStartReleaseTargetingDragSession = {
35
+ type: 'startReleaseTargetingDragSession';
36
+ sourceSquare: Square;
37
+ targetSquare: Square;
38
+ startButton: number;
39
+ };
40
+ export type RuntimeInteractionActionCompleteCoreDragSessionTo = {
41
+ type: 'completeCoreDragSessionTo';
42
+ targetSquare: Square;
43
+ };
44
+ export type RuntimeInteractionActionCompleteExtensionDragSession = {
45
+ type: 'completeExtensionDragSession';
46
+ targetSquare: Square | null;
33
47
  };
34
48
  export type RuntimeInteractionActionUpdateDragSessionCurrentTarget = {
35
49
  type: 'updateDragSessionCurrentTarget';
36
- target: Square | null;
50
+ targetSquare: Square | null;
37
51
  };
38
52
  export type RuntimeInteractionActionCancelActiveInteraction = {
39
53
  type: 'cancelActiveInteraction';
@@ -41,5 +55,5 @@ export type RuntimeInteractionActionCancelActiveInteraction = {
41
55
  export type RuntimeInteractionActionCancelInteraction = {
42
56
  type: 'cancelInteraction';
43
57
  };
44
- export type RuntimeInteractionAction = RuntimeInteractionActionStartLiftedDrag | RuntimeInteractionActionStartReleaseTargetingDrag | RuntimeInteractionActionCompleteCoreDrag | RuntimeInteractionActionCompleteExtensionDrag | RuntimeInteractionActionUpdateDragSessionCurrentTarget | RuntimeInteractionActionCancelActiveInteraction | RuntimeInteractionActionCancelInteraction;
58
+ export type RuntimeInteractionAction = RuntimeInteractionActionStartPendingLiftedDragSession | RuntimeInteractionActionStartActiveLiftedDragSession | RuntimeInteractionActionActivatePendingLiftedDragSession | RuntimeInteractionActionStartReleaseTargetingDragSession | RuntimeInteractionActionCompleteCoreDragSessionTo | RuntimeInteractionActionCompleteExtensionDragSession | RuntimeInteractionActionUpdateDragSessionCurrentTarget | RuntimeInteractionActionCancelActiveInteraction | RuntimeInteractionActionCancelInteraction;
45
59
  export {};
@@ -1,15 +1,25 @@
1
- import { ReadonlyDeep } from 'type-fest';
1
+ import type { ReadonlyDeep } from 'type-fest';
2
2
  import type { PieceCode, Square } from '../../../state/board/types/internal.js';
3
+ import type { ScenePoint } from './transient-visuals.js';
3
4
  export interface ExtensionDragSessionBase {
4
5
  sourceSquare: Square | null;
5
6
  sourcePieceCode: PieceCode | null;
6
7
  targetSquare: Square | null;
7
8
  startButton: number;
8
9
  }
9
- export interface ExtensionDragSessionLiftedPiece extends ExtensionDragSessionBase {
10
+ export interface ExtensionDragSessionLiftedPieceBase extends ExtensionDragSessionBase {
10
11
  type: 'lifted-piece-drag';
11
12
  sourcePieceCode: PieceCode;
12
13
  }
14
+ export interface ExtensionDragSessionPendingLiftedPiece extends ExtensionDragSessionLiftedPieceBase {
15
+ phase: 'pending';
16
+ startPoint: ScenePoint;
17
+ thresholdPx: number;
18
+ }
19
+ export interface ExtensionDragSessionActiveLiftedPiece extends ExtensionDragSessionLiftedPieceBase {
20
+ phase: 'active';
21
+ }
22
+ export type ExtensionDragSessionLiftedPiece = ExtensionDragSessionPendingLiftedPiece | ExtensionDragSessionActiveLiftedPiece;
13
23
  export interface ExtensionDragSessionReleaseTargeting extends ExtensionDragSessionBase {
14
24
  type: 'release-targeting';
15
25
  }
@@ -2,8 +2,8 @@ import { LayoutSnapshot } from '../../../layout/types.js';
2
2
  import { ColorInput, MoveRequestInput, PiecePositionInput, PositionInput, SquareString } from '../../../state/board/types/input.js';
3
3
  import { Move } from '../../../state/board/types/internal.js';
4
4
  import { DeferredUIMoveResolutionDetails } from '../../../state/change/types/ui-move.js';
5
- import { MovabilityInput } from '../../../state/interaction/types/input.js';
6
- import { DragSessionExtensionOwned } from '../../../state/interaction/types/internal.js';
5
+ import { InteractionConfigInput, MovabilityInput } from '../../../state/interaction/types/input.js';
6
+ import { DragSessionExtensionOwned, InteractionConfigSnapshot } from '../../../state/interaction/types/internal.js';
7
7
  import { RuntimeStateSnapshot } from '../../../state/types.js';
8
8
  import { ExtensionDragSession } from '../basic/interaction.js';
9
9
  export interface ExtensionRuntimeSurfaceCommandsSnapshot {
@@ -25,6 +25,8 @@ export interface ExtensionRuntimeSurfaceCommands {
25
25
  startDrag(session: ExtensionDragSession): boolean;
26
26
  clearActiveInteraction(): boolean;
27
27
  clearInteraction(): boolean;
28
+ setInteractionConfig(config: InteractionConfigInput): boolean;
29
+ getInteractionConfig(): InteractionConfigSnapshot;
28
30
  resolveDeferredUIMoveRequest(details: DeferredUIMoveResolutionDetails): Move;
29
31
  cancelDeferredUIMoveRequest(): boolean;
30
32
  requestRender(request: ExtensionRuntimeSurfaceCommandsRenderRequest): boolean;
@@ -1,6 +1,7 @@
1
1
  import assert from '@ktarmyshov/assert';
2
2
  import { isEmptyPieceCode } from '../../state/board/check.js';
3
- import { isDragSessionCoreOwned } from '../../state/interaction/helpers.js';
3
+ import { isDragSessionActiveLiftedPiece, isDragSessionCoreOwned, isDragSessionReleaseTargeting } from '../../state/interaction/helpers.js';
4
+ import { isStartPendingLiftedDragSessionInput } from '../input/controller/helpers.js';
4
5
  import { runtimeRunMutationPipeline } from '../mutation/run.js';
5
6
  import { uiMoveCompleteTo } from './helpers.js';
6
7
  export function notifyExtensionCancelDragIfOwned(internalState, dragSession) {
@@ -18,68 +19,93 @@ export function createRuntimeInteractionSurface(state) {
18
19
  const internalState = state();
19
20
  return internalState.state.board.getPieceCodeAt(square);
20
21
  },
21
- startLiftedDrag(source, target, startButton) {
22
+ startLiftedDragSession(input) {
22
23
  const internalState = state();
23
24
  const interactionMutationSession = internalState.mutation.getSession();
24
25
  const interaction = internalState.state.interaction;
25
- const pieceCode = internalState.state.board.getPieceCodeAt(source);
26
+ const pieceCode = internalState.state.board.getPieceCodeAt(input.sourceSquare);
26
27
  assert(!isEmptyPieceCode(pieceCode), 'Cannot start a lifted-piece-drag session from an empty square');
27
28
  const interactionSource = {
28
- square: source,
29
+ square: input.sourceSquare,
29
30
  pieceCode
30
31
  };
31
32
  interaction.setSelected(interactionSource, interactionMutationSession);
32
- const dragSession = {
33
- owner: 'core',
34
- type: 'lifted-piece-drag',
35
- sourceSquare: interactionSource.square,
36
- sourcePieceCode: interactionSource.pieceCode,
37
- targetSquare: target,
38
- startButton
39
- };
33
+ const dragSession = isStartPendingLiftedDragSessionInput(input)
34
+ ? {
35
+ owner: 'core',
36
+ type: 'lifted-piece-drag',
37
+ phase: 'pending',
38
+ sourceSquare: interactionSource.square,
39
+ sourcePieceCode: interactionSource.pieceCode,
40
+ targetSquare: input.targetSquare,
41
+ startButton: input.startButton,
42
+ startPoint: input.startPoint,
43
+ thresholdPx: input.thresholdPx
44
+ }
45
+ : {
46
+ owner: 'core',
47
+ type: 'lifted-piece-drag',
48
+ phase: 'active',
49
+ sourceSquare: interactionSource.square,
50
+ sourcePieceCode: interactionSource.pieceCode,
51
+ targetSquare: input.targetSquare,
52
+ startButton: input.startButton
53
+ };
40
54
  interaction.setDragSession(dragSession, interactionMutationSession);
41
55
  runtimeRunMutationPipeline(internalState);
42
56
  },
57
+ activatePendingLiftedDragSession(input) {
58
+ const internalState = state();
59
+ const mutationSession = internalState.mutation.getSession();
60
+ const interaction = internalState.state.interaction;
61
+ interaction.activatePendingLiftedDragSession(input.targetSquare, mutationSession);
62
+ runtimeRunMutationPipeline(internalState);
63
+ },
43
64
  transientInput(input) {
44
65
  const internalState = state();
45
66
  internalState.renderSystem.requestRenderVisuals(input);
46
67
  },
47
- completeCoreDragTo(target) {
68
+ completeCoreDragSessionTo(target) {
48
69
  const internalState = state();
49
70
  const mutationSession = internalState.mutation.getSession();
50
71
  const interaction = internalState.state.interaction;
72
+ const currentDragSession = interaction.dragSession;
73
+ assert(currentDragSession !== null &&
74
+ isDragSessionCoreOwned(currentDragSession) &&
75
+ (isDragSessionActiveLiftedPiece(currentDragSession) ||
76
+ isDragSessionReleaseTargeting(currentDragSession)), 'completeCoreDragSessionTo requires a core-owned active lifted-piece or release-targeting drag session');
51
77
  interaction.updateDragSessionCurrentTarget(target, mutationSession);
52
78
  const dragSession = interaction.dragSession;
53
- assert(isDragSessionCoreOwned(dragSession), 'completeDragTo can only be called for core-owned drag sessions');
54
79
  uiMoveCompleteTo(internalState, target);
55
- mutationSession.addMutation('runtime.interaction.completeCoreDragTo', true, dragSession);
80
+ mutationSession.addMutation('runtime.interaction.completeCoreDragSessionTo', true, dragSession);
56
81
  runtimeRunMutationPipeline(internalState);
57
82
  },
58
- completeExtensionDrag(target) {
83
+ completeExtensionDragSession(target) {
59
84
  const internalState = state();
60
85
  const mutationSession = internalState.mutation.getSession();
61
86
  const interaction = internalState.state.interaction;
87
+ const currentDragSession = interaction.dragSession;
88
+ assert(currentDragSession !== null && !isDragSessionCoreOwned(currentDragSession), 'completeExtensionDragSession can only be called for extension-owned drag sessions');
62
89
  interaction.updateDragSessionCurrentTarget(target, mutationSession);
63
90
  const dragSession = interaction.dragSession;
64
- assert(!isDragSessionCoreOwned(dragSession), 'completeExtensionDrag can only be called for extension-owned drag sessions');
65
91
  internalState.extensionSystem.completeDrag(dragSession);
66
92
  internalState.state.interaction.setDragSession(null, mutationSession);
67
- mutationSession.addMutation('runtime.interaction.completeExtensionDragTo', true, dragSession);
93
+ mutationSession.addMutation('runtime.interaction.completeExtensionDragSession', true, dragSession);
68
94
  runtimeRunMutationPipeline(internalState);
69
95
  },
70
- startReleaseTargetingDrag(source, target, startButton) {
96
+ startReleaseTargetingDragSession(input) {
71
97
  const internalState = state();
72
98
  const mutationSession = internalState.mutation.getSession();
73
99
  const interaction = internalState.state.interaction;
74
- assert(interaction.selected !== null, 'startReleaseTargetingDrag requires a selected piece');
75
- assert(interaction.selected.square === source, 'startReleaseTargetingDrag source must match selected square');
100
+ assert(interaction.selected !== null, 'startReleaseTargetingDragSession requires a selected piece');
101
+ assert(interaction.selected.square === input.sourceSquare, 'startReleaseTargetingDragSession source must match selected square');
76
102
  const dragSession = {
77
103
  owner: 'core',
78
104
  type: 'release-targeting',
79
- sourceSquare: source,
105
+ sourceSquare: input.sourceSquare,
80
106
  sourcePieceCode: interaction.selected.pieceCode,
81
- targetSquare: target,
82
- startButton
107
+ targetSquare: input.targetSquare,
108
+ startButton: input.startButton
83
109
  };
84
110
  interaction.setDragSession(dragSession, mutationSession);
85
111
  runtimeRunMutationPipeline(internalState);
@@ -91,6 +91,17 @@ function createExtensionRuntimeSurfaceCommandsInternalSurface(getInternalState)
91
91
  runtimeRunMutationPipeline(state);
92
92
  return changed;
93
93
  },
94
+ setInteractionConfig(config) {
95
+ const state = getInternalState();
96
+ const mutationSession = state.mutation.getSession();
97
+ const changed = state.state.interaction.setConfig(config, mutationSession);
98
+ runtimeRunMutationPipeline(state);
99
+ return changed;
100
+ },
101
+ getInteractionConfig() {
102
+ const state = getInternalState();
103
+ return state.state.interaction.getConfig();
104
+ },
94
105
  select(square) {
95
106
  const state = getInternalState();
96
107
  const mutationSession = state.mutation.getSession();
@@ -3,5 +3,6 @@ export const NEED_EVENT_TYPES = new Set([
3
3
  'pointermove',
4
4
  'pointerup',
5
5
  'pointercancel',
6
- 'lostpointercapture'
6
+ 'lostpointercapture',
7
+ 'dragstart'
7
8
  ]);
@@ -19,22 +19,49 @@ export function createInteractionController(options) {
19
19
  transmitTransientInput(internalState, context);
20
20
  return; // The event has been handled by the surface (extensions)
21
21
  }
22
+ else if (context.rawEvent.type === 'dragstart') {
23
+ // Prevent native drag-and-drop behavior to avoid conflicts with custom drag handling
24
+ context.rawEvent.preventDefault();
25
+ }
22
26
  if (action) {
23
27
  switch (action.type) {
24
- case 'startLiftedDrag':
25
- internalState.surface.startLiftedDrag(action.source, action.target, action.startButton);
28
+ case 'startLiftedDragSession':
29
+ internalState.surface.startLiftedDragSession(action.phase === 'pending'
30
+ ? {
31
+ phase: 'pending',
32
+ sourceSquare: action.sourceSquare,
33
+ targetSquare: action.targetSquare,
34
+ startButton: action.startButton,
35
+ startPoint: action.startPoint,
36
+ thresholdPx: action.thresholdPx
37
+ }
38
+ : {
39
+ phase: 'active',
40
+ sourceSquare: action.sourceSquare,
41
+ targetSquare: action.targetSquare,
42
+ startButton: action.startButton
43
+ });
44
+ break;
45
+ case 'activatePendingLiftedDragSession':
46
+ internalState.surface.activatePendingLiftedDragSession({
47
+ targetSquare: action.targetSquare
48
+ });
26
49
  break;
27
- case 'startReleaseTargetingDrag':
28
- internalState.surface.startReleaseTargetingDrag(action.source, action.target, action.startButton);
50
+ case 'startReleaseTargetingDragSession':
51
+ internalState.surface.startReleaseTargetingDragSession({
52
+ sourceSquare: action.sourceSquare,
53
+ targetSquare: action.targetSquare,
54
+ startButton: action.startButton
55
+ });
29
56
  break;
30
- case 'completeCoreDragTo':
31
- internalState.surface.completeCoreDragTo(action.target);
57
+ case 'completeCoreDragSessionTo':
58
+ internalState.surface.completeCoreDragSessionTo(action.targetSquare);
32
59
  break;
33
- case 'completeExtensionDrag':
34
- internalState.surface.completeExtensionDrag(action.target);
60
+ case 'completeExtensionDragSession':
61
+ internalState.surface.completeExtensionDragSession(action.targetSquare);
35
62
  break;
36
63
  case 'updateDragSessionCurrentTarget':
37
- internalState.surface.updateDragSessionCurrentTarget(action.target);
64
+ internalState.surface.updateDragSessionCurrentTarget(action.targetSquare);
38
65
  break;
39
66
  case 'cancelActiveInteraction':
40
67
  internalState.surface.cancelActiveInteraction();
@@ -1,4 +1,9 @@
1
+ import type { ScenePoint } from '../../../extensions/types/basic/transient-visuals.js';
1
2
  import type { Square } from '../../../state/board/types/internal.js';
2
3
  import type { InteractionStateSnapshot } from '../../../state/interaction/types/main.js';
4
+ import type { StartActiveLiftedDragSessionInput, StartLiftedDragSessionInput, StartPendingLiftedDragSessionInput } from './types.js';
3
5
  export declare function buttonToButtonsMask(button: number): number | null;
4
6
  export declare function canMoveTo(snapshot: InteractionStateSnapshot, target: Square): boolean;
7
+ export declare function isStartPendingLiftedDragSessionInput(input: StartLiftedDragSessionInput): input is StartPendingLiftedDragSessionInput;
8
+ export declare function isStartActiveLiftedDragSessionInput(input: StartLiftedDragSessionInput): input is StartActiveLiftedDragSessionInput;
9
+ export declare function isMovementBeyondThreshold(start: ScenePoint, current: ScenePoint, thresholdPx: number): boolean;
@@ -26,3 +26,14 @@ export function canMoveTo(snapshot, target) {
26
26
  // strict: target must be in the computed active destinations
27
27
  return snapshot.activeDestinations.has(target);
28
28
  }
29
+ export function isStartPendingLiftedDragSessionInput(input) {
30
+ return input.phase === 'pending';
31
+ }
32
+ export function isStartActiveLiftedDragSessionInput(input) {
33
+ return input.phase === 'active';
34
+ }
35
+ export function isMovementBeyondThreshold(start, current, thresholdPx) {
36
+ const dx = current.x - start.x;
37
+ const dy = current.y - start.y;
38
+ return dx * dx + dy * dy >= thresholdPx * thresholdPx;
39
+ }
@@ -1,8 +1,8 @@
1
1
  import assert from '@ktarmyshov/assert';
2
2
  import { isEmptyPieceCode, isNonEmptyPieceCode } from '../../../state/board/check.js';
3
3
  import { fromPieceCode } from '../../../state/board/piece.js';
4
- import { isDragSessionCoreOwned } from '../../../state/interaction/helpers.js';
5
- import { buttonToButtonsMask, canMoveTo } from './helpers.js';
4
+ import { isDragSessionActiveLiftedPiece, isDragSessionCoreOwned, isDragSessionPendingLiftedPiece, isDragSessionReleaseTargeting } from '../../../state/interaction/helpers.js';
5
+ import { buttonToButtonsMask, canMoveTo, isMovementBeyondThreshold } from './helpers.js';
6
6
  export function determineActionPointerDown(state, context) {
7
7
  assert(context.rawEvent.type === 'pointerdown', 'determineActionPointerDown should only be called for pointerdown events');
8
8
  const rawEvent = context.rawEvent;
@@ -29,9 +29,9 @@ export function determineActionPointerDown(state, context) {
29
29
  const targetPieceCode = state.surface.getPieceCodeAt(sceneEvent.targetSquare);
30
30
  if (isEmptyPieceCode(targetPieceCode)) {
31
31
  return {
32
- type: 'startReleaseTargetingDrag',
33
- source: interaction.selected.square,
34
- target: sceneEvent.targetSquare,
32
+ type: 'startReleaseTargetingDragSession',
33
+ sourceSquare: interaction.selected.square,
34
+ targetSquare: sceneEvent.targetSquare,
35
35
  startButton: rawEvent.button
36
36
  };
37
37
  }
@@ -41,9 +41,9 @@ export function determineActionPointerDown(state, context) {
41
41
  if (fromPieceCode(targetPieceCode).color !== fromPieceCode(selectedPieceCode).color &&
42
42
  isLegalMoveTarget) {
43
43
  return {
44
- type: 'startReleaseTargetingDrag',
45
- source: interaction.selected.square,
46
- target: sceneEvent.targetSquare,
44
+ type: 'startReleaseTargetingDragSession',
45
+ sourceSquare: interaction.selected.square,
46
+ targetSquare: sceneEvent.targetSquare,
47
47
  startButton: rawEvent.button
48
48
  };
49
49
  }
@@ -54,10 +54,23 @@ export function determineActionPointerDown(state, context) {
54
54
  */
55
55
  const pieceCode = state.surface.getPieceCodeAt(sceneEvent.targetSquare);
56
56
  if (!isEmptyPieceCode(pieceCode)) {
57
+ const thresholdPx = interaction.config.drag.liftedActivation.thresholdPx;
58
+ if (thresholdPx > 0) {
59
+ return {
60
+ type: 'startLiftedDragSession',
61
+ phase: 'pending',
62
+ sourceSquare: sceneEvent.targetSquare,
63
+ targetSquare: sceneEvent.targetSquare,
64
+ startButton: rawEvent.button,
65
+ startPoint: sceneEvent.point,
66
+ thresholdPx
67
+ };
68
+ }
57
69
  return {
58
- type: 'startLiftedDrag',
59
- source: sceneEvent.targetSquare,
60
- target: sceneEvent.targetSquare,
70
+ type: 'startLiftedDragSession',
71
+ phase: 'active',
72
+ sourceSquare: sceneEvent.targetSquare,
73
+ targetSquare: sceneEvent.targetSquare,
61
74
  startButton: rawEvent.button
62
75
  };
63
76
  }
@@ -67,13 +80,25 @@ export function determineActionPointerDown(state, context) {
67
80
  export function determineActionPointerMove(state, context) {
68
81
  assert(context.rawEvent.type === 'pointermove', 'determineActionPointerMove should only be called for pointermove events');
69
82
  const interaction = state.surface.getInteractionStateSnapshot();
70
- if (interaction.dragSession) {
71
- return {
72
- type: 'updateDragSessionCurrentTarget',
73
- target: context.sceneEvent?.targetSquare ?? null
74
- };
83
+ const dragSession = interaction.dragSession;
84
+ if (!dragSession) {
85
+ return null;
75
86
  }
76
- return null;
87
+ const sceneEvent = context.sceneEvent;
88
+ assert(sceneEvent, 'pointermove requires a scene event');
89
+ if (isDragSessionPendingLiftedPiece(dragSession)) {
90
+ if (isMovementBeyondThreshold(dragSession.startPoint, sceneEvent.point, dragSession.thresholdPx)) {
91
+ return {
92
+ type: 'activatePendingLiftedDragSession',
93
+ targetSquare: sceneEvent.targetSquare
94
+ };
95
+ }
96
+ return { type: 'updateDragSessionCurrentTarget', targetSquare: sceneEvent.targetSquare };
97
+ }
98
+ return {
99
+ type: 'updateDragSessionCurrentTarget',
100
+ targetSquare: sceneEvent.targetSquare
101
+ };
77
102
  }
78
103
  function determineActionTerminalRelease(state, context) {
79
104
  const interaction = state.surface.getInteractionStateSnapshot();
@@ -85,15 +110,17 @@ function determineActionTerminalRelease(state, context) {
85
110
  assert(sceneEvent, 'Scene event should be present for terminal release');
86
111
  if (!isDragSessionCoreOwned(dragSession)) {
87
112
  return {
88
- type: 'completeExtensionDrag',
89
- target: sceneEvent.targetSquare
113
+ type: 'completeExtensionDragSession',
114
+ targetSquare: sceneEvent.targetSquare
90
115
  };
91
116
  }
92
117
  if (sceneEvent.targetSquare === dragSession.sourceSquare) {
93
118
  return { type: 'cancelActiveInteraction' };
94
119
  }
95
- if (sceneEvent.targetSquare !== null && canMoveTo(interaction, sceneEvent.targetSquare)) {
96
- return { type: 'completeCoreDragTo', target: sceneEvent.targetSquare };
120
+ if (sceneEvent.targetSquare !== null &&
121
+ canMoveTo(interaction, sceneEvent.targetSquare) &&
122
+ (isDragSessionActiveLiftedPiece(dragSession) || isDragSessionReleaseTargeting(dragSession))) {
123
+ return { type: 'completeCoreDragSessionTo', targetSquare: sceneEvent.targetSquare };
97
124
  }
98
125
  if (dragSession.type === 'lifted-piece-drag') {
99
126
  return { type: 'cancelActiveInteraction' };
@@ -1,14 +1,23 @@
1
1
  import type { TransientInput } from '../../../extensions/types/basic/transient-visuals.js';
2
2
  import type { ExtensionOnEventContext } from '../../../extensions/types/context/events.js';
3
3
  import type { PieceCode, Square } from '../../../state/board/types/internal.js';
4
+ import type { DragSessionActiveLiftedPieceCoreOwned, DragSessionPendingLiftedPieceCoreOwned, DragSessionReleaseTargetingCoreOwned } from '../../../state/interaction/types/internal.js';
4
5
  import type { InteractionStateSnapshot } from '../../../state/interaction/types/main.js';
6
+ export type StartPendingLiftedDragSessionInput = Pick<DragSessionPendingLiftedPieceCoreOwned, 'phase' | 'sourceSquare' | 'targetSquare' | 'startButton' | 'startPoint' | 'thresholdPx'>;
7
+ export type StartActiveLiftedDragSessionInput = Pick<DragSessionActiveLiftedPieceCoreOwned, 'phase' | 'sourceSquare' | 'targetSquare' | 'startButton'>;
8
+ export type StartLiftedDragSessionInput = StartPendingLiftedDragSessionInput | StartActiveLiftedDragSessionInput;
9
+ export type ActivatePendingLiftedDragSessionInput = {
10
+ targetSquare: Square | null;
11
+ };
12
+ export type StartReleaseTargetingDragSessionInput = Pick<DragSessionReleaseTargetingCoreOwned, 'sourceSquare' | 'targetSquare' | 'startButton'>;
5
13
  export interface RuntimeInteractionSurface {
6
14
  getInteractionStateSnapshot(): InteractionStateSnapshot;
7
15
  getPieceCodeAt(square: Square): PieceCode;
8
- startLiftedDrag(source: Square, target: Square, startButton: number): void;
9
- startReleaseTargetingDrag(source: Square, target: Square, startButton: number): void;
10
- completeCoreDragTo(target: Square): void;
11
- completeExtensionDrag(target: Square | null): void;
16
+ startLiftedDragSession(input: StartLiftedDragSessionInput): void;
17
+ activatePendingLiftedDragSession(input: ActivatePendingLiftedDragSessionInput): void;
18
+ startReleaseTargetingDragSession(input: StartReleaseTargetingDragSessionInput): void;
19
+ completeCoreDragSessionTo(target: Square): void;
20
+ completeExtensionDragSession(target: Square | null): void;
12
21
  updateDragSessionCurrentTarget(target: Square | null): void;
13
22
  cancelActiveInteraction(): void;
14
23
  cancelInteraction(): void;
@@ -4,7 +4,7 @@ export const reconcileInteractionSelectionAfterBoardStateChange = (context, muta
4
4
  const { current } = context;
5
5
  if (!mutationSession.hasMutation({ prefixes: ['state.board.'] }) ||
6
6
  mutationSession.hasMutation({
7
- causes: ['state.interaction.clear', 'runtime.interaction.completeCoreDragTo']
7
+ causes: ['state.interaction.clear', 'runtime.interaction.completeCoreDragSessionTo']
8
8
  })) {
9
9
  return;
10
10
  }