@mirasen/chessboard 0.1.1 → 1.0.0-rc.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 (125) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +224 -47
  3. package/dist/animation/planner.d.ts +11 -6
  4. package/dist/animation/planner.js +164 -45
  5. package/dist/animation/render/fade.d.ts +2 -2
  6. package/dist/animation/render/fade.js +3 -3
  7. package/dist/animation/render/move.d.ts +3 -3
  8. package/dist/animation/render/move.js +5 -5
  9. package/dist/animation/render/plan.d.ts +3 -3
  10. package/dist/animation/render/static.d.ts +2 -2
  11. package/dist/animation/render/static.js +3 -3
  12. package/dist/animation/types.d.ts +14 -15
  13. package/dist/animation/types.js +1 -6
  14. package/dist/extensions/factory/main.js +23 -2
  15. package/dist/extensions/factory/runtime.d.ts +2 -1
  16. package/dist/extensions/factory/runtime.js +48 -4
  17. package/dist/extensions/first-party/active-target/factory.js +5 -5
  18. package/dist/extensions/first-party/auto-promote/factory.d.ts +2 -0
  19. package/dist/extensions/first-party/auto-promote/factory.js +51 -0
  20. package/dist/extensions/first-party/auto-promote/types.d.ts +14 -0
  21. package/dist/extensions/first-party/auto-promote/types.js +2 -0
  22. package/dist/extensions/first-party/board-events/factory.js +1 -16
  23. package/dist/extensions/first-party/board-events/types.d.ts +1 -2
  24. package/dist/extensions/first-party/last-move/factory.js +6 -6
  25. package/dist/extensions/first-party/legal-moves/factory.js +1 -1
  26. package/dist/extensions/first-party/main-renderer/animation/render.js +1 -2
  27. package/dist/extensions/first-party/main-renderer/animation/update.js +9 -23
  28. package/dist/extensions/first-party/main-renderer/board/render.js +3 -3
  29. package/dist/extensions/first-party/main-renderer/board/update.js +5 -3
  30. package/dist/extensions/first-party/main-renderer/coordinates/render.js +4 -4
  31. package/dist/extensions/first-party/main-renderer/drag/render.js +1 -1
  32. package/dist/extensions/first-party/main-renderer/normalize.d.ts +2 -3
  33. package/dist/extensions/first-party/main-renderer/normalize.js +1 -1
  34. package/dist/extensions/first-party/main-renderer/pieces/render.js +5 -5
  35. package/dist/extensions/first-party/main-renderer/pieces/suppress.js +7 -2
  36. package/dist/extensions/first-party/main-renderer/types/extension.d.ts +1 -1
  37. package/dist/extensions/first-party/main-renderer/types/extension.js +1 -1
  38. package/dist/extensions/first-party/promotion/factory.d.ts +3 -0
  39. package/dist/extensions/first-party/promotion/factory.js +219 -0
  40. package/dist/extensions/first-party/promotion/normalize.d.ts +3 -0
  41. package/dist/extensions/first-party/promotion/normalize.js +37 -0
  42. package/dist/extensions/first-party/promotion/types/input.d.ts +10 -0
  43. package/dist/extensions/first-party/promotion/types/internal.d.ts +12 -0
  44. package/dist/extensions/first-party/promotion/types/internal.js +22 -0
  45. package/dist/extensions/first-party/promotion/types/main.d.ts +24 -0
  46. package/dist/extensions/first-party/promotion/types/main.js +2 -0
  47. package/dist/extensions/first-party/selected-square/factory.js +3 -3
  48. package/dist/extensions/index.d.ts +1 -1
  49. package/dist/extensions/types/basic/events.d.ts +8 -31
  50. package/dist/extensions/types/basic/events.js +9 -5
  51. package/dist/extensions/types/basic/interaction.d.ts +21 -0
  52. package/dist/extensions/types/basic/transient-visuals.d.ts +2 -1
  53. package/dist/extensions/types/context/events.d.ts +5 -0
  54. package/dist/extensions/types/context/events.js +1 -0
  55. package/dist/extensions/types/context/ui-move.d.ts +3 -15
  56. package/dist/extensions/types/extension.d.ts +10 -8
  57. package/dist/extensions/types/main.d.ts +11 -6
  58. package/dist/extensions/types/surface/commands.d.ts +9 -0
  59. package/dist/extensions/types/surface/events.d.ts +2 -3
  60. package/dist/extensions/types/wrapper.d.ts +23 -22
  61. package/dist/extensions/types/wrapper.js +34 -6
  62. package/dist/extensions/update.d.ts +2 -2
  63. package/dist/extensions/update.js +1 -1
  64. package/dist/layout/factory.js +3 -3
  65. package/dist/layout/geometry/factory.d.ts +2 -11
  66. package/dist/layout/geometry/factory.js +16 -17
  67. package/dist/layout/geometry/helpers.d.ts +5 -0
  68. package/dist/layout/geometry/helpers.js +33 -0
  69. package/dist/layout/geometry/types.d.ts +13 -12
  70. package/dist/layout/helpers.d.ts +3 -4
  71. package/dist/layout/helpers.js +5 -5
  72. package/dist/layout/reducers.js +9 -8
  73. package/dist/layout/types.d.ts +5 -5
  74. package/dist/render/mount.js +2 -2
  75. package/dist/render/rendering/state.js +7 -7
  76. package/dist/runtime/factory/events.d.ts +3 -0
  77. package/dist/runtime/factory/events.js +18 -0
  78. package/dist/runtime/factory/helpers.js +12 -12
  79. package/dist/runtime/factory/input.js +17 -10
  80. package/dist/runtime/factory/main.js +44 -4
  81. package/dist/runtime/input/adapter/event.d.ts +3 -0
  82. package/dist/runtime/input/adapter/event.js +19 -0
  83. package/dist/runtime/input/adapter/factory.js +19 -38
  84. package/dist/runtime/input/adapter/helpers.d.ts +3 -5
  85. package/dist/runtime/input/adapter/helpers.js +60 -12
  86. package/dist/runtime/input/adapter/pointer.d.ts +6 -10
  87. package/dist/runtime/input/adapter/pointer.js +37 -70
  88. package/dist/runtime/input/adapter/types.d.ts +6 -3
  89. package/dist/runtime/input/adapter/types.js +6 -1
  90. package/dist/runtime/input/controller/factory.js +12 -11
  91. package/dist/runtime/input/controller/pointer.d.ts +5 -6
  92. package/dist/runtime/input/controller/pointer.js +36 -35
  93. package/dist/runtime/input/controller/transient-visuals.d.ts +2 -2
  94. package/dist/runtime/input/controller/transient-visuals.js +12 -5
  95. package/dist/runtime/input/controller/types.d.ts +5 -4
  96. package/dist/runtime/lifecycle.js +6 -6
  97. package/dist/runtime/mutation/interaction.js +1 -5
  98. package/dist/runtime/mutation/types.d.ts +10 -8
  99. package/dist/runtime/types/main.d.ts +2 -0
  100. package/dist/state/change/{factory.d.ts → factory/main.d.ts} +1 -1
  101. package/dist/state/change/{factory.js → factory/main.js} +8 -5
  102. package/dist/state/change/factory/ui-move.d.ts +4 -0
  103. package/dist/{runtime → state/change}/factory/ui-move.js +9 -9
  104. package/dist/state/change/helpers.d.ts +4 -2
  105. package/dist/state/change/helpers.js +11 -1
  106. package/dist/state/change/mutation.d.ts +1 -1
  107. package/dist/state/change/reducers.d.ts +3 -3
  108. package/dist/state/change/reducers.js +4 -4
  109. package/dist/state/change/types/main.d.ts +18 -0
  110. package/dist/state/change/types/main.js +1 -0
  111. package/dist/state/change/types/ui-move.d.ts +25 -0
  112. package/dist/state/change/types/ui-move.js +1 -0
  113. package/dist/state/factory.js +1 -1
  114. package/dist/state/interaction/factory.js +4 -2
  115. package/dist/state/interaction/helpers.d.ts +2 -0
  116. package/dist/state/interaction/helpers.js +3 -0
  117. package/dist/state/interaction/types/internal.d.ts +9 -1
  118. package/dist/state/types.d.ts +1 -1
  119. package/dist/wrapper/factory.js +2 -26
  120. package/package.json +9 -5
  121. package/dist/runtime/factory/ui-move.d.ts +0 -4
  122. package/dist/runtime/types/ui-move.d.ts +0 -9
  123. package/dist/state/change/types.d.ts +0 -17
  124. /package/dist/{runtime/types/ui-move.js → extensions/first-party/promotion/types/input.js} +0 -0
  125. /package/dist/{state/change/types.js → extensions/types/basic/interaction.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @mirasen/chessboard
2
2
 
3
+ ## 1.0.0-rc.0
4
+
5
+ ### Major Changes
6
+
7
+ - 2609920: First 1.0 release candidate with the new extension-driven architecture, built-in chess interaction baseline, promotion flow, and auto-promote support.
8
+
9
+ ### Patch Changes
10
+
11
+ - 2609920: Added promotion and auto-promotion to queen extensions
12
+
3
13
  ## 0.1.1
4
14
 
5
15
  ### Patch Changes
package/README.md CHANGED
@@ -6,45 +6,48 @@
6
6
 
7
7
  # @mirasen/chessboard
8
8
 
9
- Framework-agnostic TypeScript chessboard library focused on clean architecture, explicit rendering ownership, and extension-driven features.
9
+ A framework-agnostic TypeScript chessboard platform with real chess interaction built in and an extension-driven architecture.
10
10
 
11
- ## Status
11
+ ## Why this exists
12
+
13
+ Most chessboards fall into one of two buckets:
12
14
 
13
- **Early public release (`0.1.x`)**
15
+ - rendering primitives that leave core interaction UX to app code
16
+ - tightly coupled monoliths that are hard to extend cleanly
14
17
 
15
- The core architecture is already usable, but the package is still pre-`1.0.0` and the API may evolve.
18
+ `@mirasen/chessboard` aims for a better balance:
16
19
 
17
- Current focus:
20
+ - meaningful built-in chess interaction out of the box
21
+ - first-party extensions for common board UX
22
+ - explicit rendering and ownership boundaries
23
+ - extension-driven growth without turning the board into glue hell
18
24
 
19
- - stable core runtime
20
- - extension-driven rendering and interaction
21
- - public wrapper / package API
22
- - early integration ergonomics
25
+ This is not just a board renderer. It is a chessboard platform designed for real chess interaction.
23
26
 
24
- Not finalized yet:
27
+ ## What you get out of the box
25
28
 
26
- - promotion flow
27
- - some higher-level public APIs
28
- - broader test coverage
29
- - final `1.0.0` API guarantees
29
+ The built-in first-party extension baseline currently includes both foundational runtime-facing extensions and user-facing board features.
30
30
 
31
- ## Goals
31
+ - `renderer` — the first-party rendering extension that validates the same extension architecture used for board features
32
+ - `events`
33
+ - `selectedSquare`
34
+ - `activeTarget`
35
+ - `legalMoves`
36
+ - `lastMove`
37
+ - `promotion`
38
+ - `autoPromote`
32
39
 
33
- - framework-agnostic board core
34
- - explicit runtime / render / interaction boundaries
35
- - extension-first architecture
36
- - predictable rendering ownership
37
- - room for advanced features without core hacks
40
+ That means the board already covers a meaningful baseline chess UX instead of stopping at “draw squares and pieces”.
38
41
 
39
- ## Current features
42
+ ## Highlights
40
43
 
41
- - board rendering
42
- - orientation support
43
- - selection and drag interaction
44
- - move targeting
45
- - move animation foundation
46
- - built-in visual extensions such as selected square, active target, legal moves, and last move
47
- - board events extension with `setOnUiMove(...)` and `setOnRawUpdate(...)`
44
+ - Real built-in chess interaction
45
+ - Extension-driven architecture
46
+ - First-party baseline extensions
47
+ - Explicit SVG slot ownership
48
+ - Clean separation between runtime, rendering, interaction, and animation
49
+ - Promotion flow built into the board pipeline
50
+ - Optional auto-promotion as a separate behavior extension
48
51
 
49
52
  ## Installation
50
53
 
@@ -52,45 +55,219 @@ Not finalized yet:
52
55
  npm install @mirasen/chessboard
53
56
  ```
54
57
 
55
- ## Example
58
+ ## Usage
59
+
60
+ ### Minimal example
56
61
 
57
62
  ```ts
58
63
  import { createBoard } from '@mirasen/chessboard';
59
64
 
65
+ const element = document.getElementById('board');
66
+
67
+ if (!element) {
68
+ throw new Error('Missing board element');
69
+ }
70
+
60
71
  const board = createBoard({
61
- // API still evolving in 0.1.x
72
+ element
62
73
  });
74
+ // Set free movability, default - disabled
75
+ board.setMovability({
76
+ mode: 'free'
77
+ });
78
+ ```
79
+
80
+ ### Explicit built-in interaction extensions
81
+
82
+ ```ts
83
+ import { createBoard } from '@mirasen/chessboard';
84
+
85
+ const element = document.getElementById('board');
86
+
87
+ if (!element) {
88
+ throw new Error('Missing board element');
89
+ }
90
+
91
+ const board = createBoard({
92
+ element,
93
+ extensions: [
94
+ // Always add renderer first, otherwise nothing will be rendered
95
+ 'renderer',
96
+ 'selectedSquare',
97
+ 'activeTarget',
98
+ 'legalMoves',
99
+ 'lastMove',
100
+ 'events',
101
+ // Keep autoPromote before promotion, otherwise promotion may defer the move first
102
+ 'autoPromote',
103
+ 'promotion'
104
+ ]
105
+ });
106
+ ```
63
107
 
64
- board.extensions.events.setOnUiMove((move) => {
108
+ ### setOnUIMove
109
+
110
+ ```ts
111
+ import { createBoard } from '@mirasen/chessboard';
112
+
113
+ const element = document.getElementById('board');
114
+
115
+ if (!element) {
116
+ throw new Error('Missing board element');
117
+ }
118
+
119
+ const board = createBoard({
120
+ element,
121
+ state: {
122
+ board: 'start',
123
+ interaction: {
124
+ movability: {
125
+ mode: 'free'
126
+ }
127
+ }
128
+ }
129
+ });
130
+
131
+ board.extensions.events.setOnUIMove((move) => {
65
132
  console.log('UI move:', move);
66
133
  });
67
134
  ```
68
135
 
69
- ## Artwork
136
+ ### setMovability - strict with explicit destinations
137
+
138
+ ```ts
139
+ import { createBoard } from '@mirasen/chessboard';
140
+
141
+ const element = document.getElementById('board');
142
+
143
+ if (!element) {
144
+ throw new Error('Missing board element');
145
+ }
146
+
147
+ const board = createBoard({
148
+ element
149
+ });
150
+
151
+ board.setMovability({
152
+ mode: 'strict',
153
+ destinations: {
154
+ e2: [{ to: 'e3' }, { to: 'e4' }],
155
+ d7: [
156
+ { to: 'c8', promotedTo: ['queen', 'rook', 'bishop', 'knight'] },
157
+ { to: 'd8', promotedTo: ['queen', 'rook', 'bishop', 'knight'] }
158
+ ],
159
+ g1: [{ to: 'f3' }, { to: 'h3' }]
160
+ }
161
+ });
162
+ ```
163
+
164
+ ### setMovability - strict with a resolver
165
+
166
+ ```ts
167
+ import { createBoard } from '@mirasen/chessboard';
168
+
169
+ const element = document.getElementById('board');
170
+
171
+ if (!element) {
172
+ throw new Error('Missing board element');
173
+ }
174
+
175
+ const board = createBoard({
176
+ element
177
+ });
178
+
179
+ board.setMovability({
180
+ mode: 'strict',
181
+ destinations: (source) => {
182
+ if (source === 'e2')
183
+ return [
184
+ { to: 'e8', promotedTo: ['bishop', 'queen'] },
185
+ { to: 'd8', promotedTo: ['rook', 'knight', 'queen'] }
186
+ ];
187
+ if (source === 'f7')
188
+ return [
189
+ { to: 'f8', promotedTo: ['bishop', 'rook', 'knight', 'queen'] },
190
+ { to: 'g8', promotedTo: ['bishop', 'rook', 'knight', 'queen'] }
191
+ ];
192
+ if (source === 'd7')
193
+ return [
194
+ { to: 'd1', promotedTo: ['bishop', 'knight'] },
195
+ { to: 'e1', promotedTo: ['rook', 'knight', 'queen'] }
196
+ ];
197
+ if (source === 'c2')
198
+ return [
199
+ { to: 'c1', promotedTo: ['bishop', 'rook', 'knight', 'queen'] },
200
+ { to: 'b1', promotedTo: ['bishop', 'rook', 'knight', 'queen'] }
201
+ ];
202
+ return undefined;
203
+ }
204
+ });
205
+ ```
206
+
207
+ ## Promotion and auto-promotion
70
208
 
71
- ### Chessnut piece set
209
+ Promotion is handled inside the board interaction pipeline through a natural deferred move flow.
72
210
 
73
- This project uses the Chessnut chess piece set.
211
+ That means the board does not need the awkward pattern of:
74
212
 
75
- This is the current default piece set used by the library.
213
+ - reject move
214
+ - open external dialog
215
+ - replay the move later
76
216
 
77
- - Author: Alexis Luengas — https://github.com/LexLuengas
78
- - Source: https://github.com/LexLuengas/chessnut-pieces
79
- - License: Apache License 2.0 — https://github.com/LexLuengas/chessnut-pieces/blob/master/LICENSE.txt
217
+ Instead:
80
218
 
81
- For details, see:
219
+ - `promotion` provides the built-in promotion chooser UI
220
+ - `autoPromote` provides behavior-only auto-promotion logic
82
221
 
83
- - [./assets/pieces/chessnut/ATTRIBUTION.md](./assets/pieces/chessnut/ATTRIBUTION.md)
84
- - [./assets/pieces/chessnut/LICENSE.txt](./assets/pieces/chessnut/LICENSE.txt)
222
+ These are separate extensions on purpose:
223
+
224
+ - `promotion` is UI
225
+ - `autoPromote` is behavior
226
+
227
+ This separation is a good example of the architecture: common chess UX can be built in without collapsing everything into one hardcoded board layer.
228
+
229
+ ## Architecture
230
+
231
+ `@mirasen/chessboard` is built around explicit boundaries:
232
+
233
+ - runtime owns board, interaction, and update semantics
234
+ - renderer owns SVG structure and visual layers
235
+ - animation owns transition planning and temporary visual occupancy
236
+ - extensions render through declared slots instead of ad hoc DOM access
237
+
238
+ The goal is to make common chess interaction powerful without making the system brittle.
239
+
240
+ ## Not just a primitive, not a monolith
241
+
242
+ The project is intentionally positioned between two weak extremes:
243
+
244
+ ### Primitive-only board
245
+
246
+ A board that only renders pieces and leaves everything else to app code often forces consumers to rebuild the same interaction UX repeatedly.
247
+
248
+ ### Hardcoded monolith
249
+
250
+ A board that handles everything internally without clean boundaries becomes difficult to evolve, customize, or extend.
251
+
252
+ `@mirasen/chessboard` aims for a middle path:
253
+
254
+ - strong built-in baseline interaction
255
+ - clean internal architecture
256
+ - extension-driven feature growth
257
+
258
+ ## Status
85
259
 
86
- ## Versioning note
260
+ Release candidate for version `1.0.0`, focused on a strong built-in interaction baseline and extension-driven architecture.
87
261
 
88
- `0.1.x` is an early public package release.
262
+ ## Roadmap after RC
89
263
 
90
- It exists so the package can be installed, explored, integrated experimentally, and published publicly while the library continues moving toward a more complete `1.0.0`.
264
+ After the first release candidate, the next steps are expected to include:
91
265
 
92
- Breaking changes are still possible before `1.0.0`.
266
+ - remaining test hardening
267
+ - documentation expansion
268
+ - more examples
269
+ - further polish toward final `1.0.0`
93
270
 
94
- ## License
271
+ ## Project direction
95
272
 
96
- MIT
273
+ This platform is being built as a foundation for richer chess learning and interaction workflows, but the board itself is designed to stand as a serious open-source UI platform in its own right.
@@ -1,6 +1,11 @@
1
- import { Square } from '../state/board/types/internal.js';
2
- import { BoardStateSnapshot } from '../state/board/types/main.js';
3
- import { AnimationPlan, AnimationTrack, CalculateAnimationTracksOptions } from './types.js';
4
- export declare function collectSuppressedSquares(tracks: AnimationTrack[]): ReadonlySet<Square>;
5
- export declare function calculateAnimationTracks(pos1: BoardStateSnapshot, pos2: BoardStateSnapshot, options?: CalculateAnimationTracksOptions): AnimationTrack[];
6
- export declare function calculateAnimationPlan(pos1: BoardStateSnapshot, pos2: BoardStateSnapshot, sessionId: number, options?: CalculateAnimationTracksOptions): AnimationPlan;
1
+ import type { AnimationPlan, AnimationPlanningInput } from './types.js';
2
+ /**
3
+ * Plan animation tracks and suppressed squares for a state transition.
4
+ *
5
+ * This is the single entry point for animation planning. It:
6
+ * 1. Normalizes both snapshots (incorporates deferredUIMoveRequest).
7
+ * 2. Detects lifted-piece-drag drop completion and builds an internal exclude.
8
+ * 3. Computes tracks via greedy min-distance matching.
9
+ * 4. Assembles the full suppressedSquares set (track-derived + excluded drop endpoints).
10
+ */
11
+ export declare function calculateAnimationPlan(input: AnimationPlanningInput): AnimationPlan;
@@ -1,9 +1,11 @@
1
+ import assert from '@ktarmyshov/assert';
1
2
  import { isNonEmptyPieceCode } from '../state/board/check.js';
2
3
  import { fileOf, rankOf } from '../state/board/coords.js';
3
4
  import { SQUARE_COUNT } from '../state/board/types/internal.js';
4
- import { isMoveExclude, isSquareExclude } from './types.js';
5
+ // ---------------------------------------------------------------------------
5
6
  // Precomputed squared distance: (dFile² + dRank²) for all square pairs.
6
- // Max value = 7² + 7² = 98, fits in Uint8. Index: a * 64 + b.
7
+ // Max value = 7² + 7² = 98, fits in Uint8. Index: a * 64 + b.
8
+ // ---------------------------------------------------------------------------
7
9
  const SQUARE_DIST = new Uint8Array(SQUARE_COUNT * SQUARE_COUNT);
8
10
  for (let a = 0; a < SQUARE_COUNT; a++) {
9
11
  for (let b = 0; b < SQUARE_COUNT; b++) {
@@ -12,7 +14,107 @@ for (let a = 0; a < SQUARE_COUNT; a++) {
12
14
  SQUARE_DIST[a * SQUARE_COUNT + b] = df * df + dr * dr;
13
15
  }
14
16
  }
15
- export function collectSuppressedSquares(tracks) {
17
+ const EMPTY_EXCLUDE = {
18
+ move: new Map(),
19
+ square: new Set()
20
+ };
21
+ /**
22
+ * Detect whether a lifted-piece-drag was just completed (dropped).
23
+ *
24
+ * Condition:
25
+ * - previous snapshot had an active lifted-piece-drag session
26
+ * - current snapshot has no drag session
27
+ * - current snapshot has a lastMove
28
+ *
29
+ * Returns the lastMove when the condition holds, otherwise null.
30
+ */
31
+ function detectLiftedPieceDrop(previous, current) {
32
+ const prevDrag = previous.interaction.dragSession;
33
+ if (!prevDrag || prevDrag.type !== 'lifted-piece-drag')
34
+ return null;
35
+ if (current.interaction.dragSession !== null)
36
+ return null;
37
+ return current.change.lastMove ?? null;
38
+ }
39
+ /**
40
+ * Build an ExcludeSpec that suppresses the move track and both endpoint
41
+ * fades for a given lastMove (used when a lifted-piece drop is detected).
42
+ */
43
+ function buildExcludeForDrop(lastMove) {
44
+ const moveMap = new Map();
45
+ moveMap.set(lastMove.from, new Set([lastMove.to]));
46
+ const squareSet = new Set([lastMove.from, lastMove.to]);
47
+ return { move: moveMap, square: squareSet };
48
+ }
49
+ /**
50
+ * Normalize a snapshot by incorporating a pending deferredUIMoveRequest
51
+ * into the board and lastMove. Currently limited to pawn moves.
52
+ *
53
+ * If there is no deferred request the snapshot is returned as-is.
54
+ */
55
+ function buildEffectiveAnimationPlanningSnapshot(state) {
56
+ const request = state.change.deferredUIMoveRequest;
57
+ if (request === null)
58
+ return state; // nothing to do
59
+ const newPieces = new Uint8Array(state.board.pieces);
60
+ const from = request.sourceSquare;
61
+ const to = request.destination.to;
62
+ const pieceCode = state.board.pieces[from];
63
+ assert(pieceCode === 1 /* PieceCode.WhitePawn */ || pieceCode === 9 /* PieceCode.BlackPawn */, 'Only pawn moves should be deferred UI move requests at this time');
64
+ newPieces[to] = pieceCode;
65
+ newPieces[from] = 0 /* PieceCode.Empty */;
66
+ const newBoard = {
67
+ ...state.board,
68
+ pieces: newPieces
69
+ };
70
+ const newLastMove = {
71
+ from,
72
+ to,
73
+ piece: pieceCode
74
+ };
75
+ const newChange = {
76
+ deferredUIMoveRequest: null,
77
+ lastMove: newLastMove
78
+ };
79
+ return {
80
+ ...state,
81
+ board: newBoard,
82
+ change: newChange
83
+ };
84
+ }
85
+ /**
86
+ * When a promotion was already auto-resolved/auto-promoted (no deferredUIMoveRequest but
87
+ * lastMove.promotedTo is set), the current board already contains the promoted
88
+ * piece (e.g. queen) on the target square. For animation planning we
89
+ * temporarily replace it with the original piece (pawn) so that the greedy
90
+ * matching can build a proper move track from→to instead of separate fades.
91
+ *
92
+ * After the animation finishes the promoted piece will "snap" into view on
93
+ * the next render frame.
94
+ */
95
+ function buildPromotionEffectiveCurrent(state) {
96
+ const lastMove = state.change.lastMove;
97
+ if (state.change.deferredUIMoveRequest !== null ||
98
+ lastMove === null ||
99
+ lastMove.promotedTo === undefined) {
100
+ return state; // not an auto-resolved/auto-promoted move — nothing to do
101
+ }
102
+ const newPieces = new Uint8Array(state.board.pieces);
103
+ newPieces[lastMove.to] = lastMove.piece;
104
+ const newBoard = {
105
+ ...state.board,
106
+ pieces: newPieces
107
+ };
108
+ return {
109
+ ...state,
110
+ board: newBoard
111
+ };
112
+ }
113
+ /**
114
+ * Collect suppressed squares purely from the given tracks.
115
+ * For move tracks both endpoints are included; for fade/static the single square.
116
+ */
117
+ function collectSuppressedSquaresFromTracks(tracks) {
16
118
  const result = new Set();
17
119
  for (const track of tracks) {
18
120
  if (track.effect === 'move') {
@@ -25,14 +127,20 @@ export function collectSuppressedSquares(tracks) {
25
127
  }
26
128
  return result;
27
129
  }
28
- export function calculateAnimationTracks(pos1, pos2, options) {
130
+ /**
131
+ * Low-level track calculation.
132
+ *
133
+ * Compares two board snapshots (already derived / effective), applies the
134
+ * provided exclude spec, and returns the resulting animation tracks.
135
+ */
136
+ function calculateTracks(prevBoard, currBoard, exclude) {
29
137
  const tracks = [];
30
138
  let nextId = 0;
31
139
  const removed = [];
32
140
  const added = [];
33
141
  for (let sq = 0; sq < SQUARE_COUNT; sq++) {
34
- const c1 = pos1.pieces[sq];
35
- const c2 = pos2.pieces[sq];
142
+ const c1 = prevBoard.pieces[sq];
143
+ const c2 = currBoard.pieces[sq];
36
144
  if (c1 === c2)
37
145
  continue;
38
146
  if (isNonEmptyPieceCode(c1))
@@ -43,29 +151,29 @@ export function calculateAnimationTracks(pos1, pos2, options) {
43
151
  // Step 2: greedy min-distance matching of same-code pairs → move tracks
44
152
  const removedMatched = new Uint8Array(removed.length);
45
153
  const addedMatched = new Uint8Array(added.length);
46
- // toSq → code of the piece that moved there (used for static detection)
47
- const movedToSqCode = new Map();
48
154
  const candidates = [];
49
155
  for (let ri = 0; ri < removed.length; ri++) {
50
156
  for (let ai = 0; ai < added.length; ai++) {
51
157
  if (removed[ri].pieceCode !== added[ai].pieceCode)
52
158
  continue;
53
- candidates.push({ dist: SQUARE_DIST[removed[ri].sq * SQUARE_COUNT + added[ai].sq], ri, ai });
159
+ candidates.push({
160
+ dist: SQUARE_DIST[removed[ri].sq * SQUARE_COUNT + added[ai].sq],
161
+ ri,
162
+ ai
163
+ });
54
164
  }
55
165
  }
56
166
  candidates.sort((a, b) => a.dist - b.dist);
57
- const excl = options?.exclude;
58
- const preparedExcl = prepareExclude(excl);
59
167
  for (const { ri, ai } of candidates) {
60
168
  if (removedMatched[ri] || addedMatched[ai])
61
169
  continue;
62
170
  removedMatched[ri] = 1;
63
171
  addedMatched[ai] = 1;
64
- movedToSqCode.set(added[ai].sq, removed[ri].pieceCode);
65
- if (excl &&
66
- preparedExcl.move.has(removed[ri].sq) &&
67
- preparedExcl.move.get(removed[ri].sq).has(added[ai].sq)) {
68
- // Suppress this move track but keep both squares matched so they don't become fades.
172
+ // Check if this move pair is excluded (e.g. lifted-piece drop)
173
+ const fromSet = exclude.move.get(removed[ri].sq);
174
+ if (fromSet && fromSet.has(added[ai].sq)) {
175
+ // Suppress the move track but keep both entries matched so they
176
+ // don't turn into fades.
69
177
  continue;
70
178
  }
71
179
  tracks.push({
@@ -80,9 +188,8 @@ export function calculateAnimationTracks(pos1, pos2, options) {
80
188
  for (let ai = 0; ai < added.length; ai++) {
81
189
  if (addedMatched[ai])
82
190
  continue;
83
- if (preparedExcl.square.has(added[ai].sq)) {
191
+ if (exclude.square.has(added[ai].sq))
84
192
  continue;
85
- }
86
193
  tracks.push({
87
194
  id: nextId++,
88
195
  pieceCode: added[ai].pieceCode,
@@ -90,39 +197,51 @@ export function calculateAnimationTracks(pos1, pos2, options) {
90
197
  effect: 'fade-in'
91
198
  });
92
199
  }
93
- // Step 4: unmatched removed → fade-out or static
200
+ // Step 4: unmatched removed → fade-out
94
201
  for (let ri = 0; ri < removed.length; ri++) {
95
202
  if (removedMatched[ri])
96
203
  continue;
97
- if (preparedExcl.square.has(removed[ri].sq)) {
204
+ if (exclude.square.has(removed[ri].sq))
98
205
  continue;
99
- }
100
- const { pieceCode: code, sq } = removed[ri];
101
- /*const movedCode = movedToSqCode.get(sq);
102
- const isCapture =
103
- movedCode !== undefined && fromPieceCode(code).color !== fromPieceCode(movedCode).color;*/
104
- const pieceCode = code;
105
- tracks.push({ id: nextId++, pieceCode, sq, effect: 'fade-out' });
206
+ tracks.push({
207
+ id: nextId++,
208
+ pieceCode: removed[ri].pieceCode,
209
+ sq: removed[ri].sq,
210
+ effect: 'fade-out'
211
+ });
106
212
  }
107
213
  return tracks;
108
214
  }
109
- export function calculateAnimationPlan(pos1, pos2, sessionId, options) {
110
- return { sessionId, tracks: calculateAnimationTracks(pos1, pos2, options) };
111
- }
112
- function prepareExclude(exclude) {
113
- const result = { move: new Map(), square: new Set() };
114
- if (!exclude)
115
- return result;
116
- for (const e of exclude) {
117
- if (isMoveExclude(e)) {
118
- if (!result.move.has(e.fromSq)) {
119
- result.move.set(e.fromSq, new Set());
120
- }
121
- result.move.get(e.fromSq).add(e.toSq);
122
- }
123
- else if (isSquareExclude(e)) {
124
- result.square.add(e.sq);
125
- }
215
+ // ---------------------------------------------------------------------------
216
+ // Public API — single orchestrator
217
+ // ---------------------------------------------------------------------------
218
+ /**
219
+ * Plan animation tracks and suppressed squares for a state transition.
220
+ *
221
+ * This is the single entry point for animation planning. It:
222
+ * 1. Normalizes both snapshots (incorporates deferredUIMoveRequest).
223
+ * 2. Detects lifted-piece-drag drop completion and builds an internal exclude.
224
+ * 3. Computes tracks via greedy min-distance matching.
225
+ * 4. Assembles the full suppressedSquares set (track-derived + excluded drop endpoints).
226
+ */
227
+ export function calculateAnimationPlan(input) {
228
+ // 1) Normalize snapshots to account for deferredUIMoveRequest
229
+ const effectivePrevious = buildEffectiveAnimationPlanningSnapshot(input.previous);
230
+ const effectiveCurrent = buildEffectiveAnimationPlanningSnapshot(input.current);
231
+ // 2) Detect lifted-piece drop and prepare exclude spec
232
+ const droppedMove = detectLiftedPieceDrop(effectivePrevious, effectiveCurrent);
233
+ const exclude = droppedMove !== null ? buildExcludeForDrop(droppedMove) : EMPTY_EXCLUDE;
234
+ // 3) Normalize current for auto-resolved/auto-promoted move (pawn→queen on target square)
235
+ const planningCurrent = buildPromotionEffectiveCurrent(effectiveCurrent);
236
+ // 4) Calculate tracks
237
+ const tracks = calculateTracks(effectivePrevious.board, planningCurrent.board, exclude);
238
+ // 5) Build suppressedSquares: track-derived + excluded drop endpoints
239
+ const suppressed = collectSuppressedSquaresFromTracks(tracks);
240
+ if (droppedMove !== null) {
241
+ // The dropped move was intentionally suppressed (no move track emitted),
242
+ // but the base render must still hide both endpoints.
243
+ suppressed.add(droppedMove.from);
244
+ suppressed.add(droppedMove.to);
126
245
  }
127
- return result;
246
+ return { tracks, suppressedSquares: suppressed };
128
247
  }
@@ -1,7 +1,7 @@
1
1
  import type { PieceUrls } from '../../extensions/first-party/main-renderer/types/internal.js';
2
- import type { RenderGeometry } from '../../layout/geometry/types.js';
2
+ import type { SceneRenderGeometry } from '../../layout/geometry/types.js';
3
3
  import type { AnimationTrackFade } from '../types.js';
4
4
  import type { PreparedFadeNode } from './types.js';
5
- export declare function prepareFadeTrack(track: AnimationTrackFade, geometry: RenderGeometry, pieceUrls: PieceUrls, layer: SVGElement): PreparedFadeNode;
5
+ export declare function prepareFadeTrack(track: AnimationTrackFade, geometry: SceneRenderGeometry, pieceUrls: PieceUrls, layer: SVGElement): PreparedFadeNode;
6
6
  export declare function renderFadeTrack(node: PreparedFadeNode, progress: number): void;
7
7
  export declare function cleanFadeTrack(node: PreparedFadeNode): void;
@@ -1,6 +1,6 @@
1
1
  import { createSvgElement, updateElementAttributes } from '../../render/svg/helpers.js';
2
2
  export function prepareFadeTrack(track, geometry, pieceUrls, layer) {
3
- const r = geometry.squareRect(track.sq);
3
+ const r = geometry.getSquareRect(track.sq);
4
4
  const url = pieceUrls[track.pieceCode];
5
5
  const initialOpacity = track.effect === 'fade-in' ? '0' : '1';
6
6
  const root = createSvgElement(layer, 'image', {
@@ -9,8 +9,8 @@ export function prepareFadeTrack(track, geometry, pieceUrls, layer) {
9
9
  href: url,
10
10
  x: r.x.toString(),
11
11
  y: r.y.toString(),
12
- width: r.size.toString(),
13
- height: r.size.toString(),
12
+ width: r.width.toString(),
13
+ height: r.height.toString(),
14
14
  opacity: initialOpacity
15
15
  });
16
16
  return { ...track, root };
@@ -1,7 +1,7 @@
1
1
  import type { PieceUrls } from '../../extensions/first-party/main-renderer/types/internal.js';
2
- import type { RenderGeometry } from '../../layout/geometry/types.js';
2
+ import type { SceneRenderGeometry } from '../../layout/geometry/types.js';
3
3
  import type { AnimationTrackMove } from '../types.js';
4
4
  import type { PreparedMoveNode } from './types.js';
5
- export declare function prepareMoveTrack(track: AnimationTrackMove, geometry: RenderGeometry, pieceUrls: PieceUrls, layer: SVGElement): PreparedMoveNode;
6
- export declare function renderMoveTrack(node: PreparedMoveNode, geometry: RenderGeometry, progress: number): void;
5
+ export declare function prepareMoveTrack(track: AnimationTrackMove, geometry: SceneRenderGeometry, pieceUrls: PieceUrls, layer: SVGElement): PreparedMoveNode;
6
+ export declare function renderMoveTrack(node: PreparedMoveNode, geometry: SceneRenderGeometry, progress: number): void;
7
7
  export declare function cleanMoveTrack(node: PreparedMoveNode): void;