@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
@@ -2,7 +2,7 @@ import assert from '@ktarmyshov/assert';
2
2
  import { toMerged } from 'es-toolkit';
3
3
  import { createSvgElement, updateElementAttributes } from '../../../render/svg/helpers.js';
4
4
  import { isUpdateContextRenderable } from '../../types/context/update.js';
5
- import { extensionCreateInternalBase, extensionDestroy, extensionMount, extensionUnmount } from '../common/helpers.js';
5
+ import { extensionCreateInternalBase, extensionDestroyBase, extensionMountBase, extensionUnmountBase } from '../common/helpers.js';
6
6
  import { DEFAULT_CONFIG, EXTENSION_ID, EXTENSION_SLOTS } from './types.js';
7
7
  export function createLastMove(config = {}) {
8
8
  const mergedConfig = toMerged(DEFAULT_CONFIG, config);
@@ -31,7 +31,7 @@ function createLastMoveInstance(config) {
31
31
  return {
32
32
  id: EXTENSION_ID,
33
33
  mount(env) {
34
- extensionMount(internalState, env.slotRoots);
34
+ extensionMountBase(internalState, env.slotRoots);
35
35
  },
36
36
  onUpdate(context) {
37
37
  const needsRender = context.mutation.hasMutation({
@@ -57,13 +57,13 @@ function createLastMoveInstance(config) {
57
57
  return;
58
58
  }
59
59
  const geometry = context.currentFrame.layout.geometry;
60
- const rFrom = geometry.squareRect(fromSq);
61
- const rTo = geometry.squareRect(toSq);
60
+ const rFrom = geometry.getSquareRect(fromSq);
61
+ const rTo = geometry.getSquareRect(toSq);
62
62
  const rectFromAttributes = {
63
63
  x: rFrom.x.toString(),
64
64
  y: rFrom.y.toString(),
65
- width: rFrom.size.toString(),
66
- height: rFrom.size.toString(),
65
+ width: rFrom.width.toString(),
66
+ height: rFrom.height.toString(),
67
67
  fill: internalState.config.color,
68
68
  'fill-opacity': internalState.config.opacity.toString(),
69
69
  'shape-rendering': 'crispEdges'
@@ -71,8 +71,8 @@ function createLastMoveInstance(config) {
71
71
  const rectToAttributes = {
72
72
  x: rTo.x.toString(),
73
73
  y: rTo.y.toString(),
74
- width: rTo.size.toString(),
75
- height: rTo.size.toString(),
74
+ width: rTo.width.toString(),
75
+ height: rTo.height.toString(),
76
76
  fill: internalState.config.color,
77
77
  'fill-opacity': internalState.config.opacity.toString(),
78
78
  'shape-rendering': 'crispEdges'
@@ -95,11 +95,11 @@ function createLastMoveInstance(config) {
95
95
  }
96
96
  },
97
97
  unmount() {
98
- extensionUnmount(internalState);
98
+ extensionUnmountBase(internalState);
99
99
  extensionClean(internalState);
100
100
  },
101
101
  destroy() {
102
- extensionDestroy(internalState);
102
+ extensionDestroyBase(internalState);
103
103
  extensionClean(internalState);
104
104
  }
105
105
  };
@@ -1,6 +1,6 @@
1
1
  import { PartialDeep } from 'type-fest';
2
2
  import { ExtensionDefinition, ExtensionInstance } from '../../types/extension.js';
3
- import { ExtensionInternal, OpaqueColor } from '../common/types.js';
3
+ import { ExtensionInternalBase, OpaqueColor } from '../common/types.js';
4
4
  export declare const EXTENSION_SLOTS: readonly ["underPieces"];
5
5
  export type ExtensionSlotsType = typeof EXTENSION_SLOTS;
6
6
  export declare const EXTENSION_ID: "lastMove";
@@ -9,7 +9,7 @@ export declare const DEFAULT_CONFIG: LastMoveConfig;
9
9
  export type LastMoveInitConfig = PartialDeep<LastMoveConfig>;
10
10
  export type LastMoveDefinition = ExtensionDefinition<typeof EXTENSION_ID, typeof EXTENSION_SLOTS, never>;
11
11
  export type LastMoveInstance = ExtensionInstance<typeof EXTENSION_ID, typeof EXTENSION_SLOTS, never>;
12
- export interface LastMoveInstanceInternal extends ExtensionInternal<ExtensionSlotsType> {
12
+ export interface LastMoveInstanceInternal extends ExtensionInternalBase<ExtensionSlotsType> {
13
13
  svgRectFrom: SVGRectElement | null;
14
14
  svgRectTo: SVGRectElement | null;
15
15
  readonly config: LastMoveConfig;
@@ -4,7 +4,7 @@ import { clearElementChildren, createSvgElement } from '../../../render/svg/help
4
4
  import { isNonEmptyPieceCode } from '../../../state/board/check.js';
5
5
  import { fromPieceCode } from '../../../state/board/piece.js';
6
6
  import { isUpdateContextRenderable } from '../../types/context/update.js';
7
- import { extensionCreateInternalBase, extensionDestroy, extensionMount, extensionUnmount } from '../common/helpers.js';
7
+ import { extensionCreateInternalBase, extensionDestroyBase, extensionMountBase, extensionUnmountBase } from '../common/helpers.js';
8
8
  import { DEFAULT_CONFIG, EXTENSION_ID, EXTENSION_SLOTS } from './types.js';
9
9
  export function createLegalMoves(config = {}) {
10
10
  const mergedConfig = toMerged(DEFAULT_CONFIG, config);
@@ -31,7 +31,7 @@ function createLegalMovesInstance(config) {
31
31
  return {
32
32
  id: EXTENSION_ID,
33
33
  mount(env) {
34
- extensionMount(internalState, env.slotRoots);
34
+ extensionMountBase(internalState, env.slotRoots);
35
35
  },
36
36
  onUpdate(context) {
37
37
  const needsRender = context.mutation.hasMutation({
@@ -70,7 +70,7 @@ function createLegalMovesInstance(config) {
70
70
  fill: 'none'
71
71
  }; // TODO: Remove, this is just for visual test
72
72
  for (const [square, destination] of interaction.activeDestinations) {
73
- const rect = geometry.squareRect(square);
73
+ const rect = geometry.getSquareRect(square);
74
74
  const circleX = rect.x + geometry.squareSize / 2;
75
75
  const circleY = rect.y + geometry.squareSize / 2;
76
76
  const selectedPieceCode = context.currentFrame.state.board.pieces[interaction.selected.square];
@@ -90,11 +90,11 @@ function createLegalMovesInstance(config) {
90
90
  }
91
91
  },
92
92
  unmount() {
93
- extensionUnmount(internalState);
93
+ extensionUnmountBase(internalState);
94
94
  extensionClean(internalState);
95
95
  },
96
96
  destroy() {
97
- extensionDestroy(internalState);
97
+ extensionDestroyBase(internalState);
98
98
  extensionClean(internalState);
99
99
  }
100
100
  };
@@ -1,6 +1,6 @@
1
1
  import { PartialDeep } from 'type-fest';
2
2
  import { ExtensionDefinition, ExtensionInstance } from '../../types/extension.js';
3
- import { ExtensionInternal, OpaqueColor } from '../common/types.js';
3
+ import { ExtensionInternalBase, OpaqueColor } from '../common/types.js';
4
4
  export declare const EXTENSION_SLOTS: readonly ["overPieces"];
5
5
  export type ExtensionSlotsType = typeof EXTENSION_SLOTS;
6
6
  export declare const EXTENSION_ID: "legalMoves";
@@ -19,7 +19,7 @@ export declare const DEFAULT_CONFIG: LegalMovesConfig;
19
19
  export type LegalMovesInitConfig = PartialDeep<LegalMovesConfig>;
20
20
  export type LegalMovesDefinition = ExtensionDefinition<typeof EXTENSION_ID, typeof EXTENSION_SLOTS, never>;
21
21
  export type LegalMovesInstance = ExtensionInstance<typeof EXTENSION_ID, typeof EXTENSION_SLOTS, never>;
22
- export interface LegalMovesInstanceInternal extends ExtensionInternal<ExtensionSlotsType> {
22
+ export interface LegalMovesInstanceInternal extends ExtensionInternalBase<ExtensionSlotsType> {
23
23
  destroyed: boolean;
24
24
  svgCircles: SVGCircleElement[];
25
25
  readonly config: LegalMovesConfig;
@@ -1,4 +1,3 @@
1
- import { collectSuppressedSquares } from '../../../../animation/planner.js';
2
1
  import { cleanAnimationPlan, prepareAnimationPlan, renderAnimationPlan } from '../../../../animation/render/plan.js';
3
2
  const EMPTY_SQUARES = new Set();
4
3
  export function rendererAnimationPrepare(state, context, layer) {
@@ -38,7 +37,7 @@ export function getAnimationSuppressedSquares(state) {
38
37
  const entry = state.entries.get(session.id);
39
38
  if (!entry)
40
39
  continue;
41
- for (const sq of collectSuppressedSquares(entry.plan.tracks)) {
40
+ for (const sq of entry.plan.suppressedSquares) {
42
41
  result.add(sq);
43
42
  }
44
43
  }
@@ -1,40 +1,26 @@
1
1
  import { calculateAnimationPlan } from '../../../../animation/planner.js';
2
2
  import { piecePositionsEqual } from '../../../../state/board/check.js';
3
+ import { changeStatesEqual } from '../../../../state/change/helpers.js';
3
4
  import { isUpdateContextRenderable } from '../../../types/context/update.js';
4
5
  const DEFAULT_ANIMATION_DURATION_MS = 180;
5
6
  export function rendererAnimationOnUpdate(state, context) {
6
7
  if (!isUpdateContextRenderable(context) ||
7
- !context.mutation.hasMutation({ prefixes: ['state.board.'] }) ||
8
+ !context.mutation.hasMutation({ prefixes: ['state.board.', 'state.change.'] }) ||
8
9
  !context.previousFrame) {
9
10
  return;
10
11
  }
11
- const previousBoard = context.previousFrame.state.board;
12
- const currentBoard = context.currentFrame.state.board;
13
- if (piecePositionsEqual(previousBoard, currentBoard))
12
+ if (piecePositionsEqual(context.previousFrame.state.board, context.currentFrame.state.board) &&
13
+ changeStatesEqual(context.previousFrame.state.change, context.currentFrame.state.change))
14
14
  return;
15
- // On DropTo exclude the move that user did from the animation plan
16
- const exclude = [];
17
- if (context.mutation.hasMutation({ causes: ['runtime.interaction.dropTo'] }) &&
18
- context.currentFrame.state.change.lastMove) {
19
- exclude.push({
20
- fromSq: context.currentFrame.state.change.lastMove.from,
21
- toSq: context.currentFrame.state.change.lastMove.to
22
- });
23
- exclude.push({
24
- sq: context.currentFrame.state.change.lastMove.from
25
- });
26
- exclude.push({
27
- sq: context.currentFrame.state.change.lastMove.to
28
- });
29
- }
30
- const plan = calculateAnimationPlan(previousBoard, currentBoard, 0 /* placeholder session id for draft plan */, {
31
- exclude
15
+ // Pass raw snapshots to the planner — it normalizes internally
16
+ const plan = calculateAnimationPlan({
17
+ previous: context.previousFrame.state,
18
+ current: context.currentFrame.state
32
19
  });
33
20
  if (plan.tracks.length === 0)
34
21
  return;
35
22
  const session = state.runtimeSurface.animation.submit({
36
23
  duration: DEFAULT_ANIMATION_DURATION_MS
37
24
  });
38
- plan.sessionId = session.id;
39
- state.entries.set(plan.sessionId, { plan, nodes: null });
25
+ state.entries.set(session.id, { plan, nodes: null });
40
26
  }
@@ -10,13 +10,13 @@ export function rendererBoardRender(state, context, layer) {
10
10
  clearElementChildren(layer);
11
11
  const { light, dark } = state.config;
12
12
  for (let sq = 0; sq < SQUARE_COUNT; sq++) {
13
- const r = geometry.squareRect(sq);
13
+ const r = geometry.getSquareRect(sq);
14
14
  createSvgElement(layer, 'rect', {
15
15
  'data-chessboard-id': `square-${sq}`,
16
16
  x: r.x.toString(),
17
17
  y: r.y.toString(),
18
- width: r.size.toString(),
19
- height: r.size.toString(),
18
+ width: r.width.toString(),
19
+ height: r.height.toString(),
20
20
  fill: isLightSquare(sq) ? light : dark,
21
21
  'shape-rendering': 'crispEdges'
22
22
  });
@@ -1,3 +1,4 @@
1
+ import { sceneRectsEqual } from '../../../../layout/geometry/helpers.js';
1
2
  import { isFrameRenderable } from '../../../types/basic/update.js';
2
3
  import { isUpdateContextRenderable } from '../../../types/context/update.js';
3
4
  import { DirtyLayer } from '../types/extension.js';
@@ -12,14 +13,15 @@ export function rendererBoardOnUpdate(_state, context) {
12
13
  }
13
14
  // Ok geometry changed, let's check the board size
14
15
  const currentGeometry = context.currentFrame.layout.geometry;
15
- const currentBoardSize = currentGeometry.boardSize;
16
+ const currentBoardRect = currentGeometry.boardRect;
16
17
  const currentOrientation = currentGeometry.orientation;
17
18
  const previousGeometry = context.previousFrame && isFrameRenderable(context.previousFrame)
18
19
  ? context.previousFrame.layout.geometry
19
20
  : null;
20
- const previousBoardSize = previousGeometry?.boardSize;
21
+ const previousBoardRect = previousGeometry?.boardRect ?? null;
21
22
  const previousOrientation = previousGeometry?.orientation;
22
- const needsRender = currentBoardSize !== previousBoardSize || currentOrientation !== previousOrientation;
23
+ const needsRender = sceneRectsEqual(currentBoardRect, previousBoardRect) === false ||
24
+ currentOrientation !== previousOrientation;
23
25
  if (!needsRender) {
24
26
  return; // no-op
25
27
  }
@@ -19,7 +19,7 @@ export function rendererCoordinatesRender(state, context, layer) {
19
19
  const logicalRank = (geometry.orientation === 0 /* ColorCode.White */ ? 7 - visualRank : visualRank);
20
20
  const sq = squareOf(rankFile, logicalRank);
21
21
  const label = geometry.orientation === 0 /* ColorCode.White */ ? String(8 - visualRank) : String(1 + visualRank);
22
- const r = geometry.squareRect(sq);
22
+ const r = geometry.getSquareRect(sq);
23
23
  const color = labelColorForSquare(sq, coords);
24
24
  const text = createSvgElement(layer, 'text', {
25
25
  'data-chessboard-id': `coord-rank-${label}`,
@@ -42,12 +42,12 @@ export function rendererCoordinatesRender(state, context, layer) {
42
42
  const label = geometry.orientation === 0 /* ColorCode.White */
43
43
  ? String.fromCharCode('a'.charCodeAt(0) + visualFile)
44
44
  : String.fromCharCode('h'.charCodeAt(0) - visualFile);
45
- const r = geometry.squareRect(sq);
45
+ const r = geometry.getSquareRect(sq);
46
46
  const color = labelColorForSquare(sq, coords);
47
47
  const text = createSvgElement(layer, 'text', {
48
48
  'data-chessboard-id': `coord-file-${label}`,
49
- x: (r.x + r.size - offset).toString(),
50
- y: (r.y + r.size - offset).toString(),
49
+ x: (r.x + r.width - offset).toString(),
50
+ y: (r.y + r.height - offset).toString(),
51
51
  'font-size': fontSize.toString(),
52
52
  'font-family': 'sans-serif',
53
53
  'font-weight': 'bold',
@@ -1,7 +1,7 @@
1
1
  import { createSvgElement, updateElementAttributes } from '../../../../render/svg/helpers.js';
2
2
  export function rendererDragRenderTransientVisuals(state, context, layer) {
3
3
  const geometry = context.currentFrame.layout.geometry;
4
- const point = context.transientInput.clampedPoint;
4
+ const point = context.transientInput.boardClampedPoint;
5
5
  const squareSize = geometry.squareSize.toString();
6
6
  const x = (point.x - geometry.squareSize / 2).toString();
7
7
  const y = (point.y - geometry.squareSize / 2).toString();
@@ -1,5 +1,5 @@
1
1
  import { toMerged } from 'es-toolkit';
2
- import { extensionCreateInternalBase, extensionDestroy, extensionMount, extensionUnmount } from '../common/helpers.js';
2
+ import { extensionCreateInternalBase, extensionDestroyBase, extensionMountBase, extensionUnmountBase } from '../common/helpers.js';
3
3
  import { createMainRendererAnimation } from './animation/factory.js';
4
4
  import { createMainRendererBoard } from './board/factory.js';
5
5
  import { createMainRendererCoordinates } from './coordinates/factory.js';
@@ -51,7 +51,7 @@ function createMainRendererInstance(options, config) {
51
51
  return {
52
52
  id: EXTENSION_ID,
53
53
  mount(env) {
54
- extensionMount(internalState, env.slotRoots);
54
+ extensionMountBase(internalState, env.slotRoots);
55
55
  },
56
56
  onUpdate(context) {
57
57
  internalState.board.onUpdate(context);
@@ -85,11 +85,11 @@ function createMainRendererInstance(options, config) {
85
85
  },
86
86
  unmount() {
87
87
  extensionUnmountLocal(internalState);
88
- extensionUnmount(internalState);
88
+ extensionUnmountBase(internalState);
89
89
  },
90
90
  destroy() {
91
91
  extensionUnmountLocal(internalState);
92
- extensionDestroy(internalState);
92
+ extensionDestroyBase(internalState);
93
93
  }
94
94
  };
95
95
  }
@@ -1,4 +1,3 @@
1
- import { MainRendererConfigInput, PieceUrlsInput } from './types/input.js';
2
- import { MainRendererConfig, PieceUrls } from './types/internal.js';
3
- export declare function normalizePieceUrls(input: PieceUrlsInput): PieceUrls;
1
+ import { MainRendererConfigInput } from './types/input.js';
2
+ import { MainRendererConfig } from './types/internal.js';
4
3
  export declare function normalizeMainRendererConfig(input: Partial<MainRendererConfigInput>): MainRendererConfig;
@@ -7,7 +7,7 @@ function assertPieceUrlsComplete(pieceUrls) {
7
7
  assert(pieceCode in pieceUrls, `Missing URL for piece code: ${pieceCode}`);
8
8
  }
9
9
  }
10
- export function normalizePieceUrls(input) {
10
+ function normalizePieceUrls(input) {
11
11
  const normalized = {};
12
12
  for (const [key, url] of Object.entries(input)) {
13
13
  const pieceCode = normalizePiece(key);
@@ -14,13 +14,13 @@ export function rendererPiecesRender(state, context, layer) {
14
14
  const suppressed = state.suppressedSquares.has(sq);
15
15
  if (isNonEmptyPieceCode(pieceCode) && !suppressed) {
16
16
  const url = state.config[pieceCode];
17
- const r = geometry.squareRect(sq);
17
+ const r = geometry.getSquareRect(sq);
18
18
  if (existing !== null) {
19
19
  updateElementAttributes(existing.root, {
20
20
  x: r.x.toString(),
21
21
  y: r.y.toString(),
22
- width: r.size.toString(),
23
- height: r.size.toString(),
22
+ width: r.width.toString(),
23
+ height: r.height.toString(),
24
24
  href: url
25
25
  });
26
26
  }
@@ -31,8 +31,8 @@ export function rendererPiecesRender(state, context, layer) {
31
31
  href: url,
32
32
  x: r.x.toString(),
33
33
  y: r.y.toString(),
34
- width: r.size.toString(),
35
- height: r.size.toString()
34
+ width: r.width.toString(),
35
+ height: r.height.toString()
36
36
  })
37
37
  });
38
38
  }
@@ -1,9 +1,14 @@
1
1
  export function calculateSuppressedSquares(_state, context, animationSuppressedSquares) {
2
2
  const dragSession = context.currentFrame.state.interaction.dragSession;
3
- const dragSquare = dragSession?.type === 'lifted-piece-drag' ? dragSession.sourceSquare : undefined;
3
+ const dragSquare = dragSession?.type === 'lifted-piece-drag' ? dragSession.sourceSquare : null;
4
4
  const result = new Set(animationSuppressedSquares);
5
- if (dragSquare !== undefined) {
5
+ if (dragSquare !== null) {
6
6
  result.add(dragSquare);
7
7
  }
8
+ const pendingMoveRequest = context.currentFrame.state.change.deferredUIMoveRequest;
9
+ if (pendingMoveRequest) {
10
+ result.add(pendingMoveRequest.sourceSquare);
11
+ result.add(pendingMoveRequest.destination.to);
12
+ }
8
13
  return result;
9
14
  }
@@ -3,7 +3,7 @@ import { MainRendererConfigInput } from './input.js';
3
3
  export type MainRendererInitOptions = Partial<MainRendererConfigInput>;
4
4
  export declare const EXTENSION_SLOTS: readonly ["board", "coordinates", "pieces", "animation", "drag"];
5
5
  export type ExtensionSlotsType = typeof EXTENSION_SLOTS;
6
- export declare const EXTENSION_ID: "main-renderer";
6
+ export declare const EXTENSION_ID: "renderer";
7
7
  export type MainRendererDefinition = ExtensionDefinition<typeof EXTENSION_ID, typeof EXTENSION_SLOTS, never>;
8
8
  export type MainRendererInstance = ExtensionInstance<typeof EXTENSION_ID, typeof EXTENSION_SLOTS, never>;
9
9
  export declare enum DirtyLayer {
@@ -1,5 +1,5 @@
1
1
  export const EXTENSION_SLOTS = ['board', 'coordinates', 'pieces', 'animation', 'drag'];
2
- export const EXTENSION_ID = 'main-renderer';
2
+ export const EXTENSION_ID = 'renderer';
3
3
  export var DirtyLayer;
4
4
  (function (DirtyLayer) {
5
5
  DirtyLayer[DirtyLayer["Board"] = 1] = "Board";
@@ -1,12 +1,12 @@
1
1
  import { ExtensionRuntimeSurface } from '../../../types/surface/main.js';
2
- import { ExtensionInternal } from '../../common/types.js';
2
+ import { ExtensionInternalBase } from '../../common/types.js';
3
3
  import { MainRendererAnimation } from '../animation/types.js';
4
4
  import { MainRendererBoard } from '../board/types.js';
5
5
  import { MainRendererCoordinates } from '../coordinates/types.js';
6
6
  import { MainRendererDrag } from '../drag/types.js';
7
7
  import { MainRendererPieces } from '../pieces/types.js';
8
8
  import { ExtensionSlotsType } from './extension.js';
9
- export interface MainRendererInstanceInternal extends ExtensionInternal<ExtensionSlotsType> {
9
+ export interface MainRendererInstanceInternal extends ExtensionInternalBase<ExtensionSlotsType> {
10
10
  readonly board: MainRendererBoard;
11
11
  readonly coordinates: MainRendererCoordinates;
12
12
  readonly pieces: MainRendererPieces;
@@ -0,0 +1,3 @@
1
+ import { PromotionInitConfig } from './types/input.js';
2
+ import { PromotionDefinition } from './types/main.js';
3
+ export declare function createPromotion(config?: PromotionInitConfig): PromotionDefinition;
@@ -0,0 +1,219 @@
1
+ import assert from '@ktarmyshov/assert';
2
+ import { createSvgElement, updateElementAttributes } from '../../../render/svg/helpers.js';
3
+ import { fileOf, rankOf, squareOf } from '../../../state/board/coords.js';
4
+ import { toPieceCode } from '../../../state/board/piece.js';
5
+ import { isUpdateContextRenderable } from '../../types/context/update.js';
6
+ import { extensionCreateInternalBase, extensionDestroyBase, extensionMountBase, extensionUnmountBase } from '../common/helpers.js';
7
+ import { normalizePromotionConfig } from './normalize.js';
8
+ import { EXTENSION_ID, EXTENSION_SLOTS } from './types/main.js';
9
+ export function createPromotion(config = {}) {
10
+ const mergedConfig = normalizePromotionConfig(config);
11
+ return {
12
+ id: EXTENSION_ID,
13
+ slots: EXTENSION_SLOTS,
14
+ createInstance(options) {
15
+ return createPromotionInstance(options, mergedConfig);
16
+ }
17
+ };
18
+ }
19
+ function createPromotionInternal(options, config) {
20
+ return {
21
+ ...extensionCreateInternalBase(),
22
+ svgPromotionPieces: new Map(),
23
+ activePromotionSquares: new Map(),
24
+ svgHoverRect: null,
25
+ config,
26
+ runtimeSurface: options.runtimeSurface
27
+ };
28
+ }
29
+ function extensionCleanPieceNode(state, roleCode) {
30
+ const pieceNode = state.svgPromotionPieces.get(roleCode);
31
+ if (pieceNode) {
32
+ pieceNode.svg.remove();
33
+ pieceNode.rect.remove();
34
+ state.svgPromotionPieces.delete(roleCode);
35
+ }
36
+ }
37
+ function extensionCleanPieceNodes(state) {
38
+ for (const pieceNode of state.svgPromotionPieces.values()) {
39
+ pieceNode.svg.remove();
40
+ pieceNode.rect.remove();
41
+ }
42
+ state.svgPromotionPieces.clear();
43
+ }
44
+ function extensionCleanHoverRect(state) {
45
+ if (state.svgHoverRect) {
46
+ state.svgHoverRect.remove();
47
+ state.svgHoverRect = null;
48
+ }
49
+ }
50
+ function extensionClean(state) {
51
+ extensionCleanPieceNodes(state);
52
+ extensionCleanHoverRect(state);
53
+ state.activePromotionSquares.clear();
54
+ }
55
+ function createPromotionInstance(options, config) {
56
+ const internalState = createPromotionInternal(options, config);
57
+ return {
58
+ id: EXTENSION_ID,
59
+ mount(env) {
60
+ extensionMountBase(internalState, env.slotRoots);
61
+ },
62
+ onUIMoveRequest(context) {
63
+ const request = context.request;
64
+ if (request.status !== 'unresolved')
65
+ return; // no-op if the request is already resolved or deferred
66
+ if (request.canBeAutoResolved)
67
+ return; // no-op, core will auto-resolve
68
+ // OK! now this one is our request!
69
+ request.defer(); // defer the request until the user selects a piece to promote to
70
+ },
71
+ onUpdate(context) {
72
+ const needsRender = context.mutation.hasMutation({
73
+ causes: ['state.change.setDeferredUIMoveRequest', 'layout.refreshGeometry']
74
+ }) && isUpdateContextRenderable(context);
75
+ if (!needsRender) {
76
+ return; // no-op
77
+ }
78
+ const request = context.currentFrame.state.change.deferredUIMoveRequest;
79
+ if (request !== null) {
80
+ internalState.runtimeSurface.transientVisuals.subscribe();
81
+ internalState.runtimeSurface.events.subscribeEvent('pointerdown');
82
+ }
83
+ else {
84
+ internalState.runtimeSurface.transientVisuals.unsubscribe();
85
+ internalState.runtimeSurface.events.unsubscribeEvent('pointerdown');
86
+ }
87
+ context.invalidation.markDirty(1 /* DirtyLayer.Promotion */);
88
+ },
89
+ render(context) {
90
+ assert(context.invalidation.dirtyLayers !== 0, 'Render should only be called when there are dirty layers');
91
+ const deferredRequest = context.currentFrame.state.change.deferredUIMoveRequest;
92
+ if (deferredRequest === null) {
93
+ // No deferred request, so we can clean up any existing promotion piece nodes and highlights
94
+ extensionClean(internalState);
95
+ return;
96
+ }
97
+ const targetSquare = deferredRequest.destination.to;
98
+ const targetFile = fileOf(targetSquare);
99
+ const targetRank = rankOf(targetSquare);
100
+ const promotedTo = deferredRequest.destination.promotedTo;
101
+ assert(promotedTo && promotedTo.length > 1 && promotedTo.length <= 4, 'PromotedTo should be available in a deferred promotion request and have between 2 and 4 roles');
102
+ const promotedToSorted = promotedTo.toSorted((a, b) => b - a); // Descending order to render queen on top
103
+ const geometry = context.currentFrame.layout.geometry;
104
+ // Determine piece color from target rank
105
+ const isWhitePromotion = targetRank === 7;
106
+ const pieceColor = isWhitePromotion ? 0 /* ColorCode.White */ : 8 /* ColorCode.Black */;
107
+ // Direction: white pieces cascade downward (rank decreases), black upward (rank increases)
108
+ const rankStep = isWhitePromotion ? -1 : 1;
109
+ // Rebuild the active promotion squares set
110
+ internalState.activePromotionSquares.clear();
111
+ // Remove nodes for roles no longer in the desired set
112
+ const desiredRoles = new Set(promotedToSorted);
113
+ for (const [roleCode] of internalState.svgPromotionPieces) {
114
+ if (!desiredRoles.has(roleCode)) {
115
+ extensionCleanPieceNode(internalState, roleCode);
116
+ }
117
+ }
118
+ // Create or update a node for each promotion role
119
+ assert(internalState.slotRoots, 'Slot roots should be available when render is called');
120
+ const layer = internalState.slotRoots.animation;
121
+ for (let i = 0; i < promotedToSorted.length; i++) {
122
+ const roleCode = promotedToSorted[i];
123
+ const displayRank = (targetRank + i * rankStep);
124
+ const displaySquare = squareOf(targetFile, displayRank);
125
+ const pieceCode = toPieceCode(roleCode, pieceColor);
126
+ const url = internalState.config.pieceUrls[pieceCode];
127
+ const r = geometry.getSquareRect(displaySquare);
128
+ const rectAttrs = {
129
+ x: r.x.toString(),
130
+ y: r.y.toString(),
131
+ width: r.width.toString(),
132
+ height: r.height.toString(),
133
+ fill: internalState.config.squareColor.color,
134
+ 'fill-opacity': internalState.config.squareColor.opacity.toString(),
135
+ 'shape-rendering': 'crispEdges'
136
+ };
137
+ const imageAttrs = {
138
+ href: url,
139
+ x: r.x.toString(),
140
+ y: r.y.toString(),
141
+ width: r.width.toString(),
142
+ height: r.height.toString()
143
+ };
144
+ internalState.activePromotionSquares.set(displaySquare, roleCode);
145
+ const existing = internalState.svgPromotionPieces.get(roleCode) ?? null;
146
+ if (existing !== null) {
147
+ updateElementAttributes(existing.rect, rectAttrs);
148
+ updateElementAttributes(existing.svg, imageAttrs);
149
+ }
150
+ else {
151
+ const rect = createSvgElement(layer, 'rect', {
152
+ 'data-chessboard-id': `promotion-bg-${roleCode}`,
153
+ ...rectAttrs
154
+ });
155
+ const svg = createSvgElement(layer, 'image', {
156
+ 'data-chessboard-id': `promotion-piece-${roleCode}`,
157
+ ...imageAttrs
158
+ });
159
+ internalState.svgPromotionPieces.set(roleCode, { svg, rect });
160
+ }
161
+ }
162
+ },
163
+ renderTransientVisuals(context) {
164
+ const target = context.transientInput.target;
165
+ if (target !== null && internalState.activePromotionSquares.has(target)) {
166
+ // Hover is over one of the promotion squares
167
+ const geometry = context.currentFrame.layout.geometry;
168
+ const r = geometry.getSquareRect(target);
169
+ const attrs = {
170
+ x: r.x.toString(),
171
+ y: r.y.toString(),
172
+ width: r.width.toString(),
173
+ height: r.height.toString(),
174
+ fill: internalState.config.hoverColor.color,
175
+ 'fill-opacity': internalState.config.hoverColor.opacity.toString(),
176
+ 'shape-rendering': 'crispEdges'
177
+ };
178
+ if (internalState.svgHoverRect) {
179
+ updateElementAttributes(internalState.svgHoverRect, attrs);
180
+ }
181
+ else {
182
+ assert(internalState.slotRoots, 'Slot roots should be available when renderTransientVisuals is called');
183
+ const layer = internalState.slotRoots.animation;
184
+ internalState.svgHoverRect = createSvgElement(layer, 'rect', {
185
+ 'data-chessboard-id': 'promotion-hover',
186
+ ...attrs
187
+ });
188
+ }
189
+ }
190
+ else {
191
+ // Not hovering over a promotion square — remove hover rect
192
+ extensionCleanHoverRect(internalState);
193
+ }
194
+ },
195
+ onEvent(context) {
196
+ assert(context.rawEvent.type === 'pointerdown', 'Only pointerdown events should be subscribed to');
197
+ const targetSquare = context.sceneEvent?.targetSquare ?? null;
198
+ if (targetSquare !== null) {
199
+ const roleCode = internalState.activePromotionSquares.get(targetSquare);
200
+ if (roleCode !== undefined) {
201
+ context.rawEvent.preventDefault(); // Prevent focus change and other side effects if it is our promotion square
202
+ internalState.runtimeSurface.commands.resolveDeferredUIMoveRequest({
203
+ promotedTo: roleCode
204
+ });
205
+ return;
206
+ }
207
+ }
208
+ internalState.runtimeSurface.commands.cancelDeferredUIMoveRequest();
209
+ },
210
+ unmount() {
211
+ extensionUnmountBase(internalState);
212
+ extensionClean(internalState);
213
+ },
214
+ destroy() {
215
+ extensionDestroyBase(internalState);
216
+ extensionClean(internalState);
217
+ }
218
+ };
219
+ }
@@ -0,0 +1,3 @@
1
+ import { PromotionInitConfig } from './types/input.js';
2
+ import { PromotionConfig } from './types/internal.js';
3
+ export declare function normalizePromotionConfig(config: PromotionInitConfig): PromotionConfig;