@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.
- package/CHANGELOG.md +24 -0
- package/README.md +240 -45
- package/dist/animation/planner.d.ts +11 -6
- package/dist/animation/planner.js +164 -45
- package/dist/animation/render/fade.d.ts +2 -2
- package/dist/animation/render/fade.js +3 -3
- package/dist/animation/render/move.d.ts +3 -3
- package/dist/animation/render/move.js +5 -5
- package/dist/animation/render/plan.d.ts +3 -3
- package/dist/animation/render/static.d.ts +2 -2
- package/dist/animation/render/static.js +3 -3
- package/dist/animation/types.d.ts +14 -15
- package/dist/animation/types.js +1 -6
- package/dist/extensions/build/index.d.ts +21 -0
- package/dist/extensions/build/index.js +9 -0
- package/dist/extensions/factory/main.js +23 -2
- package/dist/extensions/factory/runtime.d.ts +2 -1
- package/dist/extensions/factory/runtime.js +48 -4
- package/dist/extensions/first-party/active-target/factory.js +9 -9
- package/dist/extensions/first-party/active-target/types.d.ts +2 -2
- package/dist/extensions/first-party/active-target/types.js +4 -1
- package/dist/extensions/first-party/auto-promote/factory.d.ts +2 -0
- package/dist/extensions/first-party/auto-promote/factory.js +51 -0
- package/dist/extensions/first-party/auto-promote/types.d.ts +12 -0
- package/dist/extensions/first-party/auto-promote/types.js +2 -0
- package/dist/extensions/first-party/board-events/factory.js +1 -16
- package/dist/extensions/first-party/board-events/types.d.ts +1 -4
- package/dist/extensions/first-party/common/helpers.d.ts +7 -7
- package/dist/extensions/first-party/common/helpers.js +11 -11
- package/dist/extensions/first-party/common/types.d.ts +2 -3
- package/dist/extensions/first-party/last-move/factory.js +10 -10
- package/dist/extensions/first-party/last-move/types.d.ts +2 -2
- package/dist/extensions/first-party/legal-moves/factory.js +5 -5
- package/dist/extensions/first-party/legal-moves/types.d.ts +2 -2
- package/dist/extensions/first-party/main-renderer/animation/render.js +1 -2
- package/dist/extensions/first-party/main-renderer/animation/update.js +9 -23
- package/dist/extensions/first-party/main-renderer/board/render.js +3 -3
- package/dist/extensions/first-party/main-renderer/board/update.js +5 -3
- package/dist/extensions/first-party/main-renderer/coordinates/render.js +4 -4
- package/dist/extensions/first-party/main-renderer/drag/render.js +1 -1
- package/dist/extensions/first-party/main-renderer/factory.js +4 -4
- package/dist/extensions/first-party/main-renderer/normalize.d.ts +2 -3
- package/dist/extensions/first-party/main-renderer/normalize.js +1 -1
- package/dist/extensions/first-party/main-renderer/pieces/render.js +5 -5
- package/dist/extensions/first-party/main-renderer/pieces/suppress.js +7 -2
- package/dist/extensions/first-party/main-renderer/types/extension.d.ts +1 -1
- package/dist/extensions/first-party/main-renderer/types/extension.js +1 -1
- package/dist/extensions/first-party/main-renderer/types/instance.d.ts +2 -2
- package/dist/extensions/first-party/promotion/factory.d.ts +3 -0
- package/dist/extensions/first-party/promotion/factory.js +219 -0
- package/dist/extensions/first-party/promotion/normalize.d.ts +3 -0
- package/dist/extensions/first-party/promotion/normalize.js +37 -0
- package/dist/extensions/first-party/promotion/types/input.d.ts +10 -0
- package/dist/extensions/first-party/promotion/types/internal.d.ts +12 -0
- package/dist/extensions/first-party/promotion/types/internal.js +22 -0
- package/dist/extensions/first-party/promotion/types/main.d.ts +24 -0
- package/dist/extensions/first-party/promotion/types/main.js +2 -0
- package/dist/extensions/first-party/selected-square/factory.js +7 -7
- package/dist/extensions/first-party/selected-square/types.d.ts +2 -2
- package/dist/extensions/index.d.ts +1 -8
- package/dist/extensions/index.js +1 -6
- package/dist/extensions/types/basic/events.d.ts +8 -31
- package/dist/extensions/types/basic/events.js +9 -5
- package/dist/extensions/types/basic/interaction.d.ts +21 -0
- package/dist/extensions/types/basic/transient-visuals.d.ts +2 -1
- package/dist/extensions/types/context/events.d.ts +5 -0
- package/dist/extensions/types/context/events.js +1 -0
- package/dist/extensions/types/context/ui-move.d.ts +3 -15
- package/dist/extensions/types/extension.d.ts +10 -8
- package/dist/extensions/types/main.d.ts +11 -6
- package/dist/extensions/types/surface/commands.d.ts +9 -0
- package/dist/extensions/types/surface/events.d.ts +2 -3
- package/dist/extensions/types/wrapper.d.ts +23 -22
- package/dist/extensions/types/wrapper.js +34 -6
- package/dist/extensions/update.d.ts +2 -2
- package/dist/extensions/update.js +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/layout/factory.js +3 -3
- package/dist/layout/geometry/factory.d.ts +2 -11
- package/dist/layout/geometry/factory.js +16 -17
- package/dist/layout/geometry/helpers.d.ts +5 -0
- package/dist/layout/geometry/helpers.js +33 -0
- package/dist/layout/geometry/types.d.ts +13 -12
- package/dist/layout/helpers.d.ts +3 -4
- package/dist/layout/helpers.js +5 -5
- package/dist/layout/reducers.js +9 -8
- package/dist/layout/types.d.ts +5 -5
- package/dist/render/mount.js +2 -2
- package/dist/render/rendering/state.js +7 -7
- package/dist/runtime/factory/events.d.ts +3 -0
- package/dist/runtime/factory/events.js +18 -0
- package/dist/runtime/factory/helpers.js +12 -12
- package/dist/runtime/factory/input.js +17 -10
- package/dist/runtime/factory/main.js +44 -4
- package/dist/runtime/input/adapter/event.d.ts +3 -0
- package/dist/runtime/input/adapter/event.js +19 -0
- package/dist/runtime/input/adapter/factory.js +19 -38
- package/dist/runtime/input/adapter/helpers.d.ts +3 -5
- package/dist/runtime/input/adapter/helpers.js +60 -12
- package/dist/runtime/input/adapter/pointer.d.ts +6 -10
- package/dist/runtime/input/adapter/pointer.js +37 -70
- package/dist/runtime/input/adapter/types.d.ts +6 -3
- package/dist/runtime/input/adapter/types.js +6 -1
- package/dist/runtime/input/controller/factory.js +12 -11
- package/dist/runtime/input/controller/pointer.d.ts +5 -6
- package/dist/runtime/input/controller/pointer.js +36 -35
- package/dist/runtime/input/controller/transient-visuals.d.ts +2 -2
- package/dist/runtime/input/controller/transient-visuals.js +12 -5
- package/dist/runtime/input/controller/types.d.ts +5 -4
- package/dist/runtime/lifecycle.js +6 -6
- package/dist/runtime/mutation/interaction.js +1 -5
- package/dist/runtime/mutation/types.d.ts +10 -8
- package/dist/runtime/types/main.d.ts +2 -0
- package/dist/state/change/{factory.d.ts → factory/main.d.ts} +1 -1
- package/dist/state/change/{factory.js → factory/main.js} +8 -5
- package/dist/state/change/factory/ui-move.d.ts +4 -0
- package/dist/{runtime → state/change}/factory/ui-move.js +9 -9
- package/dist/state/change/helpers.d.ts +4 -2
- package/dist/state/change/helpers.js +11 -1
- package/dist/state/change/mutation.d.ts +1 -1
- package/dist/state/change/reducers.d.ts +3 -3
- package/dist/state/change/reducers.js +4 -4
- package/dist/state/change/types/main.d.ts +18 -0
- package/dist/state/change/types/main.js +1 -0
- package/dist/state/change/types/ui-move.d.ts +25 -0
- package/dist/state/change/types/ui-move.js +1 -0
- package/dist/state/factory.js +1 -1
- package/dist/state/interaction/factory.js +4 -2
- package/dist/state/interaction/helpers.d.ts +2 -0
- package/dist/state/interaction/helpers.js +3 -0
- package/dist/state/interaction/types/internal.d.ts +9 -1
- package/dist/state/types.d.ts +1 -1
- package/dist/wrapper/factory.js +2 -26
- package/package.json +17 -11
- package/dist/runtime/factory/ui-move.d.ts +0 -4
- package/dist/runtime/types/ui-move.d.ts +0 -9
- package/dist/state/change/types.d.ts +0 -17
- /package/dist/{runtime/types/ui-move.js → extensions/first-party/promotion/types/input.js} +0 -0
- /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,
|
|
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
|
-
|
|
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.
|
|
61
|
-
const rTo = geometry.
|
|
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.
|
|
66
|
-
height: rFrom.
|
|
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.
|
|
75
|
-
height: rTo.
|
|
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
|
-
|
|
98
|
+
extensionUnmountBase(internalState);
|
|
99
99
|
extensionClean(internalState);
|
|
100
100
|
},
|
|
101
101
|
destroy() {
|
|
102
|
-
|
|
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 {
|
|
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
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
93
|
+
extensionUnmountBase(internalState);
|
|
94
94
|
extensionClean(internalState);
|
|
95
95
|
},
|
|
96
96
|
destroy() {
|
|
97
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
//
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
context.currentFrame.state
|
|
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
|
-
|
|
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.
|
|
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.
|
|
19
|
-
height: r.
|
|
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
|
|
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
|
|
21
|
+
const previousBoardRect = previousGeometry?.boardRect ?? null;
|
|
21
22
|
const previousOrientation = previousGeometry?.orientation;
|
|
22
|
-
const needsRender =
|
|
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.
|
|
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.
|
|
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.
|
|
50
|
-
y: (r.y + r.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
88
|
+
extensionUnmountBase(internalState);
|
|
89
89
|
},
|
|
90
90
|
destroy() {
|
|
91
91
|
extensionUnmountLocal(internalState);
|
|
92
|
-
|
|
92
|
+
extensionDestroyBase(internalState);
|
|
93
93
|
}
|
|
94
94
|
};
|
|
95
95
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { MainRendererConfigInput
|
|
2
|
-
import { MainRendererConfig
|
|
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
|
-
|
|
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.
|
|
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.
|
|
23
|
-
height: r.
|
|
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.
|
|
35
|
-
height: r.
|
|
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 :
|
|
3
|
+
const dragSquare = dragSession?.type === 'lifted-piece-drag' ? dragSession.sourceSquare : null;
|
|
4
4
|
const result = new Set(animationSuppressedSquares);
|
|
5
|
-
if (dragSquare !==
|
|
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: "
|
|
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 = '
|
|
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 {
|
|
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
|
|
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,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
|
+
}
|