@mirasen/chessboard 0.1.1 → 1.0.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 (139) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +240 -45
  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/build/index.d.ts +21 -0
  15. package/dist/extensions/build/index.js +9 -0
  16. package/dist/extensions/factory/main.js +23 -2
  17. package/dist/extensions/factory/runtime.d.ts +2 -1
  18. package/dist/extensions/factory/runtime.js +48 -4
  19. package/dist/extensions/first-party/active-target/factory.js +9 -9
  20. package/dist/extensions/first-party/active-target/types.d.ts +2 -2
  21. package/dist/extensions/first-party/active-target/types.js +4 -1
  22. package/dist/extensions/first-party/auto-promote/factory.d.ts +2 -0
  23. package/dist/extensions/first-party/auto-promote/factory.js +51 -0
  24. package/dist/extensions/first-party/auto-promote/types.d.ts +12 -0
  25. package/dist/extensions/first-party/auto-promote/types.js +2 -0
  26. package/dist/extensions/first-party/board-events/factory.js +1 -16
  27. package/dist/extensions/first-party/board-events/types.d.ts +1 -4
  28. package/dist/extensions/first-party/common/helpers.d.ts +7 -7
  29. package/dist/extensions/first-party/common/helpers.js +11 -11
  30. package/dist/extensions/first-party/common/types.d.ts +2 -3
  31. package/dist/extensions/first-party/last-move/factory.js +10 -10
  32. package/dist/extensions/first-party/last-move/types.d.ts +2 -2
  33. package/dist/extensions/first-party/legal-moves/factory.js +5 -5
  34. package/dist/extensions/first-party/legal-moves/types.d.ts +2 -2
  35. package/dist/extensions/first-party/main-renderer/animation/render.js +1 -2
  36. package/dist/extensions/first-party/main-renderer/animation/update.js +9 -23
  37. package/dist/extensions/first-party/main-renderer/board/render.js +3 -3
  38. package/dist/extensions/first-party/main-renderer/board/update.js +5 -3
  39. package/dist/extensions/first-party/main-renderer/coordinates/render.js +4 -4
  40. package/dist/extensions/first-party/main-renderer/drag/render.js +1 -1
  41. package/dist/extensions/first-party/main-renderer/factory.js +4 -4
  42. package/dist/extensions/first-party/main-renderer/normalize.d.ts +2 -3
  43. package/dist/extensions/first-party/main-renderer/normalize.js +1 -1
  44. package/dist/extensions/first-party/main-renderer/pieces/render.js +5 -5
  45. package/dist/extensions/first-party/main-renderer/pieces/suppress.js +7 -2
  46. package/dist/extensions/first-party/main-renderer/types/extension.d.ts +1 -1
  47. package/dist/extensions/first-party/main-renderer/types/extension.js +1 -1
  48. package/dist/extensions/first-party/main-renderer/types/instance.d.ts +2 -2
  49. package/dist/extensions/first-party/promotion/factory.d.ts +3 -0
  50. package/dist/extensions/first-party/promotion/factory.js +219 -0
  51. package/dist/extensions/first-party/promotion/normalize.d.ts +3 -0
  52. package/dist/extensions/first-party/promotion/normalize.js +37 -0
  53. package/dist/extensions/first-party/promotion/types/input.d.ts +10 -0
  54. package/dist/extensions/first-party/promotion/types/internal.d.ts +12 -0
  55. package/dist/extensions/first-party/promotion/types/internal.js +22 -0
  56. package/dist/extensions/first-party/promotion/types/main.d.ts +24 -0
  57. package/dist/extensions/first-party/promotion/types/main.js +2 -0
  58. package/dist/extensions/first-party/selected-square/factory.js +7 -7
  59. package/dist/extensions/first-party/selected-square/types.d.ts +2 -2
  60. package/dist/extensions/index.d.ts +1 -8
  61. package/dist/extensions/index.js +1 -6
  62. package/dist/extensions/types/basic/events.d.ts +8 -31
  63. package/dist/extensions/types/basic/events.js +9 -5
  64. package/dist/extensions/types/basic/interaction.d.ts +21 -0
  65. package/dist/extensions/types/basic/transient-visuals.d.ts +2 -1
  66. package/dist/extensions/types/context/events.d.ts +5 -0
  67. package/dist/extensions/types/context/events.js +1 -0
  68. package/dist/extensions/types/context/ui-move.d.ts +3 -15
  69. package/dist/extensions/types/extension.d.ts +10 -8
  70. package/dist/extensions/types/main.d.ts +11 -6
  71. package/dist/extensions/types/surface/commands.d.ts +9 -0
  72. package/dist/extensions/types/surface/events.d.ts +2 -3
  73. package/dist/extensions/types/wrapper.d.ts +23 -22
  74. package/dist/extensions/types/wrapper.js +34 -6
  75. package/dist/extensions/update.d.ts +2 -2
  76. package/dist/extensions/update.js +1 -1
  77. package/dist/index.d.ts +1 -2
  78. package/dist/layout/factory.js +3 -3
  79. package/dist/layout/geometry/factory.d.ts +2 -11
  80. package/dist/layout/geometry/factory.js +16 -17
  81. package/dist/layout/geometry/helpers.d.ts +5 -0
  82. package/dist/layout/geometry/helpers.js +33 -0
  83. package/dist/layout/geometry/types.d.ts +13 -12
  84. package/dist/layout/helpers.d.ts +3 -4
  85. package/dist/layout/helpers.js +5 -5
  86. package/dist/layout/reducers.js +9 -8
  87. package/dist/layout/types.d.ts +5 -5
  88. package/dist/render/mount.js +2 -2
  89. package/dist/render/rendering/state.js +7 -7
  90. package/dist/runtime/factory/events.d.ts +3 -0
  91. package/dist/runtime/factory/events.js +18 -0
  92. package/dist/runtime/factory/helpers.js +12 -12
  93. package/dist/runtime/factory/input.js +17 -10
  94. package/dist/runtime/factory/main.js +44 -4
  95. package/dist/runtime/input/adapter/event.d.ts +3 -0
  96. package/dist/runtime/input/adapter/event.js +19 -0
  97. package/dist/runtime/input/adapter/factory.js +19 -38
  98. package/dist/runtime/input/adapter/helpers.d.ts +3 -5
  99. package/dist/runtime/input/adapter/helpers.js +60 -12
  100. package/dist/runtime/input/adapter/pointer.d.ts +6 -10
  101. package/dist/runtime/input/adapter/pointer.js +37 -70
  102. package/dist/runtime/input/adapter/types.d.ts +6 -3
  103. package/dist/runtime/input/adapter/types.js +6 -1
  104. package/dist/runtime/input/controller/factory.js +12 -11
  105. package/dist/runtime/input/controller/pointer.d.ts +5 -6
  106. package/dist/runtime/input/controller/pointer.js +36 -35
  107. package/dist/runtime/input/controller/transient-visuals.d.ts +2 -2
  108. package/dist/runtime/input/controller/transient-visuals.js +12 -5
  109. package/dist/runtime/input/controller/types.d.ts +5 -4
  110. package/dist/runtime/lifecycle.js +6 -6
  111. package/dist/runtime/mutation/interaction.js +1 -5
  112. package/dist/runtime/mutation/types.d.ts +10 -8
  113. package/dist/runtime/types/main.d.ts +2 -0
  114. package/dist/state/change/{factory.d.ts → factory/main.d.ts} +1 -1
  115. package/dist/state/change/{factory.js → factory/main.js} +8 -5
  116. package/dist/state/change/factory/ui-move.d.ts +4 -0
  117. package/dist/{runtime → state/change}/factory/ui-move.js +9 -9
  118. package/dist/state/change/helpers.d.ts +4 -2
  119. package/dist/state/change/helpers.js +11 -1
  120. package/dist/state/change/mutation.d.ts +1 -1
  121. package/dist/state/change/reducers.d.ts +3 -3
  122. package/dist/state/change/reducers.js +4 -4
  123. package/dist/state/change/types/main.d.ts +18 -0
  124. package/dist/state/change/types/main.js +1 -0
  125. package/dist/state/change/types/ui-move.d.ts +25 -0
  126. package/dist/state/change/types/ui-move.js +1 -0
  127. package/dist/state/factory.js +1 -1
  128. package/dist/state/interaction/factory.js +4 -2
  129. package/dist/state/interaction/helpers.d.ts +2 -0
  130. package/dist/state/interaction/helpers.js +3 -0
  131. package/dist/state/interaction/types/internal.d.ts +9 -1
  132. package/dist/state/types.d.ts +1 -1
  133. package/dist/wrapper/factory.js +2 -26
  134. package/package.json +17 -11
  135. package/dist/runtime/factory/ui-move.d.ts +0 -4
  136. package/dist/runtime/types/ui-move.d.ts +0 -9
  137. package/dist/state/change/types.d.ts +0 -17
  138. /package/dist/{runtime/types/ui-move.js → extensions/first-party/promotion/types/input.js} +0 -0
  139. /package/dist/{state/change/types.js → extensions/types/basic/interaction.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @mirasen/chessboard
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 2609920: First v1.0.0 release with the new extension-driven architecture, built-in chess interaction baseline, promotion flow, and auto-promote support.
8
+
9
+ ### Patch Changes
10
+
11
+ - 31364b7: dependabot: directory '/', update @sveltejs/kit
12
+ - 31364b7: dependabot: directory '/', update @vitest/browser-playwright
13
+ - 31364b7: dependabot: directory '/', update eslint-plugin-svelte
14
+ - 31364b7: dependabot: directory '/', update svelte
15
+ - 2609920: Added promotion and auto-promotion to queen extensions
16
+
17
+ ## 1.0.0-rc.0
18
+
19
+ ### Major Changes
20
+
21
+ - 2609920: First 1.0 release candidate with the new extension-driven architecture, built-in chess interaction baseline, promotion flow, and auto-promote support.
22
+
23
+ ### Patch Changes
24
+
25
+ - 2609920: Added promotion and auto-promotion to queen extensions
26
+
3
27
  ## 0.1.1
4
28
 
5
29
  ### Patch Changes
package/README.md CHANGED
@@ -1,50 +1,51 @@
1
1
  [![NPM Version](https://img.shields.io/npm/v/%40mirasen%2Fchessboard)](https://www.npmjs.com/package/@mirasen/chessboard)
2
2
  [![CI](https://github.com/mirasen-io/chessboard/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/mirasen-io/chessboard/actions/workflows/ci.yml)
3
- [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=mirasen-io_chessboard&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=mirasen-io_chessboard)
4
- [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=mirasen-io_chessboard&metric=coverage)](https://sonarcloud.io/summary/new_code?id=mirasen-io_chessboard)
5
3
  [![License](https://img.shields.io/npm/l/@mirasen/chessboard)](./LICENSE)
6
4
 
7
5
  # @mirasen/chessboard
8
6
 
9
- Framework-agnostic TypeScript chessboard library focused on clean architecture, explicit rendering ownership, and extension-driven features.
7
+ A framework-agnostic TypeScript chessboard platform with real chess interaction built in and an extension-driven architecture.
10
8
 
11
- ## Status
9
+ ## Why this exists
12
10
 
13
- **Early public release (`0.1.x`)**
11
+ Most chessboards fall into one of two buckets:
14
12
 
15
- The core architecture is already usable, but the package is still pre-`1.0.0` and the API may evolve.
13
+ - rendering primitives that leave core interaction UX to app code
14
+ - tightly coupled monoliths that are hard to extend cleanly
16
15
 
17
- Current focus:
16
+ `@mirasen/chessboard` aims for a better balance:
18
17
 
19
- - stable core runtime
20
- - extension-driven rendering and interaction
21
- - public wrapper / package API
22
- - early integration ergonomics
18
+ - meaningful built-in chess interaction out of the box
19
+ - first-party extensions for common board UX
20
+ - explicit rendering and ownership boundaries
21
+ - extension-driven growth without turning the board into glue hell
23
22
 
24
- Not finalized yet:
23
+ This is not just a board renderer. It is a chessboard platform designed for real chess interaction.
25
24
 
26
- - promotion flow
27
- - some higher-level public APIs
28
- - broader test coverage
29
- - final `1.0.0` API guarantees
25
+ ## What you get out of the box
30
26
 
31
- ## Goals
27
+ The built-in first-party extension baseline currently includes both foundational runtime-facing extensions and user-facing board features.
32
28
 
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
29
+ - `renderer` — the first-party rendering extension that validates the same extension architecture used for board features
30
+ - `events`
31
+ - `selectedSquare`
32
+ - `activeTarget`
33
+ - `legalMoves`
34
+ - `lastMove`
35
+ - `promotion`
36
+ - `autoPromote`
38
37
 
39
- ## Current features
38
+ That means the board already covers a meaningful baseline chess UX instead of stopping at “draw squares and pieces”.
40
39
 
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(...)`
40
+ ## Highlights
41
+
42
+ - Real built-in chess interaction
43
+ - Extension-driven architecture
44
+ - First-party baseline extensions
45
+ - Explicit SVG slot ownership
46
+ - Clean separation between runtime, rendering, interaction, and animation
47
+ - Promotion flow built into the board pipeline
48
+ - Optional auto-promotion as a separate behavior extension
48
49
 
49
50
  ## Installation
50
51
 
@@ -52,20 +53,226 @@ Not finalized yet:
52
53
  npm install @mirasen/chessboard
53
54
  ```
54
55
 
55
- ## Example
56
+ ## Usage
57
+
58
+ ### Minimal example
56
59
 
57
60
  ```ts
58
61
  import { createBoard } from '@mirasen/chessboard';
59
62
 
63
+ const element = document.getElementById('board');
64
+
65
+ if (!element) {
66
+ throw new Error('Missing board element');
67
+ }
68
+
60
69
  const board = createBoard({
61
- // API still evolving in 0.1.x
70
+ element
71
+ });
72
+ // Set free movability, default - disabled
73
+ board.setMovability({
74
+ mode: 'free'
62
75
  });
76
+ ```
77
+
78
+ ### Explicit extensions
79
+
80
+ ```ts
81
+ import { createBoard, type Chessboard, type ChessboardExtensionInput } from '@mirasen/chessboard';
82
+
83
+ const extensionList = [
84
+ // Always add renderer first, otherwise nothing will be rendered
85
+ 'renderer',
86
+ 'lastMove',
87
+ // Keep autoPromote before promotion, otherwise promotion may defer the move first
88
+ 'autoPromote',
89
+ 'promotion'
90
+ // customExtensionDefinition
91
+ ] as const satisfies readonly ChessboardExtensionInput[];
92
+ let board: Chessboard<typeof extensionList> | null = null;
93
+
94
+ const element = document.getElementById('board');
95
+
96
+ if (!element) {
97
+ throw new Error('Missing board element');
98
+ }
99
+
100
+ board = createBoard({
101
+ element,
102
+ extensions: extensionList
103
+ });
104
+ ```
63
105
 
64
- board.extensions.events.setOnUiMove((move) => {
106
+ ### setOnUIMove
107
+
108
+ ```ts
109
+ import { createBoard } from '@mirasen/chessboard';
110
+
111
+ const element = document.getElementById('board');
112
+
113
+ if (!element) {
114
+ throw new Error('Missing board element');
115
+ }
116
+
117
+ const board = createBoard({
118
+ element,
119
+ state: {
120
+ board: 'start',
121
+ interaction: {
122
+ movability: {
123
+ mode: 'free'
124
+ }
125
+ }
126
+ }
127
+ });
128
+
129
+ board.extensions.events.setOnUIMove((move) => {
65
130
  console.log('UI move:', move);
66
131
  });
67
132
  ```
68
133
 
134
+ ### setMovability - strict with explicit destinations
135
+
136
+ ```ts
137
+ import { createBoard } from '@mirasen/chessboard';
138
+
139
+ const element = document.getElementById('board');
140
+
141
+ if (!element) {
142
+ throw new Error('Missing board element');
143
+ }
144
+
145
+ const board = createBoard({
146
+ element
147
+ });
148
+
149
+ board.setMovability({
150
+ mode: 'strict',
151
+ destinations: {
152
+ e2: [{ to: 'e3' }, { to: 'e4' }],
153
+ d7: [
154
+ { to: 'c8', promotedTo: ['queen', 'rook', 'bishop', 'knight'] },
155
+ { to: 'd8', promotedTo: ['queen', 'rook', 'bishop', 'knight'] }
156
+ ],
157
+ g1: [{ to: 'f3' }, { to: 'h3' }]
158
+ }
159
+ });
160
+ ```
161
+
162
+ ### setMovability - strict with a resolver
163
+
164
+ ```ts
165
+ import { createBoard } from '@mirasen/chessboard';
166
+
167
+ const element = document.getElementById('board');
168
+
169
+ if (!element) {
170
+ throw new Error('Missing board element');
171
+ }
172
+
173
+ const board = createBoard({
174
+ element
175
+ });
176
+
177
+ board.setMovability({
178
+ mode: 'strict',
179
+ destinations: (source) => {
180
+ if (source === 'e2')
181
+ return [
182
+ { to: 'e8', promotedTo: ['bishop', 'queen'] },
183
+ { to: 'd8', promotedTo: ['rook', 'knight', 'queen'] }
184
+ ];
185
+ if (source === 'f7')
186
+ return [
187
+ { to: 'f8', promotedTo: ['bishop', 'rook', 'knight', 'queen'] },
188
+ { to: 'g8', promotedTo: ['bishop', 'rook', 'knight', 'queen'] }
189
+ ];
190
+ if (source === 'd7')
191
+ return [
192
+ { to: 'd1', promotedTo: ['bishop', 'knight'] },
193
+ { to: 'e1', promotedTo: ['rook', 'knight', 'queen'] }
194
+ ];
195
+ if (source === 'c2')
196
+ return [
197
+ { to: 'c1', promotedTo: ['bishop', 'rook', 'knight', 'queen'] },
198
+ { to: 'b1', promotedTo: ['bishop', 'rook', 'knight', 'queen'] }
199
+ ];
200
+ return undefined;
201
+ }
202
+ });
203
+ ```
204
+
205
+ ## Promotion and auto-promotion
206
+
207
+ Promotion is handled inside the board interaction pipeline through a natural deferred move flow.
208
+
209
+ That means the board does not need the awkward pattern of:
210
+
211
+ - reject move
212
+ - open external dialog
213
+ - replay the move later
214
+
215
+ Instead:
216
+
217
+ - `promotion` provides the built-in promotion chooser UI
218
+ - `autoPromote` provides behavior-only auto-promotion logic
219
+
220
+ These are separate extensions on purpose:
221
+
222
+ - `promotion` is UI
223
+ - `autoPromote` is behavior
224
+
225
+ This separation is a good example of the architecture: common chess UX can be built in without collapsing everything into one hardcoded board layer.
226
+
227
+ ```ts
228
+ import { createBoard } from '@mirasen/chessboard';
229
+
230
+ const element = document.getElementById('board');
231
+
232
+ if (!element) {
233
+ throw new Error('Missing board element');
234
+ }
235
+
236
+ const board = createBoard({
237
+ element
238
+ });
239
+
240
+ board.extensions.autoPromote.toQueen = true;
241
+ ```
242
+
243
+ ## Architecture
244
+
245
+ `@mirasen/chessboard` is built around explicit boundaries:
246
+
247
+ - runtime owns board, interaction, and update semantics
248
+ - renderer owns SVG structure and visual layers
249
+ - animation owns transition planning and temporary visual occupancy
250
+ - extensions render through declared slots instead of ad hoc DOM access
251
+
252
+ The goal is to make common chess interaction powerful without making the system brittle.
253
+
254
+ ## Not just a primitive, not a monolith
255
+
256
+ The project is intentionally positioned between two weak extremes:
257
+
258
+ ### Primitive-only board
259
+
260
+ A board that only renders pieces and leaves everything else to app code often forces consumers to rebuild the same interaction UX repeatedly.
261
+
262
+ ### Hardcoded monolith
263
+
264
+ A board that handles everything internally without clean boundaries becomes difficult to evolve, customize, or extend.
265
+
266
+ `@mirasen/chessboard` aims for a middle path:
267
+
268
+ - strong built-in baseline interaction
269
+ - clean internal architecture
270
+ - extension-driven feature growth
271
+
272
+ ## Project direction
273
+
274
+ 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.
275
+
69
276
  ## Artwork
70
277
 
71
278
  ### Chessnut piece set
@@ -82,15 +289,3 @@ For details, see:
82
289
 
83
290
  - [./assets/pieces/chessnut/ATTRIBUTION.md](./assets/pieces/chessnut/ATTRIBUTION.md)
84
291
  - [./assets/pieces/chessnut/LICENSE.txt](./assets/pieces/chessnut/LICENSE.txt)
85
-
86
- ## Versioning note
87
-
88
- `0.1.x` is an early public package release.
89
-
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`.
91
-
92
- Breaking changes are still possible before `1.0.0`.
93
-
94
- ## License
95
-
96
- MIT
@@ -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;