@connectorvol/chess-widgets 6.4.0 → 6.5.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/dist/constants/editable-board-settings.d.ts +1 -1
- package/dist/constants/editable-board-settings.js +1 -1
- package/dist/game-analyzer/GameAnalyzer.svelte +75 -0
- package/dist/game-analyzer/GameAnalyzer.svelte.d.ts +4 -0
- package/dist/game-analyzer/gameAnalyzer.svelte.js +124 -0
- package/dist/game-analyzer/types.d.ts +35 -0
- package/dist/game-analyzer/types.js +1 -0
- package/dist/game-analyzer/utils.d.ts +5 -0
- package/dist/game-analyzer/utils.js +53 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/puzzle-creation/PuzzleBoardTreeViewerPane.svelte +18 -12
- package/dist/puzzle-creation/PuzzleCreationWizard.svelte +1 -1
- package/dist/puzzle-creation/PuzzlePgnBoardTreeEditor.svelte +1 -0
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ import { Draggable, PieceInputMode } from "@connectorvol/chessboard";
|
|
|
5
5
|
export declare const DEFAULT_EDITABLE_BOARD_SETTINGS: {
|
|
6
6
|
autoQueenPromotion: false;
|
|
7
7
|
isResizable: true;
|
|
8
|
-
|
|
8
|
+
showBoardCoordinates: false;
|
|
9
9
|
orientation: "w";
|
|
10
10
|
boardSize: number;
|
|
11
11
|
draggable: Draggable.BOTH;
|
|
@@ -6,7 +6,7 @@ import { Color } from "@connectorvol/shared";
|
|
|
6
6
|
export const DEFAULT_EDITABLE_BOARD_SETTINGS = {
|
|
7
7
|
autoQueenPromotion: false,
|
|
8
8
|
isResizable: true,
|
|
9
|
-
|
|
9
|
+
showBoardCoordinates: false,
|
|
10
10
|
orientation: Color.WHITE,
|
|
11
11
|
boardSize: 38,
|
|
12
12
|
draggable: Draggable.BOTH,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { TGameAnalyzerProps } from "./types.js";
|
|
3
|
+
|
|
4
|
+
import { Chessboard, DEFAULT_BOARD_SETTINGS } from "@connectorvol/chessboard";
|
|
5
|
+
import { TreeViewer, TreeViewerPanelManager } from "@connectorvol/tree";
|
|
6
|
+
import { Game } from "./gameAnalyzer.svelte.js";
|
|
7
|
+
import { untrack } from "svelte";
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
pgn,
|
|
11
|
+
onChangePGN,
|
|
12
|
+
boardTheme,
|
|
13
|
+
boardAppearanceSettings,
|
|
14
|
+
editMode = true,
|
|
15
|
+
class: className,
|
|
16
|
+
}: TGameAnalyzerProps = $props();
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
let game = $derived(new Game(
|
|
20
|
+
untrack(() => pgn),
|
|
21
|
+
{
|
|
22
|
+
...DEFAULT_BOARD_SETTINGS,
|
|
23
|
+
...boardAppearanceSettings,
|
|
24
|
+
boardSize: "auto",
|
|
25
|
+
},
|
|
26
|
+
"chess",
|
|
27
|
+
boardTheme,
|
|
28
|
+
))
|
|
29
|
+
|
|
30
|
+
// $effect(() => {
|
|
31
|
+
// if (pgn !== internalPgn) {
|
|
32
|
+
// internalPgn = pgn;
|
|
33
|
+
// game.loadPgn(pgn);
|
|
34
|
+
// }
|
|
35
|
+
// });
|
|
36
|
+
|
|
37
|
+
function handleChangeDirty(_setIsDirty: (value: boolean) => void) {
|
|
38
|
+
console.log('handleChangeDirty')
|
|
39
|
+
const exported = game.exportPgn();
|
|
40
|
+
onChangePGN?.(exported);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<div class={["mx-4 lg:container lg:mx-auto gap-4", className]}>
|
|
46
|
+
<div class="flex justify-center relative lg:gap-4 flex-col lg:flex-row">
|
|
47
|
+
<div class="lg:w-2/5">
|
|
48
|
+
<Chessboard facade={game.chessboard} />
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
<div class="lg:w-3/5 flex lg:flex-col flex-col-reverse h-full">
|
|
53
|
+
<div class="h-full">
|
|
54
|
+
<TreeViewer
|
|
55
|
+
className="h-[calc(100dvh-8rem)]"
|
|
56
|
+
chessTree={game.chessTree}
|
|
57
|
+
onSelectNode={game.onSelectNode}
|
|
58
|
+
onDeleteVariant={game.onDeleteVariant}
|
|
59
|
+
setChessFen={game.setChessFen}
|
|
60
|
+
setChessboardFen={game.setChessboardFen}
|
|
61
|
+
pieceSet={game.chessboard.chessSet}
|
|
62
|
+
{editMode}
|
|
63
|
+
onChangeDirty={handleChangeDirty}
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<TreeViewerPanelManager
|
|
68
|
+
className="flex w-full justify-center select-none col-span-1 my-4"
|
|
69
|
+
chessTree={game.chessTree}
|
|
70
|
+
setChessFen={game.setChessFen}
|
|
71
|
+
setChessboardFen={game.setChessboardFen}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Color, calculatePly, generateId } from "@connectorvol/shared";
|
|
2
|
+
import { CHESSBOARD_THEMES, createBoardApi, DEFAULT_BOARD_SETTINGS, Draggable, syncMoveEvaluationNagBadge, } from "@connectorvol/chessboard";
|
|
3
|
+
import { PgnOps } from "@connectorvol/chessops/pgnOps.svelte";
|
|
4
|
+
import { transformPgnToChessNode, createPgnFromTree } from "@connectorvol/tree";
|
|
5
|
+
import { ChessTree } from "@connectorvol/tree";
|
|
6
|
+
import { saveNodeMarkers } from "./utils.js";
|
|
7
|
+
export class Game {
|
|
8
|
+
chess;
|
|
9
|
+
chessboard;
|
|
10
|
+
chessTree;
|
|
11
|
+
previousNode;
|
|
12
|
+
boardSettings;
|
|
13
|
+
variant;
|
|
14
|
+
boardTheme;
|
|
15
|
+
constructor(pgn, chessBoardSettings = DEFAULT_BOARD_SETTINGS, variant = "chess", boardTheme) {
|
|
16
|
+
this.boardSettings = chessBoardSettings;
|
|
17
|
+
this.variant = variant;
|
|
18
|
+
this.boardTheme = boardTheme ?? CHESSBOARD_THEMES.blue;
|
|
19
|
+
const { rootNode, initialFen: position } = transformPgnToChessNode(pgn);
|
|
20
|
+
this.chessTree = new ChessTree(rootNode);
|
|
21
|
+
this.chess = new PgnOps(position, variant);
|
|
22
|
+
this.chessboard = createBoardApi({
|
|
23
|
+
fen: this.chess.fen(),
|
|
24
|
+
settings: {
|
|
25
|
+
...this.boardSettings,
|
|
26
|
+
draggable: this.chess.turn() === Color.WHITE ? Draggable.WHITE : Draggable.BLACK,
|
|
27
|
+
},
|
|
28
|
+
theme: this.boardTheme,
|
|
29
|
+
});
|
|
30
|
+
this.chessboard.setActions(this.buildActions());
|
|
31
|
+
this.previousNode = this.chessTree.currentNode;
|
|
32
|
+
$effect(() => {
|
|
33
|
+
const markerService = this.chessboard.getBoard().markerService;
|
|
34
|
+
void markerService.totalMarkerCount;
|
|
35
|
+
const changed = saveNodeMarkers(this.chessboard, this.chessTree.currentNode);
|
|
36
|
+
if (changed) {
|
|
37
|
+
this.chessTree.mutationVersion++;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
buildActions() {
|
|
42
|
+
return {
|
|
43
|
+
game: {
|
|
44
|
+
possibleMovesOnSquare: (square) => this.chess.moves(square),
|
|
45
|
+
beforePieceMoveSan: (san) => {
|
|
46
|
+
const move = this.chess.makeSanMove(san);
|
|
47
|
+
const fen = this.chess.fen();
|
|
48
|
+
const { halfMoves, fullMoves } = calculatePly(fen);
|
|
49
|
+
saveNodeMarkers(this.chessboard, this.chessTree.currentNode);
|
|
50
|
+
this.chessTree.addNodeToCurrent({
|
|
51
|
+
id: generateId(),
|
|
52
|
+
children: [],
|
|
53
|
+
data: { fen, san, ply: halfMoves, fullMoves, lastMove: move },
|
|
54
|
+
});
|
|
55
|
+
return { move, fen, turn: this.chess.turn() };
|
|
56
|
+
},
|
|
57
|
+
afterPieceMoveSan: () => {
|
|
58
|
+
syncMoveEvaluationNagBadge(this.chessboard, this.chessTree.currentNode.data);
|
|
59
|
+
this.chessboard.setNodeMarkers(this.chessTree.currentNode.data.parsedFirstComment?.shapes);
|
|
60
|
+
},
|
|
61
|
+
beforePieceMove: (from, to, promotion) => {
|
|
62
|
+
const { san, ply, fullMoves, move: lastMove } = this.chess.makeMove({
|
|
63
|
+
from,
|
|
64
|
+
to,
|
|
65
|
+
promotion,
|
|
66
|
+
});
|
|
67
|
+
const fen = this.chess.fen();
|
|
68
|
+
saveNodeMarkers(this.chessboard, this.chessTree.currentNode);
|
|
69
|
+
this.chessTree.addNodeToCurrent({
|
|
70
|
+
id: generateId(),
|
|
71
|
+
children: [],
|
|
72
|
+
data: { fen, san, ply, fullMoves, lastMove },
|
|
73
|
+
});
|
|
74
|
+
return fen;
|
|
75
|
+
},
|
|
76
|
+
afterPieceMove: () => {
|
|
77
|
+
this.chessboard.draggable =
|
|
78
|
+
this.chess.turn() === Color.WHITE ? Draggable.WHITE : Draggable.BLACK;
|
|
79
|
+
syncMoveEvaluationNagBadge(this.chessboard, this.chessTree.currentNode.data);
|
|
80
|
+
this.chessboard.setNodeMarkers(this.chessTree.currentNode.data.parsedFirstComment?.shapes);
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Перезагружает партию из нового PGN. */
|
|
86
|
+
loadPgn(pgn) {
|
|
87
|
+
const { rootNode, initialFen: position } = transformPgnToChessNode(pgn);
|
|
88
|
+
this.chessTree.replaceRootTree(rootNode);
|
|
89
|
+
this.chess = new PgnOps(position, this.variant);
|
|
90
|
+
this.chessboard.fen = this.chess.fen();
|
|
91
|
+
this.syncGameState();
|
|
92
|
+
}
|
|
93
|
+
/** Экспортирует текущее дерево в строку PGN. */
|
|
94
|
+
exportPgn() {
|
|
95
|
+
return createPgnFromTree(this.chessTree.rootNode);
|
|
96
|
+
}
|
|
97
|
+
onSelectNode = () => {
|
|
98
|
+
saveNodeMarkers(this.chessboard, this.previousNode);
|
|
99
|
+
this.chess.setFen(this.chessTree.currentNode.data.fen);
|
|
100
|
+
this.syncGameState();
|
|
101
|
+
};
|
|
102
|
+
onDeleteVariant = () => {
|
|
103
|
+
saveNodeMarkers(this.chessboard, this.previousNode);
|
|
104
|
+
this.chess.setFen(this.chessTree.currentNode.data.fen);
|
|
105
|
+
this.syncGameState();
|
|
106
|
+
};
|
|
107
|
+
setChessFen = (fen) => {
|
|
108
|
+
this.chess.setFen(fen);
|
|
109
|
+
};
|
|
110
|
+
setChessboardFen = (animationTime = 0) => {
|
|
111
|
+
saveNodeMarkers(this.chessboard, this.previousNode);
|
|
112
|
+
this.chessboard.animationTime = animationTime;
|
|
113
|
+
this.syncGameState();
|
|
114
|
+
};
|
|
115
|
+
syncGameState() {
|
|
116
|
+
this.chessboard.fen = this.chess.fen();
|
|
117
|
+
this.chessboard.addLastMove(this.chessTree.currentNode.data.lastMove ?? null);
|
|
118
|
+
syncMoveEvaluationNagBadge(this.chessboard, this.chessTree.currentNode.data);
|
|
119
|
+
this.chessboard.setNodeMarkers(this.chessTree.currentNode.data.parsedFirstComment?.shapes);
|
|
120
|
+
this.chessboard.draggable =
|
|
121
|
+
this.chess.turn() === Color.WHITE ? Draggable.WHITE : Draggable.BLACK;
|
|
122
|
+
this.previousNode = this.chessTree.currentNode;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ChessboardTheme } from "@connectorvol/chessboard";
|
|
2
|
+
import type { ChessBoardSettings } from "@connectorvol/chessboard";
|
|
3
|
+
/**
|
|
4
|
+
* Представляет тип визуальных настроек шахматной доски для анализатора партии.
|
|
5
|
+
*/
|
|
6
|
+
export type TChessboardAppearanceSettings = Omit<Partial<ChessBoardSettings>, "boardSize" | "orientation" | "draggable">;
|
|
7
|
+
/**
|
|
8
|
+
* Представляет свойства компонента анализатора партии.
|
|
9
|
+
*/
|
|
10
|
+
export interface TGameAnalyzerProps {
|
|
11
|
+
/**
|
|
12
|
+
* Возвращает PGN партии.
|
|
13
|
+
*/
|
|
14
|
+
pgn: string;
|
|
15
|
+
/**
|
|
16
|
+
* Возвращает колбэк при изменении дерева ходов (добавление/удаление ходов, комментариев, маркеров).
|
|
17
|
+
*/
|
|
18
|
+
onChangePGN?: (pgn: string) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Возвращает тему оформления шахматной доски (по умолчанию синяя).
|
|
21
|
+
*/
|
|
22
|
+
boardTheme?: ChessboardTheme;
|
|
23
|
+
/**
|
|
24
|
+
* Возвращает дополнительные визуальные настройки шахматной доски (кроме `boardSize`, `orientation`, `draggable`).
|
|
25
|
+
*/
|
|
26
|
+
boardAppearanceSettings?: TChessboardAppearanceSettings;
|
|
27
|
+
/**
|
|
28
|
+
* Возвращает признак режима правки дерева (по умолчанию `true`).
|
|
29
|
+
*/
|
|
30
|
+
editMode?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Возвращает дополнительные классы Tailwind для корневого контейнера.
|
|
33
|
+
*/
|
|
34
|
+
class?: string;
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { BoardApi } from "@connectorvol/chessboard";
|
|
2
|
+
import type { ChessTreeNode } from "@connectorvol/tree";
|
|
3
|
+
/** Сохраняет текущие маркеры доски в комментарий узла дерева (в формате PGN [%cal/%csl]).
|
|
4
|
+
* Возвращает true, если комментарий был изменён. */
|
|
5
|
+
export declare function saveNodeMarkers(boardApi: BoardApi, node: ChessTreeNode): boolean;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { parseComment, makeComment, } from "@connectorvol/chessops/pgn";
|
|
2
|
+
const HEX_TO_COMMENT_COLOR = {
|
|
3
|
+
"#ff0000": "red",
|
|
4
|
+
"#00ff00": "green",
|
|
5
|
+
"#008000": "green",
|
|
6
|
+
"#0000ff": "blue",
|
|
7
|
+
"#ffff00": "yellow",
|
|
8
|
+
};
|
|
9
|
+
function hexToCommentColor(hex) {
|
|
10
|
+
return HEX_TO_COMMENT_COLOR[hex.toLowerCase()] ?? "green";
|
|
11
|
+
}
|
|
12
|
+
function shapeKey(s) {
|
|
13
|
+
return `${s.color}:${s.from}:${s.to}`;
|
|
14
|
+
}
|
|
15
|
+
function shapesEqual(a, b) {
|
|
16
|
+
if (a.length !== b.length)
|
|
17
|
+
return false;
|
|
18
|
+
const setA = new Set(a.map(shapeKey));
|
|
19
|
+
return b.every((s) => setA.has(shapeKey(s)));
|
|
20
|
+
}
|
|
21
|
+
/** Сохраняет текущие маркеры доски в комментарий узла дерева (в формате PGN [%cal/%csl]).
|
|
22
|
+
* Возвращает true, если комментарий был изменён. */
|
|
23
|
+
export function saveNodeMarkers(boardApi, node) {
|
|
24
|
+
const shapes = boardApi.getNodeMarkers();
|
|
25
|
+
const rawFirst = node.data.comments?.[0];
|
|
26
|
+
const base = rawFirst ? parseComment(rawFirst) : { text: "", shapes: [] };
|
|
27
|
+
const newShapes = shapes.map((s) => ({
|
|
28
|
+
color: hexToCommentColor(s.color ?? "#ff0000"),
|
|
29
|
+
from: s.from,
|
|
30
|
+
to: s.to,
|
|
31
|
+
}));
|
|
32
|
+
if (shapesEqual(newShapes, base.shapes))
|
|
33
|
+
return false;
|
|
34
|
+
const newComment = makeComment({ ...base, shapes: newShapes });
|
|
35
|
+
if (newComment === rawFirst)
|
|
36
|
+
return false;
|
|
37
|
+
if (newComment === "") {
|
|
38
|
+
if (rawFirst === undefined)
|
|
39
|
+
return false;
|
|
40
|
+
const tail = node.data.comments?.slice(1);
|
|
41
|
+
if (tail?.length) {
|
|
42
|
+
node.data.comments = tail;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
node.data.comments = undefined;
|
|
46
|
+
}
|
|
47
|
+
node.data.parsedFirstComment = null;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
node.data.comments = [newComment, ...(node.data.comments?.slice(1) ?? [])];
|
|
51
|
+
node.data.parsedFirstComment = parseComment(newComment);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -12,3 +12,5 @@ export { default as EditFen } from "./position-editor/EditFen.svelte";
|
|
|
12
12
|
export { default as EditMove } from "./position-editor/EditMove.svelte";
|
|
13
13
|
export { default as EditPanel } from "./position-editor/EditPanel.svelte";
|
|
14
14
|
export { Fen, type Castling } from "./position-editor/fen.svelte.js";
|
|
15
|
+
export { default as GameAnalyzer } from "./game-analyzer/GameAnalyzer.svelte";
|
|
16
|
+
export type { TGameAnalyzerProps } from "./game-analyzer/types.js";
|
package/dist/index.js
CHANGED
|
@@ -8,3 +8,4 @@ export { default as EditFen } from "./position-editor/EditFen.svelte";
|
|
|
8
8
|
export { default as EditMove } from "./position-editor/EditMove.svelte";
|
|
9
9
|
export { default as EditPanel } from "./position-editor/EditPanel.svelte";
|
|
10
10
|
export { Fen } from "./position-editor/fen.svelte.js";
|
|
11
|
+
export { default as GameAnalyzer } from "./game-analyzer/GameAnalyzer.svelte";
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
import { Chessboard } from "@connectorvol/chessboard";
|
|
6
6
|
import type { ChessTree } from "@connectorvol/tree";
|
|
7
7
|
import { TreeViewer } from "@connectorvol/tree";
|
|
8
|
-
import { DEFAULT_PIECE_SET } from "@connectorvol/shared";
|
|
9
8
|
|
|
10
9
|
import { cn } from "../utils.js";
|
|
11
10
|
|
|
@@ -67,6 +66,10 @@
|
|
|
67
66
|
class: className,
|
|
68
67
|
belowChessboard,
|
|
69
68
|
}: Props = $props();
|
|
69
|
+
|
|
70
|
+
const hasBoardCoordinatesWithoutBorder = $derived(
|
|
71
|
+
chessboard.showBoardCoordinates && !chessboard.border,
|
|
72
|
+
);
|
|
70
73
|
</script>
|
|
71
74
|
|
|
72
75
|
<div
|
|
@@ -79,19 +82,22 @@
|
|
|
79
82
|
class="order-2 flex min-h-48 w-full flex-1 flex-col gap-2 lg:order-2 lg:min-h-0 lg:min-w-0"
|
|
80
83
|
>
|
|
81
84
|
<div
|
|
82
|
-
class=
|
|
85
|
+
class={cn(
|
|
86
|
+
"flex min-h-28 min-w-0 flex-1 flex-col overflow-hidden rounded-md border border-border lg:min-h-0",
|
|
87
|
+
hasBoardCoordinatesWithoutBorder && "mb-4",
|
|
88
|
+
)}
|
|
83
89
|
>
|
|
84
90
|
<TreeViewer
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
chessTree={chessTree}
|
|
92
|
+
{onSelectNode}
|
|
93
|
+
{onDeleteVariant}
|
|
94
|
+
{setChessFen}
|
|
95
|
+
{setChessboardFen}
|
|
96
|
+
pieceSet={chessboard.chessSet}
|
|
97
|
+
className="min-h-0 flex-1 border-x-0 border-t-0"
|
|
98
|
+
{editMode}
|
|
99
|
+
{selectable}
|
|
100
|
+
/>
|
|
95
101
|
</div>
|
|
96
102
|
</div>
|
|
97
103
|
|
|
@@ -487,6 +487,7 @@
|
|
|
487
487
|
chessboard.fen = previewChess.fen();
|
|
488
488
|
chessboard.addLastMove(tree.currentNode.data.lastMove ?? null);
|
|
489
489
|
syncMoveEvaluationNagBadge(chessboard, tree.currentNode.data);
|
|
490
|
+
chessboard.setNodeMarkers(tree.currentNode.data.parsedFirstComment?.shapes);
|
|
490
491
|
syncBoardDragFromChessTurn();
|
|
491
492
|
}
|
|
492
493
|
|