@connectorvol/chess-widgets 1.0.0 → 1.1.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/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/puzzle/mergePgnWithSetupFen.d.ts +8 -0
- package/dist/puzzle/mergePgnWithSetupFen.js +28 -0
- package/dist/puzzle/puzzlePreviewConstants.d.ts +4 -0
- package/dist/puzzle/puzzlePreviewConstants.js +4 -0
- package/dist/puzzle-creation/PuzzleBoardTreeViewerPane.svelte +106 -0
- package/dist/puzzle-creation/PuzzleBoardTreeViewerPane.svelte.d.ts +51 -0
- package/dist/puzzle-creation/PuzzleCreationWizard.svelte +6 -39
- package/dist/puzzle-creation/PuzzlePgnBoardTreeEditor.svelte +487 -0
- package/dist/puzzle-creation/PuzzlePgnBoardTreeEditor.svelte.d.ts +5 -0
- package/dist/puzzle-creation/StepMoves.svelte +14 -40
- package/dist/puzzle-creation/StepMoves.svelte.d.ts +1 -1
- package/dist/puzzle-creation/StepPosition.svelte +4 -4
- package/dist/puzzle-creation/StepPreview.svelte +14 -438
- package/dist/puzzle-creation/StepPreview.svelte.d.ts +1 -1
- package/dist/puzzle-creation/createPuzzleLineEditingBoard.d.ts +16 -0
- package/dist/puzzle-creation/createPuzzleLineEditingBoard.js +31 -0
- package/dist/puzzle-creation/types.d.ts +42 -0
- package/package.json +6 -5
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type {
|
|
3
|
+
TPuzzlePgnBoardTreeEditorOutcome,
|
|
4
|
+
TPuzzlePgnBoardTreeEditorProps,
|
|
5
|
+
} from "./types.js";
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import type { IChessBoardActions } from "@connectorvol/chessboard";
|
|
10
|
+
import type { Square } from "@connectorvol/shared";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
CHESSBOARD_THEMES,
|
|
14
|
+
createBoardApi,
|
|
15
|
+
DEFAULT_BOARD_SETTINGS,
|
|
16
|
+
Draggable,
|
|
17
|
+
syncMoveEvaluationNagBadge,
|
|
18
|
+
} from "@connectorvol/chessboard";
|
|
19
|
+
import {
|
|
20
|
+
ChessTree,
|
|
21
|
+
transformPgnToChessNode,
|
|
22
|
+
} from "@connectorvol/tree";
|
|
23
|
+
import { untrack } from "svelte";
|
|
24
|
+
|
|
25
|
+
import { INITIAL_FEN } from "@connectorvol/chessops/fen";
|
|
26
|
+
import { PgnOps } from "@connectorvol/chessops/pgnOps.svelte";
|
|
27
|
+
import { Color, type Move } from "@connectorvol/shared";
|
|
28
|
+
|
|
29
|
+
import { mergePgnWithSetupFen } from "../puzzle/mergePgnWithSetupFen.js";
|
|
30
|
+
import { createEmptyTreeFromFen } from "../puzzle/puzzleData.js";
|
|
31
|
+
import { PREVIEW_WRONG_NO_VARIANT_MESSAGE } from "../puzzle/puzzlePreviewConstants.js";
|
|
32
|
+
import {
|
|
33
|
+
puzzlePreviewClassifySolverMove,
|
|
34
|
+
puzzlePreviewDupSubtreeUnderStudentCursor,
|
|
35
|
+
puzzlePreviewIsSolvedPosition,
|
|
36
|
+
puzzlePreviewNodeAtPath,
|
|
37
|
+
puzzlePreviewSideToMoveFromFen,
|
|
38
|
+
type TSolverMoveOutcome,
|
|
39
|
+
} from "../puzzle/puzzleStepPreviewSolver.js";
|
|
40
|
+
import PuzzleBoardTreeViewerPane from "./PuzzleBoardTreeViewerPane.svelte";
|
|
41
|
+
import { cn } from "../utils.js";
|
|
42
|
+
import type { TPuzzlePgnBoardTreeEditorProps } from "./types.js";
|
|
43
|
+
|
|
44
|
+
let {
|
|
45
|
+
pgn = "",
|
|
46
|
+
fen: fenProp,
|
|
47
|
+
onOutcome,
|
|
48
|
+
boardTheme,
|
|
49
|
+
boardAppearanceSettings,
|
|
50
|
+
class: className,
|
|
51
|
+
wrongFeedback = $bindable<string | null>(null),
|
|
52
|
+
solved = $bindable(false),
|
|
53
|
+
}: TPuzzlePgnBoardTreeEditorProps = $props();
|
|
54
|
+
|
|
55
|
+
/** Корень превью: синхронизируется с эффектом смены PGN/FEN. */
|
|
56
|
+
let layoutInitialFen = $state(INITIAL_FEN);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Представляет цвет стороны решателя по текущему корню превью (`layoutInitialFen`).
|
|
60
|
+
*/
|
|
61
|
+
function puzzleSolverColor(): Color {
|
|
62
|
+
return layoutInitialFen.trim().split(/\s+/)[1] === "b"
|
|
63
|
+
? Color.BLACK
|
|
64
|
+
: Color.WHITE;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let cursorPath = $state<number[]>([]);
|
|
68
|
+
|
|
69
|
+
let wrongRevealTimer: ReturnType<typeof setTimeout> | null = null;
|
|
70
|
+
let advanceLineTimer: ReturnType<typeof setTimeout> | null = null;
|
|
71
|
+
|
|
72
|
+
let previewChess = new PgnOps(INITIAL_FEN, "chess");
|
|
73
|
+
|
|
74
|
+
const studentPreviewTree = new ChessTree(createEmptyTreeFromFen(INITIAL_FEN));
|
|
75
|
+
|
|
76
|
+
let solutionPreviewTree = $state<ChessTree | null>(null);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Представляет синхронизацию доступности перетаскивания с очередью хода и состоянием «решено».
|
|
80
|
+
*/
|
|
81
|
+
function syncBoardDragFromChessTurn(): void {
|
|
82
|
+
const solver = puzzleSolverColor();
|
|
83
|
+
chessboard.draggable = solved
|
|
84
|
+
? Draggable.NONE
|
|
85
|
+
: previewChess.turn() === solver
|
|
86
|
+
? solver === Color.WHITE
|
|
87
|
+
? Draggable.WHITE
|
|
88
|
+
: Draggable.BLACK
|
|
89
|
+
: Draggable.NONE;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Представляет добавление пути в дерево ученика без очистки уже пройденных линий.
|
|
94
|
+
*/
|
|
95
|
+
function ensureStudentPreviewTreeHasPath(path: number[]): void {
|
|
96
|
+
const solution = solutionPreviewTree;
|
|
97
|
+
if (!solution) return;
|
|
98
|
+
|
|
99
|
+
studentPreviewTree.currentNode = studentPreviewTree.rootNode.moves;
|
|
100
|
+
|
|
101
|
+
let src = solution.rootNode.moves;
|
|
102
|
+
for (const idx of path) {
|
|
103
|
+
const next = src.children[idx];
|
|
104
|
+
studentPreviewTree.addNodeToCurrent({
|
|
105
|
+
id: "",
|
|
106
|
+
children: [],
|
|
107
|
+
data: { ...next.data },
|
|
108
|
+
});
|
|
109
|
+
src = next;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Представляет обработку ошибочного хода в превью.
|
|
115
|
+
*/
|
|
116
|
+
function handleWrongOutcome(outcome: TSolverMoveOutcome): void {
|
|
117
|
+
const solution = solutionPreviewTree;
|
|
118
|
+
if (!solution) return;
|
|
119
|
+
|
|
120
|
+
if (wrongRevealTimer !== null) {
|
|
121
|
+
clearTimeout(wrongRevealTimer);
|
|
122
|
+
wrongRevealTimer = null;
|
|
123
|
+
}
|
|
124
|
+
if (advanceLineTimer !== null) {
|
|
125
|
+
clearTimeout(advanceLineTimer);
|
|
126
|
+
advanceLineTimer = null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (outcome.kind === "wrong_marked_variant") {
|
|
130
|
+
wrongFeedback =
|
|
131
|
+
"Неверно. Открываю введённый вариант в дереве, ход отменён.";
|
|
132
|
+
onOutcome?.("failed");
|
|
133
|
+
ensureStudentPreviewTreeHasPath(cursorPath);
|
|
134
|
+
puzzlePreviewDupSubtreeUnderStudentCursor(
|
|
135
|
+
studentPreviewTree,
|
|
136
|
+
outcome.wrongBranchRoot,
|
|
137
|
+
);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
wrongFeedback = PREVIEW_WRONG_NO_VARIANT_MESSAGE;
|
|
142
|
+
onOutcome?.("failed");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Представляет установку позиции и дерева ученика по пути из корня.
|
|
147
|
+
*/
|
|
148
|
+
function setStateFromPath(path: number[]): void {
|
|
149
|
+
const solution = solutionPreviewTree;
|
|
150
|
+
if (!solution) return;
|
|
151
|
+
|
|
152
|
+
previewChess.setFen(layoutInitialFen);
|
|
153
|
+
let node = solution.rootNode.moves;
|
|
154
|
+
for (const idx of path) {
|
|
155
|
+
const child = node.children[idx]!;
|
|
156
|
+
previewChess.makeSanMove(child.data.san);
|
|
157
|
+
node = child;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
cursorPath = path;
|
|
161
|
+
ensureStudentPreviewTreeHasPath(cursorPath);
|
|
162
|
+
chessboard.fen = previewChess.fen();
|
|
163
|
+
syncBoardDragFromChessTurn();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Представляет автоматический ход соперника по главному варианту после хода решателя.
|
|
168
|
+
*/
|
|
169
|
+
function applyOpponentMainLineMove(pathAfterSolver: number[]): number[] {
|
|
170
|
+
const solution = solutionPreviewTree;
|
|
171
|
+
if (!solution) return pathAfterSolver;
|
|
172
|
+
|
|
173
|
+
if (puzzlePreviewSideToMoveFromFen(previewChess.fen()) === puzzleSolverColor()) {
|
|
174
|
+
return pathAfterSolver;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const node = puzzlePreviewNodeAtPath(
|
|
178
|
+
solution.rootNode.moves,
|
|
179
|
+
pathAfterSolver,
|
|
180
|
+
);
|
|
181
|
+
if (node.children.length === 0) return pathAfterSolver;
|
|
182
|
+
|
|
183
|
+
previewChess.makeSanMove(node.children[0]!.data.san);
|
|
184
|
+
return [...pathAfterSolver, 0];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Представляет поиск следующей линии по развилкам соперника (DFS: чем глубже развилка — тем раньше возврат).
|
|
189
|
+
*/
|
|
190
|
+
function findNextOpponentLine(path: number[]): number[] | null {
|
|
191
|
+
const solution = solutionPreviewTree;
|
|
192
|
+
if (!solution) return null;
|
|
193
|
+
|
|
194
|
+
const solver = puzzleSolverColor();
|
|
195
|
+
|
|
196
|
+
let node = solution.rootNode.moves;
|
|
197
|
+
for (let depth = 0; depth <= path.length; depth++) {
|
|
198
|
+
const sideToMove = puzzlePreviewSideToMoveFromFen(node.data.fen);
|
|
199
|
+
const isOpponentToMove = sideToMove !== solver;
|
|
200
|
+
const isFork = node.children.length > 1;
|
|
201
|
+
|
|
202
|
+
if (isOpponentToMove && isFork && depth < path.length) {
|
|
203
|
+
const chosenIdx = path[depth]!;
|
|
204
|
+
if (chosenIdx < node.children.length - 1) {
|
|
205
|
+
/* кандидат на «следующую линию» с этого форка — ниже второй проход */
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (depth === path.length) break;
|
|
210
|
+
node = node.children[path[depth]!]!;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
node = solution.rootNode.moves;
|
|
214
|
+
let bestForkDepth: number | null = null;
|
|
215
|
+
let bestNextIdx = 0;
|
|
216
|
+
for (let depth = 0; depth < path.length; depth++) {
|
|
217
|
+
const sideToMove = puzzlePreviewSideToMoveFromFen(node.data.fen);
|
|
218
|
+
const isOpponentToMove = sideToMove !== solver;
|
|
219
|
+
if (isOpponentToMove && node.children.length > 1) {
|
|
220
|
+
const chosenIdx = path[depth]!;
|
|
221
|
+
if (chosenIdx < node.children.length - 1) {
|
|
222
|
+
bestForkDepth = depth;
|
|
223
|
+
bestNextIdx = chosenIdx + 1;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
node = node.children[path[depth]!]!;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (bestForkDepth === null) return null;
|
|
230
|
+
return [...path.slice(0, bestForkDepth), bestNextIdx];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Представляет переход на следующую линию ответов соперника, если возможно.
|
|
235
|
+
*/
|
|
236
|
+
function maybeAdvanceToNextOpponentLine(): boolean {
|
|
237
|
+
const nextBase = findNextOpponentLine(cursorPath);
|
|
238
|
+
if (!nextBase) return false;
|
|
239
|
+
|
|
240
|
+
solved = false;
|
|
241
|
+
wrongFeedback = null;
|
|
242
|
+
|
|
243
|
+
setStateFromPath(nextBase);
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Представляет попытку применить SAN хода решателя с автоответами соперника.
|
|
249
|
+
*/
|
|
250
|
+
function tryApplySolverSan(san: string): Move | undefined {
|
|
251
|
+
const solution = solutionPreviewTree;
|
|
252
|
+
if (!solution || solved) return undefined;
|
|
253
|
+
|
|
254
|
+
const cursorNode = puzzlePreviewNodeAtPath(
|
|
255
|
+
solution.rootNode.moves,
|
|
256
|
+
cursorPath,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const outcome = puzzlePreviewClassifySolverMove(
|
|
260
|
+
cursorNode,
|
|
261
|
+
previewChess.fen(),
|
|
262
|
+
san,
|
|
263
|
+
puzzleSolverColor(),
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (outcome.kind !== "correct") {
|
|
267
|
+
handleWrongOutcome(outcome);
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
let path = [...cursorPath, outcome.childIndex];
|
|
273
|
+
const moveRecord = previewChess.makeSanMove(san);
|
|
274
|
+
path = applyOpponentMainLineMove(path);
|
|
275
|
+
setStateFromPath(path);
|
|
276
|
+
|
|
277
|
+
const leaf = puzzlePreviewNodeAtPath(solution.rootNode.moves, cursorPath);
|
|
278
|
+
if (puzzlePreviewIsSolvedPosition(previewChess, leaf, puzzleSolverColor())) {
|
|
279
|
+
if (advanceLineTimer !== null) {
|
|
280
|
+
clearTimeout(advanceLineTimer);
|
|
281
|
+
advanceLineTimer = null;
|
|
282
|
+
}
|
|
283
|
+
advanceLineTimer = setTimeout(() => {
|
|
284
|
+
const advanced = maybeAdvanceToNextOpponentLine();
|
|
285
|
+
if (!advanced) {
|
|
286
|
+
solved = true;
|
|
287
|
+
onOutcome?.("solved");
|
|
288
|
+
}
|
|
289
|
+
syncBoardDragFromChessTurn();
|
|
290
|
+
advanceLineTimer = null;
|
|
291
|
+
}, 250);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
wrongFeedback = null;
|
|
295
|
+
syncBoardDragFromChessTurn();
|
|
296
|
+
|
|
297
|
+
return moveRecord;
|
|
298
|
+
} catch {
|
|
299
|
+
wrongFeedback = "Ход недопустим в этой позиции.";
|
|
300
|
+
onOutcome?.("failed");
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const actions: IChessBoardActions = {
|
|
306
|
+
game: {
|
|
307
|
+
possibleMovesOnSquare: (square: Square) => previewChess.moves(square),
|
|
308
|
+
beforePieceMoveSan(san: string) {
|
|
309
|
+
const move = tryApplySolverSan(san);
|
|
310
|
+
if (!move) return undefined;
|
|
311
|
+
return {
|
|
312
|
+
move,
|
|
313
|
+
fen: previewChess.fen(),
|
|
314
|
+
turn: previewChess.turn(),
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
afterPieceMoveSan: () => {
|
|
318
|
+
syncBoardDragFromChessTurn();
|
|
319
|
+
},
|
|
320
|
+
beforePieceMove: (from, to, promotion) => {
|
|
321
|
+
const san = previewChess.getSanForMove({ from, to, promotion });
|
|
322
|
+
const move = tryApplySolverSan(san);
|
|
323
|
+
if (!move) return "";
|
|
324
|
+
return previewChess.fen();
|
|
325
|
+
},
|
|
326
|
+
afterPieceMove: () => {
|
|
327
|
+
syncBoardDragFromChessTurn();
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
let chessboard = $derived(
|
|
333
|
+
createBoardApi({
|
|
334
|
+
fen: INITIAL_FEN,
|
|
335
|
+
settings: {
|
|
336
|
+
...DEFAULT_BOARD_SETTINGS,
|
|
337
|
+
...(boardAppearanceSettings ?? {}),
|
|
338
|
+
boardSize: 38,
|
|
339
|
+
isResizable: false,
|
|
340
|
+
orientation: Color.WHITE,
|
|
341
|
+
draggable: Draggable.WHITE,
|
|
342
|
+
},
|
|
343
|
+
actions,
|
|
344
|
+
theme: boardTheme ?? CHESSBOARD_THEMES.blue,
|
|
345
|
+
}),
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
const treeForViewer = $derived(
|
|
349
|
+
solved && solutionPreviewTree ? solutionPreviewTree : studentPreviewTree,
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Представляет синхронизацию выбранного узла в полном дереве решения с пройденной линией при переходе в режим просмотра (`solved`).
|
|
354
|
+
* Без этого после switch с `studentPreviewTree` у `solutionPreviewTree` остаётся `currentNode` у корня и подсветка в TreeViewer неверна.
|
|
355
|
+
*/
|
|
356
|
+
$effect(() => {
|
|
357
|
+
if (!solved || !solutionPreviewTree) return;
|
|
358
|
+
const tree = solutionPreviewTree;
|
|
359
|
+
const path = cursorPath;
|
|
360
|
+
tree.currentNode = puzzlePreviewNodeAtPath(tree.rootNode.moves, path);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
$effect(() => {
|
|
364
|
+
const trimmedPgn = pgn.trim();
|
|
365
|
+
const rootFen = fenProp?.trim() ? fenProp.trim() : INITIAL_FEN;
|
|
366
|
+
|
|
367
|
+
if (!trimmedPgn) {
|
|
368
|
+
untrack(() => {
|
|
369
|
+
layoutInitialFen = rootFen;
|
|
370
|
+
solutionPreviewTree = null;
|
|
371
|
+
previewChess.setFen(rootFen);
|
|
372
|
+
studentPreviewTree.replaceRootTree(createEmptyTreeFromFen(rootFen));
|
|
373
|
+
studentPreviewTree.currentNode = studentPreviewTree.rootNode.moves;
|
|
374
|
+
chessboard.fen = rootFen;
|
|
375
|
+
chessboard.orientation =
|
|
376
|
+
rootFen.trim().split(/\s+/)[1] === "b"
|
|
377
|
+
? Color.BLACK
|
|
378
|
+
: Color.WHITE;
|
|
379
|
+
cursorPath = [];
|
|
380
|
+
solved = false;
|
|
381
|
+
wrongFeedback = null;
|
|
382
|
+
syncBoardDragFromChessTurn();
|
|
383
|
+
});
|
|
384
|
+
return () => clearTimers();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
untrack(() => {
|
|
388
|
+
const fullPgn = mergePgnWithSetupFen(trimmedPgn, rootFen);
|
|
389
|
+
const { rootNode } = transformPgnToChessNode(fullPgn);
|
|
390
|
+
|
|
391
|
+
layoutInitialFen = rootFen;
|
|
392
|
+
solutionPreviewTree = new ChessTree(rootNode);
|
|
393
|
+
|
|
394
|
+
cursorPath = [];
|
|
395
|
+
solved = false;
|
|
396
|
+
wrongFeedback = null;
|
|
397
|
+
previewChess.setFen(rootFen);
|
|
398
|
+
studentPreviewTree.replaceRootTree(createEmptyTreeFromFen(rootFen));
|
|
399
|
+
studentPreviewTree.currentNode = studentPreviewTree.rootNode.moves;
|
|
400
|
+
chessboard.fen = rootFen;
|
|
401
|
+
chessboard.orientation =
|
|
402
|
+
rootFen.trim().split(/\s+/)[1] === "b" ? Color.BLACK : Color.WHITE;
|
|
403
|
+
syncBoardDragFromChessTurn();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
return () => clearTimers();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Представляет сброс отложенных таймеров превью.
|
|
411
|
+
*/
|
|
412
|
+
function clearTimers(): void {
|
|
413
|
+
if (wrongRevealTimer !== null) {
|
|
414
|
+
clearTimeout(wrongRevealTimer);
|
|
415
|
+
wrongRevealTimer = null;
|
|
416
|
+
}
|
|
417
|
+
if (advanceLineTimer !== null) {
|
|
418
|
+
clearTimeout(advanceLineTimer);
|
|
419
|
+
advanceLineTimer = null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Представляет синхронизацию доски с текущим узлом полного дерева решения (после `solved`).
|
|
425
|
+
*/
|
|
426
|
+
function syncBoardFromSolutionTreeNode(): void {
|
|
427
|
+
const tree = solutionPreviewTree;
|
|
428
|
+
if (!tree || !solved) return;
|
|
429
|
+
previewChess.setFen(tree.currentNode.data.fen);
|
|
430
|
+
chessboard.fen = previewChess.fen();
|
|
431
|
+
chessboard.addLastMove(tree.currentNode.data.lastMove ?? null);
|
|
432
|
+
syncMoveEvaluationNagBadge(chessboard, tree.currentNode.data);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Представляет установку FEN движка при навигации по дереву (только после полного решения превью).
|
|
437
|
+
*/
|
|
438
|
+
function treeSetChessFen(fen: string): void {
|
|
439
|
+
if (!solved) return;
|
|
440
|
+
previewChess.setFen(fen);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Представляет обновление UI доски после смены узла в дереве решения.
|
|
445
|
+
*/
|
|
446
|
+
function treeSetChessboardFen(_animationTime?: number): void {
|
|
447
|
+
if (!solved) return;
|
|
448
|
+
syncBoardFromSolutionTreeNode();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Представляет реакцию на выбор узла в дереве (клик по ходу после решения).
|
|
453
|
+
*/
|
|
454
|
+
function treeOnSelectNode(): void {
|
|
455
|
+
if (!solved) return;
|
|
456
|
+
syncBoardFromSolutionTreeNode();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Представляет синхронизацию доски после операций с вариантами (в режиме просмотра без правки не используется).
|
|
461
|
+
*/
|
|
462
|
+
function treeOnDeleteVariant(): void {
|
|
463
|
+
if (!solved) return;
|
|
464
|
+
syncBoardFromSolutionTreeNode();
|
|
465
|
+
}
|
|
466
|
+
</script>
|
|
467
|
+
|
|
468
|
+
<div class={["flex flex-col gap-2", className]}>
|
|
469
|
+
<PuzzleBoardTreeViewerPane
|
|
470
|
+
{chessboard}
|
|
471
|
+
chessTree={treeForViewer}
|
|
472
|
+
onSelectNode={treeOnSelectNode}
|
|
473
|
+
onDeleteVariant={treeOnDeleteVariant}
|
|
474
|
+
setChessFen={treeSetChessFen}
|
|
475
|
+
setChessboardFen={treeSetChessboardFen}
|
|
476
|
+
editMode={false}
|
|
477
|
+
selectable={solved}
|
|
478
|
+
>
|
|
479
|
+
{#snippet belowChessboard()}
|
|
480
|
+
{#if wrongFeedback && wrongFeedback !== PREVIEW_WRONG_NO_VARIANT_MESSAGE}
|
|
481
|
+
<p class="text-sm font-medium text-amber-600 dark:text-amber-400">
|
|
482
|
+
{wrongFeedback}
|
|
483
|
+
</p>
|
|
484
|
+
{/if}
|
|
485
|
+
{/snippet}
|
|
486
|
+
</PuzzleBoardTreeViewerPane>
|
|
487
|
+
</div>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { TPuzzlePgnBoardTreeEditorOutcome, TPuzzlePgnBoardTreeEditorProps, } from "./types.js";
|
|
2
|
+
import type { TPuzzlePgnBoardTreeEditorProps } from "./types.js";
|
|
3
|
+
declare const PuzzlePgnBoardTreeEditor: import("svelte").Component<TPuzzlePgnBoardTreeEditorProps, {}, "solved" | "wrongFeedback">;
|
|
4
|
+
type PuzzlePgnBoardTreeEditor = ReturnType<typeof PuzzlePgnBoardTreeEditor>;
|
|
5
|
+
export default PuzzlePgnBoardTreeEditor;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { BoardApi } from "@connectorvol/chessboard";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import { ChessTree
|
|
6
|
-
|
|
4
|
+
import { Draggable } from "@connectorvol/chessboard";
|
|
5
|
+
import type { ChessTree } from "@connectorvol/tree";
|
|
6
|
+
|
|
7
|
+
import { Color } from "@connectorvol/shared";
|
|
7
8
|
import { Popover } from "bits-ui";
|
|
9
|
+
|
|
8
10
|
import { buttonVariants } from "../button-variants.js";
|
|
11
|
+
import PuzzleBoardTreeViewerPane from "./PuzzleBoardTreeViewerPane.svelte";
|
|
9
12
|
import { cn } from "../utils.js";
|
|
10
13
|
|
|
11
14
|
import type { PgnOps } from "@connectorvol/chessops/pgnOps.svelte";
|
|
@@ -75,9 +78,6 @@
|
|
|
75
78
|
return null;
|
|
76
79
|
});
|
|
77
80
|
|
|
78
|
-
/** Высота блока с шахматной доской (px) для выравнивания панели TreeViewer по высоте доски на lg. */
|
|
79
|
-
let chessboardBlockHeight = $state(0);
|
|
80
|
-
|
|
81
81
|
function handleNext() {
|
|
82
82
|
if (canProceedToNextStep) {
|
|
83
83
|
onNext(mainLine);
|
|
@@ -170,40 +170,14 @@
|
|
|
170
170
|
{/if}
|
|
171
171
|
</div>
|
|
172
172
|
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
<div class="flex min-h-0 w-full flex-1 flex-col gap-2 lg:min-w-0">
|
|
183
|
-
<div
|
|
184
|
-
class={cn(
|
|
185
|
-
"flex min-w-0 flex-col overflow-hidden rounded-md border border-border",
|
|
186
|
-
chessboardBlockHeight > 0
|
|
187
|
-
? "min-h-0 shrink-0"
|
|
188
|
-
: "min-h-[12rem] max-h-[min(65vh,36rem)]",
|
|
189
|
-
)}
|
|
190
|
-
style:height={chessboardBlockHeight > 0
|
|
191
|
-
? `${chessboardBlockHeight}px`
|
|
192
|
-
: undefined}
|
|
193
|
-
>
|
|
194
|
-
<TreeViewer
|
|
195
|
-
chessTree={tree}
|
|
196
|
-
{onSelectNode}
|
|
197
|
-
{onDeleteVariant}
|
|
198
|
-
{setChessFen}
|
|
199
|
-
{setChessboardFen}
|
|
200
|
-
pieceSet={DEFAULT_PIECE_SET}
|
|
201
|
-
className="min-h-0 flex-1 border-x-0 border-t-0"
|
|
202
|
-
editMode={true}
|
|
203
|
-
/>
|
|
204
|
-
</div>
|
|
205
|
-
</div>
|
|
206
|
-
</div>
|
|
173
|
+
<PuzzleBoardTreeViewerPane
|
|
174
|
+
{chessboard}
|
|
175
|
+
chessTree={tree}
|
|
176
|
+
{onSelectNode}
|
|
177
|
+
{onDeleteVariant}
|
|
178
|
+
{setChessFen}
|
|
179
|
+
{setChessboardFen}
|
|
180
|
+
/>
|
|
207
181
|
|
|
208
182
|
<div class="flex justify-between">
|
|
209
183
|
<button
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BoardApi } from "@connectorvol/chessboard";
|
|
2
|
-
import { ChessTree } from "@connectorvol/tree";
|
|
2
|
+
import type { ChessTree } from "@connectorvol/tree";
|
|
3
3
|
import type { PgnOps } from "@connectorvol/chessops/pgnOps.svelte";
|
|
4
4
|
import { type PuzzleData } from "../puzzle/puzzleData.js";
|
|
5
5
|
interface Props {
|
|
@@ -29,15 +29,15 @@
|
|
|
29
29
|
const props: Props = $props();
|
|
30
30
|
|
|
31
31
|
/** Настройки доски мастера задачи: фиксированный размер, без ручного resize. */
|
|
32
|
-
const puzzleStepBoardSettings = {
|
|
32
|
+
const puzzleStepBoardSettings = $derived({
|
|
33
33
|
...DEFAULT_EDITABLE_BOARD_SETTINGS,
|
|
34
34
|
...(props.boardAppearanceSettings ?? {}),
|
|
35
35
|
boardSize: 29,
|
|
36
36
|
isResizable: false,
|
|
37
37
|
editSettings: DEFAULT_EDITABLE_BOARD_SETTINGS.editSettings,
|
|
38
|
-
};
|
|
38
|
+
});
|
|
39
39
|
|
|
40
|
-
let chessboard = $
|
|
40
|
+
let chessboard = $derived(
|
|
41
41
|
createBoardApi({
|
|
42
42
|
fen: (() => props.initialFen)(),
|
|
43
43
|
settings: puzzleStepBoardSettings,
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
);
|
|
47
47
|
|
|
48
48
|
/** Один объект Fen на доску: `$derived(new Fen(...))` пересоздавал бы класс при смене orientation и сбрасывал бы «Установить ход». */
|
|
49
|
-
const fen = new Fen(chessboard);
|
|
49
|
+
const fen = $derived(new Fen(chessboard));
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* Представляет подстановку полей FEN (ход, рокировка, счётчики) из строки родителя при смене стартовой позиции.
|